import { EventEmitter } from 'eventemitter3' ; import util from '../common/util' ; import FD , { Entity as FD_Entity } from './factorioData' ; import { Blueprint } from './Blueprint' ; import { getBeltWireConnectionIndex } from './spriteDataBuilder' ; import U from './generators/util' ; export interface IFilter { /** Slot index (1 based ... not 0 like arrays) */ index : number ; /** Name of entity to be filtered */ name : string ; /** If stacking is allowed, how many shall be stacked */ count ? : number ; } /** Entity Base Class */ export class Entity extends EventEmitter { /** Field to hold raw entity */ private readonly m_rawEntity : BPS . IEntity ; /** Field to hold reference to blueprint */ private readonly m_BP : Blueprint ; param rawEntity param blueprint public constructor ( rawEntity , blueprint ) { super ( ) ; this . m_BP = blueprint ; this . m_rawEntity = rawEntity ; } public static getItemName ( name ) { return FD . entities [ name ] . minable . result ; } public destroy ( ) { this . emit ( 'destroy' ) ; this . removeAllListeners ( ) ; } /** Return reference to blueprint */ public get Blueprint ( ) : Blueprint { return this . m_BP ; } /** Entity Number */ public get entityNumber ( ) : number { return this . m_rawEntity . entity_number ; } /** Entity Name */ public get name ( ) : string { return this . m_rawEntity . name ; } /** Entity Type */ public get type ( ) : string { return FD . entities [ this . name ] . type ; } /** Direct access to entity meta data from core */ public get entityData ( ) : FD_Entity { return FD . entities [ this . name ] ; } /** Entity size */ public get size ( ) : IPoint { return util . switchSizeBasedOnDirection ( this . entityData . size , this . direction ) ; } /** Entity position */ public get position ( ) : IPoint { return this . m_rawEntity . position ; } public set position ( position ) { if ( util . areObjectsEquivalent ( this . m_rawEntity . position , position ) ) return ; if ( ! this . m_BP . entityPositionGrid . canMoveTo ( this , position ) ) return ; const connectionsReach = this . m_BP . wireConnections . getEntityConnections ( this . entityNumber ) . map ( c => ( c . entityNumber1 === this . entityNumber ? c . entityNumber2 : c . entityNumber1 ) ) . map ( otherEntityNumer => this . m_BP . entities . get ( otherEntityNumer ) ) . every ( e => U . pointInCircle ( e . position , position , Math . min ( e . maxWireDistance , this . maxWireDistance ) ) ) ; if ( ! connectionsReach ) return ; this . m_BP . history . updateValue ( this . m_rawEntity , [ 'position' ] , position , 'Change position' ) . onDone ( ( newValue , oldValue ) => { this . m_BP . entityPositionGrid . removeTileData ( this , oldValue ) ; this . m_BP . entityPositionGrid . setTileData ( this , newValue ) ; this . emit ( 'position' , newValue , oldValue ) ; } ) . commit ( ) ; } public get maxWireDistance ( ) : number { return ( this . entityData . circuit_wire_max_distance || this . entityData . wire_max_distance || this . entityData . maximum_wire_distance ) ; } public moveBy ( offset ) { this . position = { x : this . position . x + offset . x , y : this . position . y + offset . y , } ; } /** Entity direction */ public get direction ( ) : number { if ( this . type === 'electric_pole' ) { return this . m_BP . wireConnections . getPowerPoleDirection ( this . entityNumber ) ; } return this . m_rawEntity . direction === undefined ? 0 : this . m_rawEntity . direction ; } public set direction ( direction ) { if ( this . m_rawEntity . direction === direction ) return ; this . m_BP . history . updateValue ( this . m_rawEntity , [ 'direction' ] , direction , 'Change direction' ) . onDone ( ( ) => this . emit ( 'direction' ) ) . commit ( ) ; } /** Direction Type (input|output) for underground belts */ public get directionType ( ) : 'input' | 'output' { return this . m_rawEntity . type ; } public set directionType ( type ) { if ( this . m_rawEntity . type === type ) return ; this . m_BP . history . updateValue ( this . m_rawEntity , [ 'type' ] , type , 'Change direction type' ) . onDone ( ( ) => this . emit ( 'directionType' ) ) . commit ( ) ; } /** Entity recipe */ public get recipe ( ) : string { return this . m_rawEntity . recipe ; } public set recipe ( recipe ) { if ( this . m_rawEntity . recipe === recipe ) return ; this . m_BP . history . startTransaction ( ) ; this . m_BP . history . updateValue ( this . m_rawEntity , [ 'recipe' ] , recipe , 'Change recipe' ) . onDone ( r => this . emit ( 'recipe' , r ) ) . commit ( ) ; if ( recipe !== undefined ) { this . modules = this . modules . map ( k => FD . items [ k ] ) . filter ( item => ! ( item . limitation !== undefined && ! item . limitation . includes ( recipe ) ) ) . map ( item => item . name ) ; } this . m_BP . history . commitTransaction ( ) ; } /** Recipes this entity can accept */ public get acceptedRecipes ( ) : string [ ] { if ( this . entityData . crafting_categories === undefined ) return [ ] ; return Object . keys ( FD . recipes ) . map ( k => FD . recipes [ k ] ) . filter ( recipe => this . entityData . crafting_categories . includes ( recipe . category ) ) . map ( recipe => recipe . name ) ; } /** Count of module slots */ public get moduleSlots ( ) : number { if ( this . entityData . module_specification === undefined ) return 0 ; return this . entityData . module_specification . module_slots ; } /** Modules this entity can accept */ public get acceptedModules ( ) : string [ ] { if ( this . entityData . module_specification === undefined ) return [ ] ; return ( FD . getModulesFor ( this . name ) . filter ( item => ! this . recipe || ! ( item . limitation && ! item . limitation . includes ( this . recipe ) ) ) . map ( item => item . name ) ) ; } /** Filters this entity can accept (only splitters, inserters and logistic chests) */ public get acceptedFilters ( ) : string [ ] { if ( this . filterSlots === 0 ) return [ ] ; return Object . keys ( FD . items ) . map ( k => FD . items [ k ] ) . map ( item => item . name ) ; } /** List of all modules */ public get modules ( ) : string [ ] { const modulesObj = this . m_rawEntity . items ; if ( modulesObj === undefined || Object . keys ( modulesObj ) . length === 0 ) return [ ] ; return Object . keys ( modulesObj ) . flatMap ( k => Array < string > ( modulesObj [ k ] ) . fill ( k ) ) ; } public set modules ( modules ) { if ( util . equalArrays ( this . modules , modules ) ) return ; const ms = { } ; for ( const m of modules ) { if ( m ) { ms [ m ] = ms [ m ] ? ms [ m ] + 1 : 1 ; } } this . m_BP . history . updateValue ( this . m_rawEntity , [ 'items' ] , ms , 'Change modules' ) . onDone ( ( ) => this . emit ( 'modules' , this . modules ) ) . commit ( ) ; } /** Count of filter slots */ public get filterSlots ( ) : number { if ( this . name . includes ( 'splitter' ) ) return 1 ; if ( this . entityData . filter_count !== undefined ) return this . entityData . filter_count ; if ( this . entityData . max_logistic_slots !== undefined ) { return this . entityData . max_logistic_slots ; } if ( this . name === 'logistic_chest_buffer' || this . name === 'logistic_chest_requester' ) { return this . logisticChestFilters . reduce ( ( max , filter ) => Math . max ( max , filter . index ) , 30 ) ; } return 0 ; } /** List of all filter(s) for splitters, inserters and logistic chests */ public get filters ( ) : IFilter [ ] { switch ( this . name ) { case 'splitter' : case 'fast_splitter' : case 'express_splitter' : { return this . splitterFilter ; } case 'filter_inserter' : case 'stack_filter_inserter' : { return this . inserterFilters ; } case 'logistic_chest_storage' : case 'logistic_chest_requester' : case 'logistic_chest_buffer' : return this . logisticChestFilters ; case 'infinity_chest' : return this . infinityChestFilters ; case 'infinity_pipe' : return this . infinityPipeFilters ; default : { return undefined ; } } } public set filters ( list ) { const FILTERS = list === undefined || list . length === 0 ? undefined : list . filter ( f => ! ! f . name ) ; switch ( this . name ) { case 'splitter' : case 'fast_splitter' : case 'express_splitter' : { this . splitterFilter = FILTERS ; return ; } case 'filter_inserter' : case 'stack_filter_inserter' : { this . inserterFilters = FILTERS ; return ; } case 'logistic_chest_storage' : case 'logistic_chest_requester' : case 'logistic_chest_buffer' : { this . logisticChestFilters = FILTERS ; } } } /** Splitter input priority */ public get splitterInputPriority ( ) : string { return this . m_rawEntity . input_priority ; } public set splitterInputPriority ( priority ) { if ( this . m_rawEntity . input_priority === priority ) return ; this . m_BP . history . updateValue ( this . m_rawEntity , [ 'input_priority' ] , priority , 'Change splitter input priority' ) . onDone ( ( ) => this . emit ( 'splitterInputPriority' , this . splitterInputPriority ) ) . commit ( ) ; } /** Splitter output priority */ public get splitterOutputPriority ( ) : string { return this . m_rawEntity . output_priority ; } public set splitterOutputPriority ( priority ) { if ( this . m_rawEntity . output_priority === priority ) return ; this . m_BP . history . startTransaction ( ) ; this . m_BP . history . updateValue ( this . m_rawEntity , [ 'output_priority' ] , priority , 'Change splitter output priority' ) . onDone ( ( ) => this . emit ( 'splitterOutputPriority' , this . splitterOutputPriority ) ) . commit ( ) ; if ( priority === undefined ) { this . filters = undefined ; } this . m_BP . history . commitTransaction ( ) ; } /** Splitter filter */ private get splitterFilter ( ) : IFilter [ ] { if ( ! this . m_rawEntity . filter ) return [ ] ; return [ { index : 1 , name : this . m_rawEntity . filter } ] ; } private set splitterFilter ( filters ) { const filter = filters === undefined ? undefined : filters [ 0 ] . name ; if ( this . m_rawEntity . filter === filter ) return ; this . m_BP . history . startTransaction ( ) ; this . m_BP . history . updateValue ( this . m_rawEntity , [ 'filter' ] , filter , 'Change splitter filter' ) . onDone ( ( ) => this . emit ( 'splitterFilter' ) ) . onDone ( ( ) => this . emit ( 'filters' ) ) . commit ( ) ; if ( this . splitterOutputPriority === undefined ) { this . splitterOutputPriority = 'left' ; } this . m_BP . history . commitTransaction ( ) ; } public get filterMode ( ) : 'whitelist' | 'blacklist' { return this . m_rawEntity . filter_mode === 'blacklist' ? 'blacklist' : 'whitelist' ; } public set filterMode ( filterMode ) { const mode = filterMode === 'blacklist' ? 'blacklist' : undefined ; this . m_BP . history . updateValue ( this . m_rawEntity , [ 'filter_mode' ] , mode , 'Change filter mode' ) . onDone ( ( ) => this . emit ( 'filterMode' , this . filterMode ) ) . commit ( ) ; } /** Inserter filter */ private get inserterFilters ( ) : IFilter [ ] { return this . m_rawEntity . filters ; } private set inserterFilters ( filters ) { if ( filters === undefined && this . m_rawEntity . filters === undefined ) return ; if ( util . areArraysEquivalent ( filters , this . m_rawEntity . filters ) ) return ; this . m_BP . history . updateValue ( this . m_rawEntity , [ 'filters' ] , filters , 'Change inserter filter' ) . onDone ( ( ) => this . emit ( 'inserterFilters' ) ) . onDone ( ( ) => this . emit ( 'filters' ) ) . commit ( ) ; } /** Logistic chest filters */ private get logisticChestFilters ( ) : IFilter [ ] { return this . m_rawEntity . request_filters || [ ] ; } private set logisticChestFilters ( filters ) { if ( filters === undefined && this . m_rawEntity . request_filters === undefined ) return ; if ( util . areArraysEquivalent ( filters , this . m_rawEntity . request_filters ) ) return ; this . m_BP . history . updateValue ( this . m_rawEntity , [ 'request_filters' ] , filters , 'Change chest filter' ) . onDone ( ( ) => this . emit ( 'logisticChestFilters' ) ) . onDone ( ( ) => this . emit ( 'filters' ) ) . commit ( ) ; } private get infinityChestFilters ( ) : IFilter [ ] { if ( ! this . m_rawEntity . infinity_settings ) return [ ] ; return this . m_rawEntity . infinity_settings . filters ; } private get infinityPipeFilters ( ) : IFilter [ ] { if ( ! this . m_rawEntity . infinity_settings ) return [ ] ; return [ { name : this . m_rawEntity . infinity_settings . name , index : 1 } ] ; } /** Requester chest - request from buffer chest */ public get requestFromBufferChest ( ) : boolean { return this . m_rawEntity . request_from_buffers ; } public set requestFromBufferChest ( request ) { if ( this . m_rawEntity . request_from_buffers === request ) return ; this . m_BP . history . updateValue ( this . m_rawEntity , [ 'request_from_buffers' ] , request , 'Change request from buffer chest' ) . onDone ( ( ) => this . emit ( 'requestFromBufferChest' ) ) . commit ( ) ; } public get inserterStackSize ( ) : number { if ( this . m_rawEntity . override_stack_size ) return this . m_rawEntity . override_stack_size ; if ( this . name . includes ( 'stack' ) ) return 12 ; return 3 ; } public get constantCombinatorFilters ( ) : BPS . IConstantCombinatorFilter [ ] { return this . m_rawEntity . control_behavior === undefined ? undefined : this . m_rawEntity . control_behavior . filters ; } public get deciderCombinatorConditions ( ) : BPS . IDeciderCondition { return this . m_rawEntity . control_behavior === undefined ? undefined : this . m_rawEntity . control_behavior . decider_conditions ; } public get arithmeticCombinatorConditions ( ) : BPS . IArithmeticCondition { return this . m_rawEntity . control_behavior === undefined ? undefined : this . m_rawEntity . control_behavior . arithmetic_conditions ; } public get generateConnector ( ) : boolean { return this . hasConnections || this . connectToLogisticNetwork ; } private get connectToLogisticNetwork ( ) : boolean { return ( this . m_rawEntity . control_behavior && this . m_rawEntity . control_behavior . connect_to_logistic_network ) ; } private get hasConnections ( ) : boolean { return this . m_BP . wireConnections . getEntityConnections ( this . entityNumber ) . length > 0 ; } public get chemicalPlantDontConnectOutput ( ) : boolean { if ( ! this . recipe ) return false ; return ! FD . recipes [ this . recipe ] . results . find ( result => result . type === 'fluid' ) ; } public get trainStopColor ( ) : BPS . IColor { return this . m_rawEntity . color ; } public get operator ( ) : string { if ( this . name === 'decider_combinator' ) { const cb = this . m_rawEntity . control_behavior ; if ( cb ) { return cb . decider_conditions === undefined ? undefined : cb . decider_conditions . comparator ; } } if ( this . name === 'arithmetic_combinator' ) { const cb = this . m_rawEntity . control_behavior ; if ( cb ) { return cb . arithmetic_conditions === undefined ? undefined : cb . arithmetic_conditions . operation ; } } return undefined ; } private get canBeRotated ( ) : boolean { if ( this . name === 'assembling_machine_2' || this . name === 'assembling_machine_3' ) { return this . assemblerCraftsWithFluid ; } return ( this . entityData . possible_rotations !== undefined && ! this . m_BP . entityPositionGrid . sharesCell ( { x : this . position . x , y : this . position . y , w : this . size . x , h : this . size . y , } ) ) ; } public rotate ( ccw = false , rotateOpposingUB = false ) { if ( ! this . canBeRotated ) return ; const pr = this . entityData . possible_rotations ; const newDir = pr [ ( pr . indexOf ( this . direction ) + ( this . size . x !== this . size . y || this . type === 'underground_belt' ? 2 : 1 ) * ( ccw ? 3 : 1 ) ) % pr . length ] ; if ( newDir === this . direction ) return ; this . m_BP . history . startTransaction ( 'Rotate entity' ) ; if ( this . type === 'underground_belt' || this . type === 'loader' ) { if ( rotateOpposingUB ) { const otherEntity = this . m_BP . entities . get ( this . m_BP . entityPositionGrid . getOpposingEntity ( this . name , this . direction , this . position , this . directionType === 'input' ? this . direction : ( this . direction + 4 ) % 8 , this . entityData . max_distance ) ) ; if ( otherEntity ) { otherEntity . rotate ( ) ; } } this . directionType = this . directionType === 'input' ? 'output' : 'input' ; } this . direction = newDir ; this . m_BP . history . commitTransaction ( ) ; } public canPasteSettings ( sourceEntity ) { return sourceEntity !== this && sourceEntity . type === this . type ; } /** Paste relevant data from source entity */ public pasteSettings ( sourceEntity ) { if ( ! this . canPasteSettings ( sourceEntity ) ) return ; this . m_BP . history . startTransaction ( 'Paste settings to entity' ) ; let tRecipe = this . recipe ; const aR = this . acceptedRecipes ; if ( aR . length > 0 && sourceEntity . acceptedRecipes ) { tRecipe = sourceEntity . recipe !== undefined && aR . includes ( sourceEntity . recipe ) ? sourceEntity . recipe : undefined ; this . recipe = tRecipe ; } if ( this . type === 'assembling_machine' && this . name !== 'assembling_machine' && tRecipe && FD . recipes [ tRecipe ] . category === 'crafting_with_fluid' ) { this . direction = sourceEntity . direction ; } const aM = this . acceptedModules ; if ( aM . length > 0 && sourceEntity . acceptedModules ) { if ( sourceEntity . modules && sourceEntity . modules . length > 0 ) { this . modules = sourceEntity . modules . filter ( m => aM . includes ( m ) ) . slice ( 0 , this . moduleSlots ) ; } else { this . modules = [ ] ; } } if ( this . type === 'splitter' && sourceEntity . type === 'splitter' ) { this . splitterInputPriority = sourceEntity . splitterInputPriority ; this . splitterOutputPriority = sourceEntity . splitterOutputPriority ; } const aF = this . acceptedFilters ; if ( aF . length > 0 && sourceEntity . acceptedFilters ) { if ( sourceEntity . filters && sourceEntity . filters . length > 0 ) { this . filters = sourceEntity . filters . filter ( f => aF . includes ( f . name ) ) . slice ( 0 , this . filterSlots ) ; } else { this . filters = [ ] ; } } if ( this . type === 'logistic_chest_requester' && sourceEntity . type === 'logistic_chest_requester' ) { this . requestFromBufferChest = sourceEntity . requestFromBufferChest ; } this . m_BP . history . commitTransaction ( ) ; } public get assemblerCraftsWithFluid ( ) : boolean { return ( this . recipe && FD . recipes [ this . recipe ] . category === 'crafting_with_fluid' && this . mayCraftWithFluid ) ; } public get mayCraftWithFluid ( ) : boolean { return ( this . entityData . crafting_categories && this . entityData . crafting_categories . includes ( 'crafting_with_fluid' ) ) ; } public get assemblerPipeDirection ( ) : 'input' | 'output' { if ( ! this . recipe ) return undefined ; const recipe = FD . recipes [ this . recipe ] ; if ( recipe . ingredients . find ( ingredient => ingredient . type === 'fluid' ) ) return 'input' ; if ( recipe . results . find ( result => result . type === 'fluid' ) ) return 'output' ; return undefined ; } public getWireConnectionPoint ( color , side , direction = this . direction ) { const e = this . entityData ; if ( e . connection_points ) return e . connection_points [ direction / 2 ] . wire [ color ] ; if ( e . input_connection_points ) { if ( side === 1 ) return e . input_connection_points [ direction / 2 ] . wire [ color ] ; return e . output_connection_points [ direction / 2 ] . wire [ color ] ; } if ( this . name === 'power_switch' && color === 'copper' ) { return side === 1 ? e . left_wire_connection_point . wire . copper : e . right_wire_connection_point . wire . copper ; } if ( e . circuit_wire_connection_point ) return e . circuit_wire_connection_point . wire [ color ] ; const getIndex = ( ) : number => { if ( this . type === 'transport_belt' ) { const i = getBeltWireConnectionIndex ( this . m_BP . entityPositionGrid , this . position , direction ) ; return i * 4 ; } if ( e . circuit_wire_connection_points . length === 8 ) return direction ; return direction / 2 ; } ; return e . circuit_wire_connection_points [ getIndex ( ) ] . wire [ color ] ; } public serialize ( entNrWhitelist ? ) { return util . duplicate ( { ... this . m_rawEntity , ... this . m_BP . wireConnections . serializeConnectionData ( this . entityNumber , entNrWhitelist ) , } ) ; } }