import * as PIXI from 'pixi.js' ; import FD from '../core/factorioData' ; import G from '../common/globals' ; import { Tile } from '../core/Tile' ; import { Entity } from '../core/Entity' ; import { Blueprint } from '../core/Blueprint' ; import { IConnection } from '../core/WireConnections' ; import { isActionActive , callAction } from '../actions' ; import { Viewport } from './Viewport' ; import { EntitySprite } from './EntitySprite' ; import { WiresContainer } from './WiresContainer' ; import { UnderlayContainer } from './UnderlayContainer' ; import { EntityContainer } from './EntityContainer' ; import { OverlayContainer } from './OverlayContainer' ; import { PaintEntityContainer } from './PaintEntityContainer' ; import { TileContainer } from './TileContainer' ; import { PaintTileContainer } from './PaintTileContainer' ; import { PaintContainer } from './PaintContainer' ; import { PaintBlueprintContainer } from './PaintBlueprintContainer' ; import { OptimizedContainer } from './OptimizedContainer' ; import { GridData } from './GridData' ; export type GridPattern = 'checker' | 'grid' ; export enum EditorMode { /** Default */ NONE , /** Active when an entity is being hovered */ EDIT , /** Active when "painting" */ PAINT , /** Active when panning */ PAN , /** Active when selecting multiple entities for copy/stamp */ COPY , /** Active when selecting multiple entities for deletion */ DELETE } export class BlueprintContainer extends PIXI . Container { /** Nr of cunks needs to be odd because the chunk grid is offset */ private readonly chunks = 32 - 1 ; /** Chunk offset - from 0,0 - Measured in tiles */ private readonly chunkOffset = 16 ; private readonly size : IPoint = { x : this . chunks * 32 * 32 , y : this . chunks * 32 * 32 , } ; private readonly anchor : IPoint = { x : 0.5 , y : 0.5 , } ; private readonly viewport : Viewport = new Viewport ( this . size , { x : G . app . screen . width , y : G . app . screen . height , } , this . anchor , 3 ) ; private _moveSpeed = 10 ; private _gridColor = 0x303030 ; private _gridPattern : GridPattern = 'grid' ; private _mode : EditorMode = EditorMode . NONE ; public readonly bp : Blueprint ; public readonly gridData : GridData ; private grid : PIXI . TilingSprite ; private readonly chunkGrid : PIXI . TilingSprite ; private readonly tileSprites : OptimizedContainer ; private readonly tilePaintSlot : PIXI . Container ; public readonly underlayContainer : UnderlayContainer ; private readonly entitySprites : OptimizedContainer ; public readonly wiresContainer : WiresContainer ; public readonly overlayContainer : OverlayContainer ; private readonly entityPaintSlot : PIXI . Container ; public hoverContainer : EntityContainer ; public paintContainer : PaintContainer ; private _entityForCopyData : Entity ; private copyModeEntities : Entity [ ] = [ ] ; private deleteModeEntities : Entity [ ] = [ ] ; private copyModeUpdateFn : ( endX : number , endY : number ) => void ; private deleteModeUpdateFn : ( endX : number , endY : number ) => void ; public viewportCulling = true ; public readonly interactive = true ; public readonly interactiveChildren = false ; public readonly hitArea = new PIXI . Rectangle ( - this . size . x * this . anchor . x , - this . size . y * this . anchor . y , this . size . x , this . size . y ) ; public constructor ( bp ) { super ( ) ; this . bp = bp ; this . gridData = new GridData ( this ) ; this . grid = this . generateGrid ( ) ; this . chunkGrid = this . generateChunkGrid ( this . chunkOffset ) ; this . tileSprites = new OptimizedContainer ( this ) ; this . tilePaintSlot = new PIXI . Container ( ) ; this . underlayContainer = new UnderlayContainer ( ) ; this . entitySprites = new OptimizedContainer ( this ) ; this . wiresContainer = new WiresContainer ( this . bp ) ; this . overlayContainer = new OverlayContainer ( this ) ; this . entityPaintSlot = new PIXI . Container ( ) ; this . addChild ( this . grid , this . chunkGrid , this . tileSprites , this . tilePaintSlot , this . underlayContainer , this . entitySprites , this . wiresContainer , this . overlayContainer , this . entityPaintSlot ) ; this . on ( 'pointerover' , ( ) => { if ( this . mode === EditorMode . PAINT ) { this . paintContainer . show ( ) ; } this . updateHoverContainer ( ) ; } ) ; this . on ( 'pointerout' , ( ) => { if ( this . mode === EditorMode . PAINT ) { this . paintContainer . hide ( ) ; } this . updateHoverContainer ( ) ; } ) ; const panCb = ( ) : void => { if ( this . mode !== EditorMode . PAN ) { const WSXOR = isActionActive ( 'moveUp' ) !== isActionActive ( 'moveDown' ) ; const ADXOR = isActionActive ( 'moveLeft' ) !== isActionActive ( 'moveRight' ) ; if ( WSXOR || ADXOR ) { const finalSpeed = this . moveSpeed / ( WSXOR && ADXOR ? 1.4142 : 1 ) ; this . viewport . translateBy ( ( ADXOR ? ( isActionActive ( 'moveLeft' ) ? 1 : - 1 ) : 0 ) * finalSpeed , ( WSXOR ? ( isActionActive ( 'moveUp' ) ? 1 : - 1 ) : 0 ) * finalSpeed ) ; } } } ; const onUpdate16 = ( ) : void => { if ( this . mode === EditorMode . PAINT ) { this . paintContainer . moveAtCursor ( ) ; } } ; const onUpdate32 = ( ) : void => { this . updateHoverContainer ( ) ; if ( isActionActive ( 'build' ) ) { callAction ( 'build' ) ; } if ( isActionActive ( 'mine' ) ) { callAction ( 'mine' ) ; } if ( isActionActive ( 'pasteEntitySettings' ) ) { callAction ( 'pasteEntitySettings' ) ; } } ; let lastX = 0 ; let lastY = 0 ; const onMouseMove = ( e ) : void => { if ( this . mode === EditorMode . PAN ) { this . viewport . translateBy ( e . clientX - lastX , e . clientY - lastY ) ; } lastX = e . clientX ; lastY = e . clientY ; } ; const onResize = ( ) : void => { this . viewport . setSize ( G . app . screen . width , G . app . screen . height ) ; } ; G . app . ticker . add ( panCb ) ; this . gridData . on ( 'update16' , onUpdate16 ) ; this . gridData . on ( 'update32' , onUpdate32 ) ; document . addEventListener ( 'mousemove' , onMouseMove ) ; window . addEventListener ( 'resize' , onResize , false ) ; this . on ( 'destroy' , ( ) => { G . app . ticker . remove ( panCb ) ; this . gridData . off ( 'update16' , onUpdate16 ) ; this . gridData . off ( 'update32' , onUpdate32 ) ; this . gridData . destroy ( ) ; document . removeEventListener ( 'mousemove' , onMouseMove ) ; window . removeEventListener ( 'resize' , onResize , false ) ; } ) ; } public get entityForCopyData ( ) : Entity { return this . _entityForCopyData ; } public copyEntitySettings ( ) { if ( this . mode === EditorMode . EDIT ) { this . _entityForCopyData = this . hoverContainer . entity ; } } public pasteEntitySettings ( ) { if ( this . _entityForCopyData && this . mode === EditorMode . EDIT ) { this . hoverContainer . entity . pasteSettings ( this . _entityForCopyData ) ; } } public getViewportScale ( ) { return this . viewport . getCurrentScale ( ) ; } /** screen to world */ public toWorld ( x , y ) { const t = this . viewport . getTransform ( ) ; return [ ( x - t . tx ) / t . a , ( y - t . ty ) / t . d ] ; } public render ( renderer ) { if ( this . viewport . update ( ) ) { this . gridData . recalculate ( ) ; } const destinationFrame = renderer . screen ; const sourceFrame = destinationFrame . clone ( ) ; const t = this . viewport . getTransform ( ) ; sourceFrame . x -= t . tx / t . a ; sourceFrame . y -= t . ty / t . d ; sourceFrame . width /= t . a ; sourceFrame . height /= t . d ; const previous = renderer . renderTexture . current ; for ( const child of this . children ) { renderer . renderTexture . bind ( null , sourceFrame , destinationFrame ) ; child . render ( renderer ) ; } renderer . batch . flush ( ) ; renderer . renderTexture . bind ( previous ) ; } public get mode ( ) : EditorMode { return this . _mode ; } private setMode ( mode ) { this . _mode = mode ; this . emit ( 'mode' , mode ) ; } public enterCopyMode ( ) { if ( this . mode === EditorMode . COPY ) return ; if ( this . mode === EditorMode . PAINT ) this . paintContainer . destroy ( ) ; this . updateHoverContainer ( true ) ; this . setMode ( EditorMode . COPY ) ; this . overlayContainer . showSelectionArea ( 0x00d400 ) ; const startPos = { x : this . gridData . x32 , y : this . gridData . y32 } ; this . copyModeUpdateFn = ( endX , endY ) => { const X = Math . min ( startPos . x , endX ) ; const Y = Math . min ( startPos . y , endY ) ; const W = Math . abs ( endX - startPos . x ) + 1 ; const H = Math . abs ( endY - startPos . y ) + 1 ; for ( const e of this . copyModeEntities ) { EntityContainer . mappings . get ( e . entityNumber ) . cursorBox = undefined ; } this . copyModeEntities = this . bp . entityPositionGrid . getEntitiesInArea ( { x : X + W / 2 , y : Y + H / 2 , w : W , h : H , } ) ; for ( const e of this . copyModeEntities ) { EntityContainer . mappings . get ( e . entityNumber ) . cursorBox = 'copy' ; } } ; this . copyModeUpdateFn ( startPos . x , startPos . y ) ; this . gridData . on ( 'update32' , this . copyModeUpdateFn ) ; } public exitCopyMode ( cancel = false ) { if ( this . mode !== EditorMode . COPY ) return ; this . overlayContainer . hideSelectionArea ( ) ; this . gridData . off ( 'update32' , this . copyModeUpdateFn ) ; this . setMode ( EditorMode . NONE ) ; this . updateHoverContainer ( ) ; if ( ! cancel && this . copyModeEntities . length !== 0 ) { this . spawnPaintContainer ( this . copyModeEntities ) ; } for ( const e of this . copyModeEntities ) { EntityContainer . mappings . get ( e . entityNumber ) . cursorBox = undefined ; } this . copyModeEntities = [ ] ; } public enterDeleteMode ( ) { if ( this . mode === EditorMode . DELETE ) return ; if ( this . mode === EditorMode . PAINT ) this . paintContainer . destroy ( ) ; this . updateHoverContainer ( true ) ; this . setMode ( EditorMode . DELETE ) ; this . overlayContainer . showSelectionArea ( 0xff3200 ) ; const startPos = { x : this . gridData . x32 , y : this . gridData . y32 } ; this . deleteModeUpdateFn = ( endX , endY ) => { const X = Math . min ( startPos . x , endX ) ; const Y = Math . min ( startPos . y , endY ) ; const W = Math . abs ( endX - startPos . x ) + 1 ; const H = Math . abs ( endY - startPos . y ) + 1 ; for ( const e of this . deleteModeEntities ) { EntityContainer . mappings . get ( e . entityNumber ) . cursorBox = undefined ; } this . deleteModeEntities = this . bp . entityPositionGrid . getEntitiesInArea ( { x : X + W / 2 , y : Y + H / 2 , w : W , h : H , } ) ; for ( const e of this . deleteModeEntities ) { EntityContainer . mappings . get ( e . entityNumber ) . cursorBox = 'not_allowed' ; } } ; this . deleteModeUpdateFn ( startPos . x , startPos . y ) ; this . gridData . on ( 'update32' , this . deleteModeUpdateFn ) ; } public exitDeleteMode ( cancel = false ) { if ( this . mode !== EditorMode . DELETE ) return ; this . overlayContainer . hideSelectionArea ( ) ; this . gridData . off ( 'update32' , this . deleteModeUpdateFn ) ; this . setMode ( EditorMode . NONE ) ; this . updateHoverContainer ( ) ; if ( cancel ) { for ( const e of this . deleteModeEntities ) { EntityContainer . mappings . get ( e . entityNumber ) . cursorBox = undefined ; } } else { this . bp . removeEntities ( this . deleteModeEntities ) ; } this . deleteModeEntities = [ ] ; } public enterPanMode ( ) { if ( this . mode === EditorMode . NONE && this . isPointerInside ) { this . setMode ( EditorMode . PAN ) ; this . cursor = 'move' ; } } public exitPanMode ( ) { if ( this . mode === EditorMode . PAN ) { this . setMode ( EditorMode . NONE ) ; this . cursor = 'inherit' ; } } public zoom ( zoomIn = true ) { const zoomFactor = 0.1 ; this . viewport . setScaleCenter ( this . gridData . x , this . gridData . y ) ; this . viewport . zoomBy ( zoomFactor * ( zoomIn ? 1 : - 1 ) ) ; } private get isPointerInside ( ) : boolean { const container = G . app . renderer . plugins . interaction . hitTest ( G . app . renderer . plugins . interaction . mouse . global , G . app . stage ) ; return container === this ; } private updateHoverContainer ( forceRemove = false ) { const removeHoverContainer = ( ) : void => { this . hoverContainer . pointerOutEventHandler ( ) ; this . hoverContainer = undefined ; this . setMode ( EditorMode . NONE ) ; this . cursor = 'inherit' ; this . emit ( 'removeHoverContainer' ) ; } ; if ( forceRemove || ! this . isPointerInside ) { if ( this . hoverContainer ) { removeHoverContainer ( ) ; } return ; } if ( ! this . bp ) return ; const entity = this . bp . entityPositionGrid . getEntityAtPosition ( this . gridData . x32 , this . gridData . y32 ) ; const eC = entity ? EntityContainer . mappings . get ( entity . entityNumber ) : undefined ; if ( eC && this . hoverContainer === eC ) return ; if ( this . mode === EditorMode . EDIT ) { removeHoverContainer ( ) ; } if ( eC && this . mode === EditorMode . NONE ) { this . hoverContainer = eC ; this . setMode ( EditorMode . EDIT ) ; this . cursor = 'pointer' ; eC . pointerOverEventHandler ( ) ; this . emit ( 'createHoverContainer' ) ; } } public get moveSpeed ( ) : number { return this . _moveSpeed ; } public set moveSpeed ( speed ) { this . _moveSpeed = speed ; } public get gridColor ( ) : number { return this . _gridColor ; } public set gridColor ( color ) { this . _gridColor = color ; this . grid . tint = color ; } public get gridPattern ( ) : GridPattern { return this . _gridPattern ; } public set gridPattern ( pattern ) { this . _gridPattern = pattern ; const index = this . getChildIndex ( this . grid ) ; const old = this . grid ; this . grid = this . generateGrid ( ) ; this . addChildAt ( this . grid , index ) ; old . destroy ( ) ; } private generateGrid ( pattern = this . gridPattern ) { const gridGraphics = pattern === 'checker' ? new PIXI . Graphics ( ) . beginFill ( 0x808080 ) . drawRect ( 0 , 0 , 32 , 32 ) . drawRect ( 32 , 32 , 32 , 32 ) . endFill ( ) . beginFill ( 0xffffff ) . drawRect ( 0 , 32 , 32 , 32 ) . drawRect ( 32 , 0 , 32 , 32 ) . endFill ( ) : new PIXI . Graphics ( ) . beginFill ( 0x808080 ) . drawRect ( 0 , 0 , 32 , 32 ) . endFill ( ) . beginFill ( 0xffffff ) . drawRect ( 1 , 1 , 31 , 31 ) . endFill ( ) ; const renderTexture = PIXI . RenderTexture . create ( { width : gridGraphics . width , height : gridGraphics . height , } ) ; renderTexture . baseTexture . mipmap = PIXI . MIPMAP_MODES . POW2 ; G . app . renderer . render ( gridGraphics , { renderTexture } ) ; const grid = new PIXI . TilingSprite ( renderTexture , this . size . x , this . size . y ) ; grid . anchor . set ( this . anchor . x , this . anchor . y ) ; grid . tint = this . gridColor ; return grid ; } private generateChunkGrid ( chunkOffset ) { const W = 32 * 32 ; const H = 32 * 32 ; const gridGraphics = new PIXI . Graphics ( ) . lineStyle ( { width : 2 , color : 0x000000 } ) . moveTo ( 0 , 0 ) . lineTo ( W , 0 ) . lineTo ( W , H ) . lineTo ( 0 , H ) . lineTo ( 0 , 0 ) ; const renderTexture = PIXI . RenderTexture . create ( { width : W , height : H , } ) ; renderTexture . baseTexture . mipmap = PIXI . MIPMAP_MODES . POW2 ; G . app . renderer . render ( gridGraphics , { renderTexture } ) ; const grid = new PIXI . TilingSprite ( renderTexture , this . size . x + W , this . size . y + H ) ; grid . position . set ( chunkOffset * 32 , chunkOffset * 32 ) ; grid . anchor . set ( this . anchor . x , this . anchor . y ) ; return grid ; } public initBP ( ) { for ( const [ , e ] of this . bp . entities ) { new EntityContainer ( e , false ) ; } for ( const [ , t ] of this . bp . tiles ) { new TileContainer ( t ) ; } const onCreateEntity = ( entity ) : void => { new EntityContainer ( entity ) ; this . updateHoverContainer ( ) ; } ; const onRemoveEntity = ( ) : void => { this . updateHoverContainer ( ) ; } ; const onCreateTile = ( tile ) : TileContainer => new TileContainer ( tile ) ; this . bp . on ( 'create-entity' , onCreateEntity ) ; this . bp . on ( 'remove-entity' , onRemoveEntity ) ; this . bp . on ( 'create-tile' , onCreateTile ) ; const onConnectionCreated = ( hash , connection ) : void => { this . wiresContainer . connect ( hash , connection ) ; } ; const onConnectionRemoved = ( hash , connection ) : void => { this . wiresContainer . disconnect ( hash , connection ) ; } ; this . bp . wireConnections . on ( 'create' , onConnectionCreated ) ; this . bp . wireConnections . on ( 'remove' , onConnectionRemoved ) ; this . bp . wireConnections . forEach ( ( connection , hash ) => this . wiresContainer . add ( hash , connection ) ) ; this . on ( 'destroy' , ( ) => { this . bp . off ( 'create-entity' , onCreateEntity ) ; this . bp . off ( 'remove-entity' , onRemoveEntity ) ; this . bp . off ( 'create-tile' , onCreateTile ) ; this . bp . wireConnections . off ( 'create' , onConnectionCreated ) ; this . bp . wireConnections . off ( 'remove' , onConnectionRemoved ) ; } ) ; this . sortEntities ( ) ; this . centerViewport ( ) ; } public destroy ( ) { this . emit ( 'destroy' ) ; super . destroy ( { children : true } ) ; } public addEntitySprites ( entitySprites , sort = true ) { this . entitySprites . addChild ( ... entitySprites ) ; if ( sort ) { this . sortEntities ( ) ; } } public addTileSprites ( tileSprites ) { this . tileSprites . addChild ( ... tileSprites ) ; } private sortEntities ( ) { this . entitySprites . children . sort ( EntitySprite . compareFn ) ; } public transparentEntities ( bool = true ) { const alpha = bool ? 0.5 : 1 ; this . entitySprites . alpha = alpha ; this . wiresContainer . alpha = alpha ; this . overlayContainer . alpha = alpha ; } public centerViewport ( ) { const bounds = this . bp . isEmpty ( ) ? new PIXI . Rectangle ( - 16 * 32 , - 16 * 32 , 32 * 32 , 32 * 32 ) : this . getBlueprintBounds ( ) ; this . viewport . centerViewPort ( { x : bounds . width , y : bounds . height , } , { x : ( this . size . x - bounds . width ) / 2 - bounds . x , y : ( this . size . y - bounds . height ) / 2 - bounds . y , } ) ; } public getBlueprintBounds ( ) { const bounds = new PIXI . Bounds ( ) ; const addBounds = ( sprite ) : void => { const sB = new PIXI . Bounds ( ) ; sB . minX = sprite . cachedBounds [ 0 ] ; sB . minY = sprite . cachedBounds [ 1 ] ; sB . maxX = sprite . cachedBounds [ 2 ] ; sB . maxY = sprite . cachedBounds [ 3 ] ; bounds . addBounds ( sB ) ; } ; this . entitySprites . children . forEach ( addBounds ) ; this . tileSprites . children . forEach ( addBounds ) ; const rect = bounds . getRectangle ( new PIXI . Rectangle ( ) ) ; const X = Math . floor ( rect . x / 32 ) * 32 ; const Y = Math . floor ( rect . y / 32 ) * 32 ; const W = Math . ceil ( ( rect . width + rect . x - X ) / 32 ) * 32 ; const H = Math . ceil ( ( rect . height + rect . y - Y ) / 32 ) * 32 ; return new PIXI . Rectangle ( X , Y , W , H ) ; } public getPicture ( ) { if ( this . bp . isEmpty ( ) ) return ; const region = this . getBlueprintBounds ( ) ; this . viewportCulling = false ; const _render = this . render ; this . render = super . render ; const texture = G . app . renderer . generateTexture ( this , { scaleMode : PIXI . SCALE_MODES . LINEAR , resolution : 1 , region , } ) ; this . render = _render ; this . viewportCulling = true ; const canvas = G . app . renderer . plugins . extract . canvas ( texture ) ; return new Promise ( resolve => { canvas . toBlob ( blob => { canvas . getContext ( '2d' ) . clearRect ( 0 , 0 , canvas . width , canvas . height ) ; resolve ( blob ) ; } ) ; } ) ; } public spawnPaintContainer ( itemNameOrEntities , direction = 0 ) { if ( this . mode === EditorMode . PAINT ) { this . paintContainer . destroy ( ) ; } this . updateHoverContainer ( true ) ; this . setMode ( EditorMode . PAINT ) ; this . cursor = 'pointer' ; if ( typeof itemNameOrEntities === 'string' ) { const itemData = FD . items [ itemNameOrEntities ] ; const tileResult = itemData . place_as_tile && itemData . place_as_tile . result ; const placeResult = itemData . place_result || tileResult ; if ( tileResult ) { this . paintContainer = new PaintTileContainer ( this , placeResult ) ; this . tilePaintSlot . addChild ( this . paintContainer ) ; } else { this . paintContainer = new PaintEntityContainer ( this , placeResult , direction ) ; this . entityPaintSlot . addChild ( this . paintContainer ) ; } } else { this . paintContainer = new PaintBlueprintContainer ( this , itemNameOrEntities ) ; this . entityPaintSlot . addChild ( this . paintContainer ) ; } if ( ! this . isPointerInside ) { this . paintContainer . hide ( ) ; } this . paintContainer . on ( 'destroy' , ( ) => { this . paintContainer = undefined ; this . setMode ( EditorMode . NONE ) ; this . updateHoverContainer ( ) ; this . cursor = 'inherit' ; } ) ; } }