1
0
mirror of https://github.com/teoxoy/factorio-blueprint-editor.git synced 2025-01-28 03:29:29 +02:00

extract website from the editor

parcel doesn't fully support yarn workspaces (parcel-bundler/parcel#2269) migrate to webpack
configure monorepo for the new changes
This commit is contained in:
Teoxoy 2019-08-02 22:06:07 +02:00
parent ddeb2c0225
commit 22b39627b7
41 changed files with 4200 additions and 3122 deletions

View File

@ -1,11 +1,16 @@
const glob = require('glob')
const configs = glob.sync('./packages/**/tsconfig.json')
module.exports = { module.exports = {
env: { env: {
browser: true, browser: true,
es6: true es6: true,
node: true
}, },
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
parserOptions: { parserOptions: {
project: './tsconfig.json' project: configs[0],
projects: configs
}, },
plugins: ['@typescript-eslint'], plugins: ['@typescript-eslint'],
extends: [ extends: [

View File

Before

Width:  |  Height:  |  Size: 487 KiB

After

Width:  |  Height:  |  Size: 487 KiB

1
.gitignore vendored
View File

@ -1,3 +1,2 @@
node_modules node_modules
.cache
dist dist

View File

@ -15,6 +15,7 @@ You can file new issues by selecting from our [new issue templates](https://gith
## Prerequisites ## Prerequisites
- [git](https://git-scm.com/) - [git](https://git-scm.com/)
- [yarn](https://yarnpkg.com)
- [node](https://nodejs.org/en/) - [node](https://nodejs.org/en/)
- [vscode](https://code.visualstudio.com/) - [vscode](https://code.visualstudio.com/)
@ -28,13 +29,13 @@ This project uses `eslint` and `prettier` to lint and format code. I would recom
1. Clone your fork 1. Clone your fork
1. Download the recommended workspace extensions in vscode 1. Download the recommended workspace extensions in vscode
1. Make your changes in a new git branch (`git checkout -b my-fix-branch master`) 1. Make your changes in a new git branch (`git checkout -b my-fix-branch master`)
1. Run `npm install` 2. Run `yarn`
1. Run `npm start` 3. Run `yarn start`
1. Open the link in a browser or use the vscode debugger 4. Open the link in a browser or use the vscode debugger
1. Make changes 5. Make changes
1. Commit your changes using a descriptive commit message 6. Commit your changes using a descriptive commit message
1. Push your branch to GitHub `git push origin my-fix-branch` 7. Push your branch to GitHub `git push origin my-fix-branch`
1. Start a pull request from GitHub 8. Start a pull request from GitHub
That's it! 🎉 Thank you for your contribution! 😃 That's it! 🎉 Thank you for your contribution! 😃

View File

@ -1,4 +1,4 @@
<img src="./logo.svg" width="100%" align="right"> <img src="./.github/logo.svg" width="100%" align="right">
# factorio-blueprint-editor # factorio-blueprint-editor

View File

@ -3,13 +3,23 @@
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
], ],
"scripts": {
"start:editor": "yarn workspace @fbe/editor run start",
"start:website": "yarn workspace @fbe/website run start",
"build:editor": "yarn workspace @fbe/editor run build",
"build:website": "yarn workspace @fbe/website run build",
"start": "yarn build:editor && concurrently \"yarn:start:editor\" \"yarn:start:website\"",
"build": "yarn build:editor && yarn build:website"
},
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^1.13.0", "@typescript-eslint/eslint-plugin": "^1.13.0",
"@typescript-eslint/parser": "^1.13.0", "@typescript-eslint/parser": "^1.13.0",
"concurrently": "^4.1.1",
"eslint": "^6.1.0", "eslint": "^6.1.0",
"eslint-config-prettier": "^6.0.0", "eslint-config-prettier": "^6.0.0",
"eslint-plugin-import": "^2.18.2", "eslint-plugin-import": "^2.18.2",
"eslint-plugin-prettier": "^3.1.0", "eslint-plugin-prettier": "^3.1.0",
"glob": "^7.1.4",
"prettier": "1.18.2", "prettier": "1.18.2",
"typescript": "^3.5.3" "typescript": "^3.5.3"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "factorio-blueprint-editor", "name": "@fbe/editor",
"version": "0.1.0", "version": "1.0.0",
"description": "A Factorio blueprint editor and renderer webapp", "description": "A Factorio blueprint editor and renderer webapp",
"author": "Teoxoy", "author": "Teoxoy",
"license": "MIT", "license": "MIT",
@ -10,41 +10,29 @@
}, },
"bugs": "https://github.com/Teoxoy/factorio-blueprint-editor/issues", "bugs": "https://github.com/Teoxoy/factorio-blueprint-editor/issues",
"homepage": "https://github.com/Teoxoy/factorio-blueprint-editor", "homepage": "https://github.com/Teoxoy/factorio-blueprint-editor",
"main": "./dist/index.js",
"types": "./dist/index.d.js",
"scripts": { "scripts": {
"start": "parcel src/index.html --port 8080", "start": "concurrently \"tsc -w\" \"ts-cleaner -w\"",
"prebuild": "rimraf dist/*", "build": "tsc && ts-cleaner"
"build": "parcel build src/index.html --public-url ./",
"setupDist": "git worktree add dist gh-pages",
"deploy": "cd dist && git add --all && git reset -- stats && git commit -m \"new version\" && git push origin gh-pages"
}, },
"browserslist": [
"last 2 Chrome versions",
"last 2 Firefox versions",
"last 2 Safari versions",
"last 2 Edge versions"
],
"dependencies": { "dependencies": {
"ajv": "^6.10.2", "ajv": "^6.10.2",
"dat.gui": "^0.7.6",
"delaunator": "^4.0.0", "delaunator": "^4.0.0",
"eventemitter3": "^4.0.0", "eventemitter3": "^4.0.0",
"factorio-data": "4.0.4", "factorio-data": "4.0.4",
"file-saver": "^2.0.2",
"keyboardjs": "^2.5.1", "keyboardjs": "^2.5.1",
"pako": "^1.0.10", "pako": "^1.0.10",
"pathfinding": "^0.4.18", "pathfinding": "^0.4.18",
"pixi.js": "^5.1.0" "pixi.js": "^5.1.0"
}, },
"devDependencies": { "devDependencies": {
"@types/dat.gui": "^0.7.3",
"@types/delaunator": "^3.0.0", "@types/delaunator": "^3.0.0",
"@types/file-saver": "^2.0.1",
"@types/keyboardjs": "^2.4.1", "@types/keyboardjs": "^2.4.1",
"@types/pako": "^1.0.1", "@types/pako": "^1.0.1",
"@types/pathfinding": "0.0.3", "@types/pathfinding": "0.0.3",
"parcel-bundler": "^1.12.3", "concurrently": "^4.1.1",
"rimraf": "^2.6.3", "ts-cleaner": "^1.0.2",
"stylus": "^0.54.5",
"typescript": "^3.5.3" "typescript": "^3.5.3"
} }
} }

View File

@ -5,12 +5,12 @@ import G from '../../common/globals'
export default class Checkbox extends PIXI.Container { export default class Checkbox extends PIXI.Container {
/** Checkmark Polygon */ /** Checkmark Polygon */
// prettier-ignore // prettier-ignore
private static readonly CHECK_POLYGON: PIXI.Polygon = new PIXI.Polygon( private static readonly CHECK_POLYGON: PIXI.Polygon = new PIXI.Polygon([
8, 8, 12, 8, 16, 12, 20, 12, 24, 8, 8, 8, 12, 8, 16, 12, 20, 12, 24, 8,
28, 8, 28, 12, 24, 16, 24, 20, 28, 24, 28, 8, 28, 12, 24, 16, 24, 20, 28, 24,
28, 28, 24, 28, 20, 24, 16, 24, 12, 28, 28, 28, 24, 28, 20, 24, 16, 24, 12, 28,
8, 28, 8, 24, 12, 20, 12, 16, 8, 12, 8, 28, 8, 24, 12, 20, 12, 16, 8, 12,
8, 8) 8, 8])
/** /**
* Draw Checkbox Graphic * Draw Checkbox Graphic

View File

@ -52,7 +52,7 @@ export default class Preview extends PIXI.Container {
private generatePreview(): PIXI.Container { private generatePreview(): PIXI.Container {
// Add all entity parts to a separate container // Add all entity parts to a separate container
const entityParts: PIXI.Container = new PIXI.Container() const entityParts: PIXI.Container = new PIXI.Container()
EntitySprite.getParts(this.m_Entity, G.quality.hr).forEach(s => entityParts.addChild(s)) EntitySprite.getParts(this.m_Entity).forEach(s => entityParts.addChild(s))
this.addChild(entityParts) this.addChild(entityParts)
const actualSpriteSize = { x: this.m_Entity.size.x, y: this.m_Entity.size.y } const actualSpriteSize = { x: this.m_Entity.size.x, y: this.m_Entity.size.y }

View File

@ -1,5 +1,4 @@
import * as PIXI from 'pixi.js' import * as PIXI from 'pixi.js'
import G from '../common/globals'
import Entity from '../factorio-data/entity' import Entity from '../factorio-data/entity'
import { DebugContainer } from './panels/debug' import { DebugContainer } from './panels/debug'
import { QuickbarContainer } from './panels/quickbar' import { QuickbarContainer } from './panels/quickbar'
@ -18,7 +17,7 @@ export default class UIContainer extends PIXI.Container {
super() super()
this.debugContainer = new DebugContainer() this.debugContainer = new DebugContainer()
this.quickbarContainer = new QuickbarContainer(G.quickbarRows) this.quickbarContainer = new QuickbarContainer(2)
this.infoEntityPanel = new InfoEntityPanel() this.infoEntityPanel = new InfoEntityPanel()
this.dialogsContainer = new PIXI.Container() this.dialogsContainer = new PIXI.Container()
this.paintIconContainer = new PIXI.Container() this.paintIconContainer = new PIXI.Container()
@ -60,12 +59,12 @@ export default class UIContainer extends PIXI.Container {
this.dialogsContainer.addChild(inv) this.dialogsContainer.addChild(inv)
} }
public changeQuickbarRows(rows: number): void { // public changeQuickbarRows(rows: number): void {
const itemNames = this.quickbarContainer.serialize() // const itemNames = this.quickbarContainer.serialize()
this.quickbarContainer.destroy() // this.quickbarContainer.destroy()
this.quickbarContainer = new QuickbarContainer(rows, itemNames) // this.quickbarContainer = new QuickbarContainer(rows, itemNames)
const index = this.getChildIndex(this.quickbarContainer) // const index = this.getChildIndex(this.quickbarContainer)
this.addChildAt(this.quickbarContainer, index) // this.addChildAt(this.quickbarContainer, index)
} // }
} }

View File

@ -1,36 +1,46 @@
import keyboardJS from 'keyboardjs' import keyboardJS from 'keyboardjs'
function initActions(canvas: HTMLCanvasElement): void {
// Set the general application keyboard context // Set the general application keyboard context
// Needed to have seperate context's for input controls (i.e. Textbox) // Needed to have seperate context's for input controls (i.e. Textbox)
keyboardJS.setContext('editor') keyboardJS.setContext('editor')
const canvasEl = document.getElementById('editor') as HTMLCanvasElement
// Bind the events on the canvas // Bind the events on the canvas
keyboardJS.watch(canvasEl) // @ts-ignore
keyboardJS.watch(canvas)
// keyboardJS.watch will bind keydown and keyup events on the canvas but // keyboardJS.watch will bind keydown and keyup events on the canvas but
// keydown and keyup will only fire if the canvas is focused // keydown and keyup will only fire if the canvas is focused
canvasEl.addEventListener('mouseover', () => canvasEl.focus()) canvas.addEventListener('mouseover', () => canvas.focus())
canvasEl.addEventListener('blur', () => { canvas.addEventListener('blur', () => {
keyboardJS.releaseAllKeys() keyboardJS.releaseAllKeys()
}) })
// Hack for plugging the mouse into keyboardJS // Hack for plugging the mouse into keyboardJS
// @ts-ignore
keyboardJS._locale.bindKeyCode(300, ['lclick']) keyboardJS._locale.bindKeyCode(300, ['lclick'])
// @ts-ignore
keyboardJS._locale.bindKeyCode(301, ['mclick']) keyboardJS._locale.bindKeyCode(301, ['mclick'])
// @ts-ignore
keyboardJS._locale.bindKeyCode(302, ['rclick']) keyboardJS._locale.bindKeyCode(302, ['rclick'])
// @ts-ignore
keyboardJS._locale.bindKeyCode(303, ['wheelNeg']) keyboardJS._locale.bindKeyCode(303, ['wheelNeg'])
// @ts-ignore
keyboardJS._locale.bindKeyCode(304, ['wheelPos']) keyboardJS._locale.bindKeyCode(304, ['wheelPos'])
canvasEl.addEventListener('mousedown', e => keyboardJS.pressKey(e.button + 300, e)) // @ts-ignore
canvas.addEventListener('mousedown', e => keyboardJS.pressKey(e.button + 300, e))
// attach mouseup to window so that releaseKey will be called even when mouseup is fired from outside the window // attach mouseup to window so that releaseKey will be called even when mouseup is fired from outside the window
// @ts-ignore
window.addEventListener('mouseup', e => keyboardJS.releaseKey(e.button + 300, e)) window.addEventListener('mouseup', e => keyboardJS.releaseKey(e.button + 300, e))
canvasEl.addEventListener('wheel', e => { canvas.addEventListener('wheel', e => {
e.preventDefault() e.preventDefault()
// @ts-ignore
keyboardJS.pressKey(Math.sign(-e.deltaY) === 1 ? 303 : 304, e) keyboardJS.pressKey(Math.sign(-e.deltaY) === 1 ? 303 : 304, e)
// @ts-ignore
keyboardJS.releaseKey(Math.sign(-e.deltaY) === 1 ? 303 : 304, e) keyboardJS.releaseKey(Math.sign(-e.deltaY) === 1 ? 303 : 304, e)
}) })
}
/** /**
* Passes trough all events to the callback * Passes trough all events to the callback
@ -49,6 +59,7 @@ function passtroughAllEvents(cb: (e: keyboardJS.KeyEvent) => boolean): void {
} }
class Action { class Action {
public readonly name: string
private readonly defaultKeyCombo: string private readonly defaultKeyCombo: string
private m_active = true private m_active = true
@ -59,7 +70,8 @@ class Action {
}[] = [] }[] = []
private _pressed = false private _pressed = false
public constructor(defaultKeyCombo: string) { public constructor(name: string, defaultKeyCombo: string) {
this.name = name
this.defaultKeyCombo = defaultKeyCombo this.defaultKeyCombo = defaultKeyCombo
this.m_keyCombo = defaultKeyCombo this.m_keyCombo = defaultKeyCombo
@ -73,6 +85,13 @@ class Action {
}) })
} }
public get prettyName(): string {
return this.name
.split(/(?=[A-Z1-9])/)
.join(' ')
.replace(/(\b\w)/, c => c.toUpperCase())
}
private get active(): boolean { private get active(): boolean {
return this.m_active return this.m_active
} }
@ -187,118 +206,68 @@ class Action {
} }
} }
const actions = { const actions = new Map<string, Action>()
clear: new Action('shift+n'),
focus: new Action('f'),
takePicture: new Action('modifier+s'),
undo: new Action('modifier+z'),
redo: new Action('modifier+y'),
info: new Action('i'),
pan: new Action('lclick'),
zoomIn: new Action('wheelNeg'),
zoomOut: new Action('wheelPos'),
moveEntityUp: new Action('up'),
moveEntityLeft: new Action('left'),
moveEntityDown: new Action('down'),
moveEntityRight: new Action('right'),
generateOilOutpost: new Action('g'), function registerAction(name: string, keyCombo: string): Action {
copySelection: new Action('modifier+lclick'), const action = new Action(name, keyCombo)
deleteSelection: new Action('modifier+rclick'), actions.set(name, action)
return action
moveUp: new Action('w'),
moveLeft: new Action('a'),
moveDown: new Action('s'),
moveRight: new Action('d'),
inventory: new Action('e'),
build: new Action('lclick'),
mine: new Action('rclick'),
copyEntitySettings: new Action('shift+rclick'),
pasteEntitySettings: new Action('shift+lclick'),
/** Used for highlighting the source entity */
tryPasteEntitySettings: new Action('shift'),
openEntityGUI: new Action('lclick'),
showInfo: new Action('alt'),
pipette: new Action('q'),
rotate: new Action('r'),
reverseRotate: new Action('shift+r'),
closeWindow: new Action('esc'),
increaseTileBuildingArea: new Action(']'),
decreaseTileBuildingArea: new Action('['),
quickbar1: new Action('1'),
quickbar2: new Action('2'),
quickbar3: new Action('3'),
quickbar4: new Action('4'),
quickbar5: new Action('5'),
quickbar6: new Action('shift+1'),
quickbar7: new Action('shift+2'),
quickbar8: new Action('shift+3'),
quickbar9: new Action('shift+4'),
quickbar10: new Action('shift+5'),
changeActiveQuickbar: new Action('x'),
copyBPString: {
bind: (opts: { press?: (e: ClipboardEvent) => void }) => {
if (opts.press === undefined) {
return
} }
document.addEventListener('copy', (e: ClipboardEvent) => {
if (document.activeElement !== canvasEl) { function callAction(name: string): void {
return const action = actions.get(name)
if (action) {
action.call()
} }
e.preventDefault() }
opts.press(e)
function isActionActive(name: string): boolean {
const action = actions.get(name)
if (action) {
return action.pressed
}
return false
}
function forEachAction(cb: (action: Action, actionName: string) => void): void {
actions.forEach((action, name) => cb(action, name))
}
function resetKeybinds(): void {
actions.forEach(action => {
action.resetKeyCombo()
}) })
} }
},
pasteBPString: { function importKeybinds(keybinds: Record<string, string>): void {
bind: (opts: { press?: (e: ClipboardEvent) => void }) => {
if (opts.press === undefined) {
return
}
document.addEventListener('paste', (e: ClipboardEvent) => {
if (document.activeElement !== canvasEl) {
return
}
e.preventDefault()
opts.press(e)
})
}
},
forEachAction(cb: (action: Action, actionName: string) => void) {
for (const actionName in this) {
if (this[actionName] instanceof Action) {
cb(this[actionName], actionName)
}
}
},
importKeybinds(keybinds: Record<string, string>) {
if (!keybinds) { if (!keybinds) {
return return
} }
actions.forEachAction((action, actionName) => { actions.forEach((action, name) => {
if (keybinds[actionName] !== undefined) { if (keybinds[name] !== undefined) {
action.keyCombo = keybinds[actionName] action.keyCombo = keybinds[name]
} }
}) })
}, }
exportKeybinds(changedOnly = true) { function exportKeybinds(changedOnly = true): Record<string, string> {
const changedKeybinds: Record<string, string> = {} const changedKeybinds: Record<string, string> = {}
actions.forEachAction((action, actionName) => { actions.forEach((action, name) => {
if (!changedOnly || !action.usesDefaultKeyCombo) { if (!changedOnly || !action.usesDefaultKeyCombo) {
changedKeybinds[actionName] = action.keyCombo changedKeybinds[name] = action.keyCombo
} }
}) })
return changedKeybinds return changedKeybinds
} }
}
export { passtroughAllEvents } export {
export default actions initActions,
passtroughAllEvents,
registerAction,
callAction,
isActionActive,
forEachAction,
resetKeybinds,
importKeybinds,
exportKeybinds
}

View File

@ -1,43 +1,15 @@
import * as PIXI from 'pixi.js' import * as PIXI from 'pixi.js'
import Blueprint from '../factorio-data/blueprint' import Blueprint from '../factorio-data/blueprint'
import { BlueprintContainer } from '../containers/blueprint' import { BlueprintContainer } from '../containers/blueprint'
import { Book } from '../factorio-data/book'
import UIContainer from '../UI/ui' import UIContainer from '../UI/ui'
const hr = false
let debug = false let debug = false
const quality = {
hr: true,
compressed: true
}
// this is how it works in factorio but js doesn't support 64bit bitwise operations
// uint64_t(developerVersion) |
// (uint64_t(minorVersion) << 16) |
// (uint64_t(majorVersion) << 32) |
// (uint64_t(mainVersion) << 48)
const getFactorioVersion = (main = 0, major = 17, minor = 14): number =>
(minor << 16) + (major | (main << 16)) * 0xffffffff
let app: PIXI.Application let app: PIXI.Application
let BPC: BlueprintContainer let BPC: BlueprintContainer
let UI: UIContainer let UI: UIContainer
const loadingScreen = {
el: document.getElementById('loadingScreen'),
show() {
this.el.classList.add('active')
},
hide() {
this.el.classList.remove('active')
}
}
let moveSpeed = 10
let quickbarRows = 2
let bp: Blueprint let bp: Blueprint
let book: Book
const colors = { const colors = {
text: { text: {
@ -96,23 +68,6 @@ const colors = {
}, },
quickbar: { quickbar: {
background: { color: 0x303030, alpha: 1, border: 2 } background: { color: 0x303030, alpha: 1, border: 2 }
},
_darkTheme: true,
_tintsToChange: [] as PIXI.Sprite[],
pattern: 'grid' as 'checker' | 'grid',
get darkTheme() {
return this._darkTheme
},
set darkTheme(value: boolean) {
this._darkTheme = value
this._tintsToChange.forEach((s: PIXI.Sprite) => {
s.tint = value ? 0x303030 : 0xc9c9c9
})
},
addSpriteForAutomaticTintChange(sprite: PIXI.Sprite) {
sprite.tint = this.darkTheme ? 0x303030 : 0xc9c9c9
this._tintsToChange.push(sprite)
} }
} }
@ -181,16 +136,11 @@ const styles = {
export default { export default {
debug, debug,
quality, hr,
getFactorioVersion,
BPC, BPC,
UI, UI,
app, app,
bp, bp,
book,
moveSpeed,
quickbarRows,
loadingScreen,
colors, colors,
fontFamily, fontFamily,
styles styles

View File

@ -2,7 +2,7 @@ import FD from 'factorio-data'
import { EventEmitter } from 'eventemitter3' import { EventEmitter } from 'eventemitter3'
import * as PIXI from 'pixi.js' import * as PIXI from 'pixi.js'
import G from '../common/globals' import G from '../common/globals'
import actions from '../actions' import { isActionActive, callAction } from '../actions'
import Entity from '../factorio-data/entity' import Entity from '../factorio-data/entity'
import Tile from '../factorio-data/tile' import Tile from '../factorio-data/tile'
import { Viewport } from '../viewport' import { Viewport } from '../viewport'
@ -123,7 +123,12 @@ class OptimizedContainer extends PIXI.ParticleContainer {
} }
} }
type GridPattern = 'checker' | 'grid'
class BlueprintContainer extends PIXI.Container { class BlueprintContainer extends PIXI.Container {
public moveSpeed = 10
private _gridColor = 0x303030
private _gridPattern: GridPattern = 'grid'
private grid: PIXI.TilingSprite private grid: PIXI.TilingSprite
private chunkGrid: PIXI.TilingSprite private chunkGrid: PIXI.TilingSprite
public wiresContainer: WiresContainer public wiresContainer: WiresContainer
@ -180,7 +185,7 @@ class BlueprintContainer extends PIXI.Container {
3 3
) )
this.generateGrid(G.colors.pattern) this.generateGrid()
this.tileSprites = new OptimizedContainer() this.tileSprites = new OptimizedContainer()
this.tilePaintSlot = new PIXI.Container() this.tilePaintSlot = new PIXI.Container()
this.visualizationAreaContainer = new VisualizationAreaContainer() this.visualizationAreaContainer = new VisualizationAreaContainer()
@ -205,14 +210,14 @@ class BlueprintContainer extends PIXI.Container {
G.app.ticker.add(() => { G.app.ticker.add(() => {
if (this.mode !== EditorMode.PAN) { if (this.mode !== EditorMode.PAN) {
const WSXOR = actions.moveUp.pressed !== actions.moveDown.pressed const WSXOR = isActionActive('moveUp') !== isActionActive('moveDown')
const ADXOR = actions.moveLeft.pressed !== actions.moveRight.pressed const ADXOR = isActionActive('moveLeft') !== isActionActive('moveRight')
if (WSXOR || ADXOR) { if (WSXOR || ADXOR) {
const finalSpeed = G.moveSpeed / (WSXOR && ADXOR ? 1.4142 : 1) const finalSpeed = this.moveSpeed / (WSXOR && ADXOR ? 1.4142 : 1)
/* eslint-disable no-nested-ternary */ /* eslint-disable no-nested-ternary */
this.viewport.translateBy( this.viewport.translateBy(
(ADXOR ? (actions.moveLeft.pressed ? 1 : -1) : 0) * finalSpeed, (ADXOR ? (isActionActive('moveLeft') ? 1 : -1) : 0) * finalSpeed,
(WSXOR ? (actions.moveUp.pressed ? 1 : -1) : 0) * finalSpeed (WSXOR ? (isActionActive('moveUp') ? 1 : -1) : 0) * finalSpeed
) )
/* eslint-enable no-nested-ternary */ /* eslint-enable no-nested-ternary */
this.applyViewportTransform() this.applyViewportTransform()
@ -230,14 +235,14 @@ class BlueprintContainer extends PIXI.Container {
// Instead of decreasing the global interactionFrequency, call the over and out entity events here // Instead of decreasing the global interactionFrequency, call the over and out entity events here
this.updateHoverContainer() this.updateHoverContainer()
if (actions.build.pressed) { if (isActionActive('build')) {
actions.build.call() callAction('build')
} }
if (actions.mine.pressed) { if (isActionActive('mine')) {
actions.mine.call() callAction('mine')
} }
if (actions.pasteEntitySettings.pressed) { if (isActionActive('pasteEntitySettings')) {
actions.pasteEntitySettings.call() callAction('pasteEntitySettings')
} }
}) })
@ -473,7 +478,25 @@ class BlueprintContainer extends PIXI.Container {
} }
} }
public generateGrid(pattern: 'checker' | 'grid' = 'checker'): void { public get gridColor(): number {
return this._gridColor
}
public set gridColor(color: number) {
this._gridColor = color
this.grid.tint = color
}
public get gridPattern(): GridPattern {
return this._gridPattern
}
public set gridPattern(pattern: GridPattern) {
this._gridPattern = pattern
this.generateGrid()
}
private generateGrid(pattern = this.gridPattern): void {
const gridGraphics = const gridGraphics =
pattern === 'checker' pattern === 'checker'
? new PIXI.Graphics() ? new PIXI.Graphics()
@ -504,7 +527,7 @@ class BlueprintContainer extends PIXI.Container {
const grid = new PIXI.TilingSprite(renderTexture, this.size.x, this.size.y) const grid = new PIXI.TilingSprite(renderTexture, this.size.x, this.size.y)
grid.anchor.set(this.anchor.x, this.anchor.y) grid.anchor.set(this.anchor.x, this.anchor.y)
G.colors.addSpriteForAutomaticTintChange(grid) grid.tint = this.gridColor
if (this.grid) { if (this.grid) {
const index = this.getChildIndex(this.grid) const index = this.getChildIndex(this.grid)
@ -683,6 +706,22 @@ class BlueprintContainer extends PIXI.Container {
return new PIXI.Rectangle(X, Y, W, H) return new PIXI.Rectangle(X, Y, W, H)
} }
public getPicture(): Promise<Blob> {
// getLocalBounds is needed because it seems that it has sideeffects
// without it generateTexture returns an empty texture
this.getLocalBounds()
const region = this.getBlueprintBounds()
const texture = G.app.renderer.generateTexture(this, PIXI.SCALE_MODES.LINEAR, 1, region)
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: string | Entity[], direction = 0): void { public spawnPaintContainer(itemNameOrEntities: string | Entity[], direction = 0): void {
if (this.mode === EditorMode.PAINT) { if (this.mode === EditorMode.PAINT) {
this.paintContainer.destroy() this.paintContainer.destroy()
@ -722,4 +761,4 @@ class BlueprintContainer extends PIXI.Container {
} }
} }
export { EditorMode, BlueprintContainer } export { EditorMode, BlueprintContainer, GridPattern }

View File

@ -295,7 +295,7 @@ export class EntityContainer {
this.entitySprites = [] this.entitySprites = []
for (const s of EntitySprite.getParts( for (const s of EntitySprite.getParts(
this.m_Entity, this.m_Entity,
G.quality.hr,
ignoreConnections ? undefined : G.bp.entityPositionGrid ignoreConnections ? undefined : G.bp.entityPositionGrid
)) { )) {
s.setPosition(this.position) s.setPosition(this.position)

View File

@ -23,7 +23,7 @@ class BlueprintEntityPaintContainer {
this.visualizationArea = G.BPC.visualizationAreaContainer.create(this.entity.name, this.position) this.visualizationArea = G.BPC.visualizationAreaContainer.create(this.entity.name, this.position)
this.entitySprites = EntitySprite.getParts(this.entity, G.quality.hr, this.bp.entityPositionGrid) this.entitySprites = EntitySprite.getParts(this.entity, this.bp.entityPositionGrid)
} }
private get entityPosition(): IPoint { private get entityPosition(): IPoint {

View File

@ -129,14 +129,11 @@ export class EntityPaintContainer extends PaintContainer {
protected redraw(): void { protected redraw(): void {
this.removeChildren() this.removeChildren()
this.addChild( this.addChild(
...EntitySprite.getParts( ...EntitySprite.getParts({
{
name: this.name, name: this.name,
direction: this.directionType === 'input' ? this.direction : (this.direction + 4) % 8, direction: this.directionType === 'input' ? this.direction : (this.direction + 4) % 8,
directionType: this.directionType directionType: this.directionType
}, })
G.quality.hr
)
) )
} }

View File

@ -31,9 +31,9 @@ export class EntitySprite extends PIXI.Sprite {
return this.nextID return this.nextID
} }
public static getParts(entity: IEntityData | Entity, hr: boolean, positionGrid?: PositionGrid): EntitySprite[] { public static getParts(entity: IEntityData | Entity, positionGrid?: PositionGrid): EntitySprite[] {
const anims = spriteDataBuilder.getSpriteData({ const anims = spriteDataBuilder.getSpriteData({
hr, hr: G.hr,
dir: dir:
positionGrid && entity.type === 'electric_pole' && entity instanceof Entity positionGrid && entity.type === 'electric_pole' && entity instanceof Entity
? G.BPC.wiresContainer.getPowerPoleDirection(entity) ? G.BPC.wiresContainer.getPowerPoleDirection(entity)

View File

@ -10,7 +10,16 @@ import generators, { IVisualization } from './generators'
import History from './history' import History from './history'
import Tile from './tile' import Tile from './tile'
const oilOutpostSettings = { interface IOilOutpostSettings extends Record<string, string | boolean | number> {
DEBUG: boolean
PUMPJACK_MODULE: string
MIN_GAP_BETWEEN_UNDERGROUNDS: number
BEACONS: boolean
MIN_AFFECTED_ENTITIES: number
BEACON_MODULE: string
}
const oilOutpostSettings: IOilOutpostSettings = {
DEBUG: false, DEBUG: false,
PUMPJACK_MODULE: 'productivity_module_3', PUMPJACK_MODULE: 'productivity_module_3',
MIN_GAP_BETWEEN_UNDERGROUNDS: 1, MIN_GAP_BETWEEN_UNDERGROUNDS: 1,
@ -19,6 +28,14 @@ const oilOutpostSettings = {
BEACON_MODULE: 'speed_module_3' BEACON_MODULE: 'speed_module_3'
} }
// this is how it works in factorio but js doesn't support 64bit bitwise operations
// uint64_t(developerVersion) |
// (uint64_t(minorVersion) << 16) |
// (uint64_t(majorVersion) << 32) |
// (uint64_t(mainVersion) << 48)
const getFactorioVersion = (main = 0, major = 17, minor = 14): number =>
(minor << 16) + (major | (main << 16)) * 0xffffffff
class OurMap<K, V> extends Map<K, V> { class OurMap<K, V> extends Map<K, V> {
public constructor(values?: V[], mapFn?: (value: V) => K) { public constructor(values?: V[], mapFn?: (value: V) => K) {
if (values) { if (values) {
@ -566,10 +583,10 @@ export default class Blueprint extends EventEmitter {
entities: this.entities.isEmpty() ? undefined : entityInfo, entities: this.entities.isEmpty() ? undefined : entityInfo,
tiles: this.tiles.isEmpty() ? undefined : tileInfo, tiles: this.tiles.isEmpty() ? undefined : tileInfo,
item: 'blueprint', item: 'blueprint',
version: G.getFactorioVersion(), version: getFactorioVersion(),
label: this.name label: this.name
} }
} }
} }
export { oilOutpostSettings } export { oilOutpostSettings, getFactorioVersion, IOilOutpostSettings }

View File

@ -1,5 +1,4 @@
import G from '../common/globals' import Blueprint, { getFactorioVersion } from './blueprint'
import Blueprint from './blueprint'
export class Book { export class Book {
private _activeIndex: number private _activeIndex: number
@ -56,7 +55,7 @@ export class Book {
blueprints, blueprints,
item: 'blueprint_book', item: 'blueprint_book',
active_index: this._activeIndex, active_index: this._activeIndex,
version: G.getFactorioVersion() version: getFactorioVersion()
} }
} }
} }

View File

@ -120,8 +120,10 @@ function addToShift(shift: IPoint | number[], tab: FD.SpriteData): FD.SpriteData
} }
function setProperty(img: FD.SpriteData, key: string, val: any): FD.SpriteData { function setProperty(img: FD.SpriteData, key: string, val: any): FD.SpriteData {
// @ts-ignore
img[key] = val img[key] = val
if (img.hr_version) { if (img.hr_version) {
// @ts-ignore
img.hr_version[key] = val img.hr_version[key] = val
} }
return img return img
@ -129,8 +131,10 @@ function setProperty(img: FD.SpriteData, key: string, val: any): FD.SpriteData {
function setPropertyUsing(img: FD.SpriteData, key: string, key2: string, mult = 1): FD.SpriteData { function setPropertyUsing(img: FD.SpriteData, key: string, key2: string, mult = 1): FD.SpriteData {
if (key2) { if (key2) {
// @ts-ignore
img[key] = img[key2] * mult img[key] = img[key2] * mult
if (img.hr_version) { if (img.hr_version) {
// @ts-ignore
img.hr_version[key] = img.hr_version[key2] * mult img.hr_version[key] = img.hr_version[key2] * mult
} }
} }

View File

@ -3,32 +3,6 @@ declare module '*.png' {
export default path export default path
} }
declare module '*.json' {
const content: any
export default content
}
interface Navigator {
clipboard: {
writeText?: (s: string) => Promise<void>
readText?: () => Promise<string>
}
}
interface Window {
doorbellOptions: {
tags?: string
id: string
appKey: string
windowLoaded?: boolean
onShow?: () => void
onHide?: () => void
onInitialized?: () => void
}
}
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
interface IPoint { interface IPoint {
x: number x: number
y: number y: number
@ -94,7 +68,7 @@ interface ISpriteData {
} }
/** Namespace for blueprint string interfaces */ /** Namespace for blueprint string interfaces */
namespace BPS { declare namespace BPS {
interface IColor { interface IColor {
r: number r: number
g: number g: number
@ -133,7 +107,7 @@ namespace BPS {
copper?: IWireColor[] copper?: IWireColor[]
} }
interface IConnection extends Record<string, ConnSide | IWireColor[]> { interface IConnection extends Record<string, IConnSide | IWireColor[]> {
1?: IConnSide 1?: IConnSide
2?: IConnSide 2?: IConnSide
Cu0?: IWireColor[] Cu0?: IWireColor[]

File diff suppressed because one or more lines are too long

View File

@ -1,102 +1,32 @@
import * as PIXI from 'pixi.js' import * as PIXI from 'pixi.js'
import FileSaver from 'file-saver' import G from './common/globals'
import U from './common/util'
import { Book } from './factorio-data/book' import { Book } from './factorio-data/book'
import Entity from './factorio-data/entity'
import Blueprint, { oilOutpostSettings, IOilOutpostSettings } from './factorio-data/blueprint'
import bpString, { ModdedBlueprintError, TrainBlueprintError } from './factorio-data/bpString' import bpString, { ModdedBlueprintError, TrainBlueprintError } from './factorio-data/bpString'
import G from './common/globals'
import { TilePaintContainer } from './containers/paintTile'
import { BlueprintContainer, EditorMode } from './containers/blueprint'
import Blueprint from './factorio-data/blueprint'
import initDoorbell from './doorbell'
import actions from './actions'
import initDatGui from './datgui'
import initToasts from './toasts'
import spritesheetsLoader from './spritesheetsLoader'
import Entity from './factorio-data/entity'
import Dialog from './UI/controls/dialog'
import { EntityContainer } from './containers/entity' import { EntityContainer } from './containers/entity'
import U from './common/util' import { TilePaintContainer } from './containers/paintTile'
import { BlueprintContainer, EditorMode, GridPattern } from './containers/blueprint'
import UIContainer from './UI/ui' import UIContainer from './UI/ui'
import Dialog from './UI/controls/dialog'
if (PIXI.utils.isMobile.any) { import {
document.getElementById('loadingScreen').classList.add('mobileError') initActions,
throw new Error('MOBILE DEVICE DETECTED') registerAction,
} callAction,
forEachAction,
console.log( resetKeybinds,
'\n%cLooking for the source?\nhttps://github.com/Teoxoy/factorio-blueprint-editor\n', importKeybinds,
'color: #1f79aa; font-weight: bold' exportKeybinds
) } from './actions'
import spritesheetsLoader from './spritesheetsLoader'
const params = window.location.search.slice(1).split('&')
let bpSource: string
let bpIndex = 0
for (const p of params) {
if (p.includes('source')) {
bpSource = p.split('=')[1]
}
if (p.includes('index')) {
bpIndex = Number(p.split('=')[1])
}
}
const { guiBPIndex } = initDatGui()
initDoorbell()
const createToast = initToasts()
function createErrorMessage(text: string, error: unknown): void {
console.error(error)
createToast({
text:
`${text}<br>` +
'Please check out the console (F12) for an error message and ' +
'report this bug on github or using the feedback button.',
type: 'error',
timeout: 10000
})
}
function createBPImportError(error: Error | ModdedBlueprintError): void {
if (error instanceof TrainBlueprintError) {
createErrorMessage(
'Blueprint with train entities not supported yet. If you think this is a mistake:',
error.errors
)
return
}
if (error instanceof ModdedBlueprintError) {
createErrorMessage(
'Blueprint with modded items not supported yet. If you think this is a mistake:',
error.errors
)
return
}
createErrorMessage('Blueprint string could not be loaded.', error)
}
function createWelcomeMessage(): void {
const notFirstRun = localStorage.getItem('firstRun') === 'false'
if (notFirstRun) {
return
}
localStorage.setItem('firstRun', 'false')
// Wait a bit just to capture the users attention
// This way they will see the toast animation
setTimeout(() => {
createToast({
text:
'> To access the inventory and start building press E<br>' +
'> To import/export a blueprint string use ctrl/cmd + C/V<br>' +
'> For more info press I<br>' +
'> Also check out the settings area',
timeout: 30000
})
}, 1000)
}
function initEditor(canvas: HTMLCanvasElement): Promise<void[]> {
PIXI.settings.MIPMAP_TEXTURES = PIXI.MIPMAP_MODES.ON PIXI.settings.MIPMAP_TEXTURES = PIXI.MIPMAP_MODES.ON
PIXI.settings.ROUND_PIXELS = true PIXI.settings.ROUND_PIXELS = true
PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.LINEAR PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.LINEAR
@ -111,20 +41,14 @@ PIXI.settings.ANISOTROPIC_LEVEL = 16
// PIXI.settings.PRECISION_VERTEX = PIXI.PRECISION.HIGH // PIXI.settings.PRECISION_VERTEX = PIXI.PRECISION.HIGH
// PIXI.settings.PRECISION_FRAGMENT = PIXI.PRECISION.HIGH // PIXI.settings.PRECISION_FRAGMENT = PIXI.PRECISION.HIGH
G.app = new PIXI.Application({ view: document.getElementById('editor') as HTMLCanvasElement }) G.app = new PIXI.Application({ view: canvas })
// https://github.com/pixijs/pixi.js/issues/3928 // https://github.com/pixijs/pixi.js/issues/3928
// G.app.renderer.plugins.interaction.moveWhenInside = true // G.app.renderer.plugins.interaction.moveWhenInside = true
// G.app.renderer.plugins.interaction.interactionFrequency = 1 // G.app.renderer.plugins.interaction.interactionFrequency = 1
G.app.renderer.resize(window.innerWidth, window.innerHeight) G.app.renderer.resize(window.innerWidth, window.innerHeight)
window.addEventListener( window.addEventListener('resize', () => G.app.renderer.resize(window.innerWidth, window.innerHeight), false)
'resize',
() => {
G.app.renderer.resize(window.innerWidth, window.innerHeight)
},
false
)
G.BPC = new BlueprintContainer() G.BPC = new BlueprintContainer()
G.app.stage.addChild(G.BPC) G.app.stage.addChild(G.BPC)
@ -133,65 +57,8 @@ G.UI = new UIContainer()
G.app.stage.addChild(G.UI) G.app.stage.addChild(G.UI)
G.UI.showDebuggingLayer = G.debug G.UI.showDebuggingLayer = G.debug
Promise.all([ initActions(canvas)
// Get bp from source registerActions()
// catch the error here so that Promise.all can resolve
bpString.getBlueprintOrBookFromSource(bpSource).catch(error => {
createBPImportError(error)
return new Blueprint()
}),
// Wait for fonts to get loaded
document.fonts.ready,
// Load spritesheets
...spritesheetsLoader.getAllPromises()
])
.then(data => {
// Load quickbarItemNames from localStorage
if (localStorage.getItem('quickbarItemNames')) {
const quickbarItemNames = JSON.parse(localStorage.getItem('quickbarItemNames'))
G.UI.quickbarContainer.generateSlots(quickbarItemNames)
}
loadBp(data[0], false)
createWelcomeMessage()
})
.catch(error => createErrorMessage('Something went wrong.', error))
function loadBp(bpOrBook: Blueprint | Book, clearData = true): void {
if (bpOrBook instanceof Book) {
G.book = bpOrBook
G.bp = G.book.getBlueprint(bpIndex ? bpIndex : undefined)
guiBPIndex.max(G.book.lastBookIndex).setValue(G.book.activeIndex)
} else {
G.book = undefined
G.bp = bpOrBook
guiBPIndex.setValue(0).max(0)
}
if (clearData) {
G.BPC.clearData()
}
G.BPC.initBP()
G.loadingScreen.hide()
if (!(bpOrBook instanceof Blueprint && bpOrBook.isEmpty())) {
createToast({ text: 'Blueprint string loaded successfully', type: 'success' })
}
Dialog.closeAll()
}
// If the tab is not active then stop the app
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
G.app.start()
} else {
G.app.stop()
}
})
window.addEventListener('unload', () => { window.addEventListener('unload', () => {
G.app.stop() G.app.stop()
@ -199,122 +66,37 @@ window.addEventListener('unload', () => {
G.app.destroy() G.app.destroy()
}) })
// ACTIONS // return Promise.all(
// Load spritesheets
actions.importKeybinds(JSON.parse(localStorage.getItem('keybinds'))) spritesheetsLoader.getAllPromises()
)
window.addEventListener('unload', () => {
const keybinds = actions.exportKeybinds()
if (Object.keys(keybinds).length) {
localStorage.setItem('keybinds', JSON.stringify(keybinds))
} else {
localStorage.removeItem('keybinds')
}
})
actions.copyBPString.bind({
press: e => {
if (G.bp.isEmpty()) {
return
} }
const onSuccess = (): void => { function loadBlueprint(bp: Blueprint): void {
createToast({ text: 'Blueprint string copied to clipboard', type: 'success' }) G.bp = bp
G.BPC.clearData()
G.BPC.initBP()
Dialog.closeAll()
} }
const onError = (error: Error): void => { function registerActions(): void {
createErrorMessage('Blueprint string could not be generated.', error) registerAction('moveUp', 'w')
} registerAction('moveLeft', 'a')
registerAction('moveDown', 's')
registerAction('moveRight', 'd')
const bpOrBook = G.book ? G.book : G.bp registerAction('showInfo', 'alt').bind({
if (navigator.clipboard && navigator.clipboard.writeText) {
bpString
.encode(bpOrBook)
.then(s => navigator.clipboard.writeText(s))
.then(onSuccess)
.catch(onError)
} else {
const data = bpString.encodeSync(bpOrBook)
if (data.value) {
e.clipboardData.setData('text/plain', data.value)
onSuccess()
} else {
onError(data.error)
}
}
}
})
actions.pasteBPString.bind({
press: e => {
G.loadingScreen.show()
const promise =
navigator.clipboard && navigator.clipboard.readText
? navigator.clipboard.readText()
: Promise.resolve(e.clipboardData.getData('text'))
promise
.then(bpString.getBlueprintOrBookFromSource)
.then(loadBp)
.catch(error => {
G.loadingScreen.hide()
createBPImportError(error)
})
}
})
actions.clear.bind({
press: () => {
loadBp(new Blueprint())
}
})
actions.takePicture.bind({
press: () => {
if (G.bp.isEmpty()) {
return
}
// getLocalBounds is needed because it seems that it has sideeffects
// without it generateTexture returns an empty texture
G.BPC.getLocalBounds()
const region = G.BPC.getBlueprintBounds()
const texture = G.app.renderer.generateTexture(G.BPC, PIXI.SCALE_MODES.LINEAR, 1, region)
const canvas = G.app.renderer.plugins.extract.canvas(texture)
canvas.toBlob(blob => {
FileSaver.saveAs(blob, `${G.bp.name}.png`)
createToast({ text: 'Blueprint image successfully generated', type: 'success' })
// Clear
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height)
})
}
})
actions.showInfo.bind({
press: () => G.BPC.overlayContainer.toggleEntityInfoVisibility() press: () => G.BPC.overlayContainer.toggleEntityInfoVisibility()
}) })
actions.info.bind({ registerAction('closeWindow', 'esc').bind({
press: () => {
const infoPanel = document.getElementById('info-panel')
if (infoPanel.classList.contains('active')) {
infoPanel.classList.remove('active')
} else {
infoPanel.classList.add('active')
}
}
})
actions.closeWindow.bind({
press: () => { press: () => {
Dialog.closeLast() Dialog.closeLast()
} }
}) })
actions.inventory.bind({ registerAction('inventory', 'e').bind({
press: () => { press: () => {
// If there is a dialog open, assume user wants to close it // If there is a dialog open, assume user wants to close it
if (Dialog.anyOpen()) { if (Dialog.anyOpen()) {
@ -325,9 +107,9 @@ actions.inventory.bind({
} }
}) })
actions.focus.bind({ press: () => G.BPC.centerViewport() }) registerAction('focus', 'f').bind({ press: () => G.BPC.centerViewport() })
actions.rotate.bind({ registerAction('rotate', 'r').bind({
press: () => { press: () => {
if (G.BPC.mode === EditorMode.EDIT) { if (G.BPC.mode === EditorMode.EDIT) {
G.BPC.hoverContainer.entity.rotate(false, true) G.BPC.hoverContainer.entity.rotate(false, true)
@ -337,7 +119,7 @@ actions.rotate.bind({
} }
}) })
actions.reverseRotate.bind({ registerAction('reverseRotate', 'shift+r').bind({
press: () => { press: () => {
if (G.BPC.mode === EditorMode.EDIT) { if (G.BPC.mode === EditorMode.EDIT) {
G.BPC.hoverContainer.entity.rotate(true, true) G.BPC.hoverContainer.entity.rotate(true, true)
@ -347,7 +129,7 @@ actions.reverseRotate.bind({
} }
}) })
actions.pipette.bind({ registerAction('pipette', 'q').bind({
press: () => { press: () => {
if (G.BPC.mode === EditorMode.EDIT) { if (G.BPC.mode === EditorMode.EDIT) {
const entity = G.BPC.hoverContainer.entity const entity = G.BPC.hoverContainer.entity
@ -362,7 +144,7 @@ actions.pipette.bind({
} }
}) })
actions.increaseTileBuildingArea.bind({ registerAction('increaseTileBuildingArea', ']').bind({
press: () => { press: () => {
if (G.BPC.paintContainer instanceof TilePaintContainer) { if (G.BPC.paintContainer instanceof TilePaintContainer) {
G.BPC.paintContainer.increaseSize() G.BPC.paintContainer.increaseSize()
@ -370,7 +152,7 @@ actions.increaseTileBuildingArea.bind({
} }
}) })
actions.decreaseTileBuildingArea.bind({ registerAction('decreaseTileBuildingArea', '[').bind({
press: () => { press: () => {
if (G.BPC.paintContainer instanceof TilePaintContainer) { if (G.BPC.paintContainer instanceof TilePaintContainer) {
G.BPC.paintContainer.decreaseSize() G.BPC.paintContainer.decreaseSize()
@ -378,56 +160,47 @@ actions.decreaseTileBuildingArea.bind({
} }
}) })
actions.undo.bind({ registerAction('undo', 'modifier+z').bind({
press: () => { press: () => {
G.bp.history.undo() G.bp.history.undo()
}, },
repeat: true repeat: true
}) })
actions.redo.bind({ registerAction('redo', 'modifier+y').bind({
press: () => { press: () => {
G.bp.history.redo() G.bp.history.redo()
}, },
repeat: true repeat: true
}) })
actions.generateOilOutpost.bind({ registerAction('copySelection', 'modifier+lclick').bind({
press: () => {
const errorMessage = G.bp.generatePipes()
if (errorMessage) {
createToast({ text: errorMessage, type: 'warning' })
}
}
})
actions.copySelection.bind({
press: () => G.BPC.enterCopyMode(), press: () => G.BPC.enterCopyMode(),
release: () => G.BPC.exitCopyMode() release: () => G.BPC.exitCopyMode()
}) })
actions.deleteSelection.bind({ registerAction('deleteSelection', 'modifier+rclick').bind({
press: () => G.BPC.enterDeleteMode(), press: () => G.BPC.enterDeleteMode(),
release: () => G.BPC.exitDeleteMode() release: () => G.BPC.exitDeleteMode()
}) })
actions.pan.bind({ registerAction('pan', 'lclick').bind({
press: () => G.BPC.enterPanMode(), press: () => G.BPC.enterPanMode(),
release: () => G.BPC.exitPanMode() release: () => G.BPC.exitPanMode()
}) })
actions.zoomIn.bind({ registerAction('zoomIn', 'wheelNeg').bind({
press: () => { press: () => {
G.BPC.zoom(true) G.BPC.zoom(true)
} }
}) })
actions.zoomOut.bind({ registerAction('zoomOut', 'wheelPos').bind({
press: () => { press: () => {
G.BPC.zoom(false) G.BPC.zoom(false)
} }
}) })
actions.build.bind({ registerAction('build', 'lclick').bind({
press: () => { press: () => {
if (G.BPC.mode === EditorMode.PAINT) { if (G.BPC.mode === EditorMode.PAINT) {
G.BPC.paintContainer.placeEntityContainer() G.BPC.paintContainer.placeEntityContainer()
@ -436,7 +209,7 @@ actions.build.bind({
repeat: true repeat: true
}) })
actions.mine.bind({ registerAction('mine', 'rclick').bind({
press: () => { press: () => {
if (G.BPC.mode === EditorMode.EDIT) { if (G.BPC.mode === EditorMode.EDIT) {
G.bp.removeEntity(G.BPC.hoverContainer.entity) G.bp.removeEntity(G.BPC.hoverContainer.entity)
@ -448,28 +221,28 @@ actions.mine.bind({
repeat: true repeat: true
}) })
actions.moveEntityUp.bind({ registerAction('moveEntityUp', 'up').bind({
press: () => { press: () => {
if (G.BPC.mode === EditorMode.EDIT) { if (G.BPC.mode === EditorMode.EDIT) {
G.BPC.hoverContainer.entity.moveBy({ x: 0, y: -1 }) G.BPC.hoverContainer.entity.moveBy({ x: 0, y: -1 })
} }
} }
}) })
actions.moveEntityLeft.bind({ registerAction('moveEntityLeft', 'left').bind({
press: () => { press: () => {
if (G.BPC.mode === EditorMode.EDIT) { if (G.BPC.mode === EditorMode.EDIT) {
G.BPC.hoverContainer.entity.moveBy({ x: -1, y: 0 }) G.BPC.hoverContainer.entity.moveBy({ x: -1, y: 0 })
} }
} }
}) })
actions.moveEntityDown.bind({ registerAction('moveEntityDown', 'down').bind({
press: () => { press: () => {
if (G.BPC.mode === EditorMode.EDIT) { if (G.BPC.mode === EditorMode.EDIT) {
G.BPC.hoverContainer.entity.moveBy({ x: 0, y: 1 }) G.BPC.hoverContainer.entity.moveBy({ x: 0, y: 1 })
} }
} }
}) })
actions.moveEntityRight.bind({ registerAction('moveEntityRight', 'right').bind({
press: () => { press: () => {
if (G.BPC.mode === EditorMode.EDIT) { if (G.BPC.mode === EditorMode.EDIT) {
G.BPC.hoverContainer.entity.moveBy({ x: 1, y: 0 }) G.BPC.hoverContainer.entity.moveBy({ x: 1, y: 0 })
@ -477,7 +250,7 @@ actions.moveEntityRight.bind({
} }
}) })
actions.openEntityGUI.bind({ registerAction('openEntityGUI', 'lclick').bind({
press: () => { press: () => {
if (G.BPC.mode === EditorMode.EDIT) { if (G.BPC.mode === EditorMode.EDIT) {
if (G.debug) { if (G.debug) {
@ -491,7 +264,8 @@ actions.openEntityGUI.bind({
}) })
let entityForCopyData: Entity | undefined let entityForCopyData: Entity | undefined
actions.copyEntitySettings.bind({ const copyEntitySettingsAction = registerAction('copyEntitySettings', 'shift+rclick')
copyEntitySettingsAction.bind({
press: () => { press: () => {
if (G.BPC.mode === EditorMode.EDIT) { if (G.BPC.mode === EditorMode.EDIT) {
// Store reference to source entity // Store reference to source entity
@ -499,7 +273,7 @@ actions.copyEntitySettings.bind({
} }
} }
}) })
actions.pasteEntitySettings.bind({ registerAction('pasteEntitySettings', 'shift+lclick').bind({
press: () => { press: () => {
if (entityForCopyData && G.BPC.mode === EditorMode.EDIT) { if (entityForCopyData && G.BPC.mode === EditorMode.EDIT) {
// Hand over reference of source entity to target entity for pasting data // Hand over reference of source entity to target entity for pasting data
@ -524,7 +298,7 @@ actions.pasteEntitySettings.bind({
copyCursorBox = G.BPC.overlayContainer.createCursorBox(srcEnt.position, entityForCopyData.size, 'copy') copyCursorBox = G.BPC.overlayContainer.createCursorBox(srcEnt.position, entityForCopyData.size, 'copy')
Promise.race([ Promise.race([
deferred.promise, deferred.promise,
new Promise(resolve => actions.copyEntitySettings.bind({ press: resolve, once: true })), new Promise(resolve => copyEntitySettingsAction.bind({ press: resolve, once: true })),
new Promise(resolve => G.BPC.once('removeHoverContainer', resolve)) new Promise(resolve => G.BPC.once('removeHoverContainer', resolve))
]).then(() => { ]).then(() => {
deferred.reset() deferred.reset()
@ -533,22 +307,88 @@ actions.pasteEntitySettings.bind({
}) })
} }
} }
actions.tryPasteEntitySettings.bind({ press: createCopyCursorBox, release: () => deferred.resolve() }) const action = registerAction('tryPasteEntitySettings', 'shift')
action.bind({ press: createCopyCursorBox, release: () => deferred.resolve() })
G.BPC.on('createHoverContainer', () => { G.BPC.on('createHoverContainer', () => {
if (actions.tryPasteEntitySettings.pressed) { if (action.pressed) {
createCopyCursorBox() createCopyCursorBox()
} }
}) })
} }
actions.quickbar1.bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(0) }) registerAction('quickbar1', '1').bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(0) })
actions.quickbar2.bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(1) }) registerAction('quickbar2', '2').bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(1) })
actions.quickbar3.bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(2) }) registerAction('quickbar3', '3').bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(2) })
actions.quickbar4.bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(3) }) registerAction('quickbar4', '4').bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(3) })
actions.quickbar5.bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(4) }) registerAction('quickbar5', '5').bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(4) })
actions.quickbar6.bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(5) }) registerAction('quickbar6', 'shift+1').bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(5) })
actions.quickbar7.bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(6) }) registerAction('quickbar7', 'shift+2').bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(6) })
actions.quickbar8.bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(7) }) registerAction('quickbar8', 'shift+3').bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(7) })
actions.quickbar9.bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(8) }) registerAction('quickbar9', 'shift+4').bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(8) })
actions.quickbar10.bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(9) }) registerAction('quickbar10', 'shift+5').bind({ press: () => G.UI.quickbarContainer.bindKeyToSlot(9) })
actions.changeActiveQuickbar.bind({ press: () => G.UI.quickbarContainer.changeActiveQuickbar() }) registerAction('changeActiveQuickbar', 'x').bind({ press: () => G.UI.quickbarContainer.changeActiveQuickbar() })
}
const getPicture = (): Promise<Blob> => G.BPC.getPicture()
const getMoveSpeed = (): number => G.BPC.moveSpeed
const setMoveSpeed = (speed: number): void => {
G.BPC.moveSpeed = speed
}
const getGridColor = (): number => G.BPC.gridColor
const setGridColor = (color: number): void => {
G.BPC.gridColor = color
}
const getGridPattern = (): GridPattern => G.BPC.gridPattern
const setGridPattern = (pattern: GridPattern): void => {
G.BPC.gridPattern = pattern
}
const getQuickbarItems = (): string[] => G.UI.quickbarContainer.serialize()
const setQuickbarItems = (items: string[]): void => {
G.UI.quickbarContainer.generateSlots(items)
}
const getOilOutpostSettings = (): IOilOutpostSettings => oilOutpostSettings
const setOilOutpostSettings = (settings: IOilOutpostSettings): void => {
Object.keys(oilOutpostSettings).forEach(k => {
if (settings[k]) {
oilOutpostSettings[k] = settings[k]
}
})
}
const isDebuggingOn = (): boolean => G.debug
const setDebugging = (on: boolean): void => {
G.debug = on
G.UI.showDebuggingLayer = on
if (G.bp) {
G.bp.history.logging = on
}
}
export { Book, Blueprint, ModdedBlueprintError, TrainBlueprintError }
export default {
initEditor,
bpStringEncodeDecode: bpString,
loadBlueprint,
registerAction,
getPicture,
getMoveSpeed,
setMoveSpeed,
getGridColor,
setGridColor,
getGridPattern,
setGridPattern,
getQuickbarItems,
setQuickbarItems,
getOilOutpostSettings,
setOilOutpostSettings,
isDebuggingOn,
setDebugging,
callAction,
forEachAction,
resetKeybinds,
importKeybinds,
exportKeybinds
}

View File

@ -1,7 +1,5 @@
import LRentitySpritesheetPNG from 'factorio-data/data/graphics/LREntitySpritesheet.png'
import LRentitySpritesheetCompressedPNG from 'factorio-data/data/graphics/LREntitySpritesheetCompressed.png' import LRentitySpritesheetCompressedPNG from 'factorio-data/data/graphics/LREntitySpritesheetCompressed.png'
import LRentitySpritesheetJSON from 'factorio-data/data/graphics/LREntitySpritesheet.json' import LRentitySpritesheetJSON from 'factorio-data/data/graphics/LREntitySpritesheet.json'
import HRentitySpritesheetPNG from 'factorio-data/data/graphics/HREntitySpritesheet.png'
import HRentitySpritesheetCompressedPNG from 'factorio-data/data/graphics/HREntitySpritesheetCompressed.png' import HRentitySpritesheetCompressedPNG from 'factorio-data/data/graphics/HREntitySpritesheetCompressed.png'
import HRentitySpritesheetJSON from 'factorio-data/data/graphics/HREntitySpritesheet.json' import HRentitySpritesheetJSON from 'factorio-data/data/graphics/HREntitySpritesheet.json'
import iconSpritesheetPNG from 'factorio-data/data/graphics/iconSpritesheet.png' import iconSpritesheetPNG from 'factorio-data/data/graphics/iconSpritesheet.png'
@ -18,47 +16,13 @@ import util from './common/util'
function getAllPromises(): Promise<void>[] { function getAllPromises(): Promise<void>[] {
return [ return [
[ [
/* eslint-disable no-nested-ternary */ G.hr ? HRentitySpritesheetCompressedPNG : LRentitySpritesheetCompressedPNG,
G.quality.hr G.hr ? HRentitySpritesheetJSON : LRentitySpritesheetJSON
? G.quality.compressed
? HRentitySpritesheetCompressedPNG
: HRentitySpritesheetPNG
: G.quality.compressed
? LRentitySpritesheetCompressedPNG
: LRentitySpritesheetPNG,
/* eslint-enable no-nested-ternary */
G.quality.hr ? HRentitySpritesheetJSON : LRentitySpritesheetJSON
], ],
[iconSpritesheetPNG, iconSpritesheetJSON], [iconSpritesheetPNG, iconSpritesheetJSON],
[utilitySpritesheetPNG, utilitySpritesheetJSON], [utilitySpritesheetPNG, utilitySpritesheetJSON],
[tilesSpritesheetPNG, tilesSpritesheetJSON] [tilesSpritesheetPNG, tilesSpritesheetJSON]
].map(data => loadSpritesheet(data[0], data[1])) ].map(data => loadSpritesheet(data[0] as string, data[1]))
}
function changeQuality(hr: boolean, compressed: boolean): void {
G.loadingScreen.show()
G.BPC.clearData()
Object.keys(PIXI.utils.TextureCache)
.filter(texture => texture.includes('graphics/entity/'))
.forEach(k => PIXI.utils.TextureCache[k].destroy(true))
loadSpritesheet(
/* eslint-disable no-nested-ternary */
hr
? compressed
? HRentitySpritesheetCompressedPNG
: HRentitySpritesheetPNG
: compressed
? LRentitySpritesheetCompressedPNG
: LRentitySpritesheetPNG,
/* eslint-enable no-nested-ternary */
hr ? HRentitySpritesheetJSON : LRentitySpritesheetJSON
).then(() => {
G.BPC.initBP()
G.loadingScreen.hide()
})
} }
function blobToImageBitmap(blob: Blob): Promise<ImageBitmap | HTMLImageElement> { function blobToImageBitmap(blob: Blob): Promise<ImageBitmap | HTMLImageElement> {
@ -108,6 +72,5 @@ function loadSpritesheet(src: string, json: any): Promise<void> {
} }
export default { export default {
getAllPromises, getAllPromises
changeQuality
} }

View File

@ -0,0 +1,18 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"target": "es6",
"module": "es6",
"moduleResolution": "node",
"lib": ["dom", "es6", "es2016.array.include", "es2017.object"],
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
}
}

View File

@ -0,0 +1,4 @@
> 1%
last 2 versions
Firefox ESR
not dead

View File

@ -0,0 +1,2 @@
plugins:
postcss-preset-env: {}

View File

@ -0,0 +1,45 @@
{
"name": "@fbe/website",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "webpack-dev-server",
"build": "webpack --mode=production"
},
"dependencies": {
"@fbe/editor": "*",
"dat.gui": "^0.7.6",
"file-saver": "^2.0.2"
},
"devDependencies": {
"@types/dat.gui": "^0.7.4",
"@types/file-saver": "^2.0.1",
"@types/html-webpack-plugin": "^3.2.1",
"@types/mini-css-extract-plugin": "^0.8.0",
"@types/node": "^12.6.9",
"@types/optimize-css-assets-webpack-plugin": "^1.3.4",
"@types/terser-webpack-plugin": "^1.2.1",
"@types/webpack": "^4.32.1",
"@types/webpack-dev-server": "^3.1.7",
"clean-webpack-plugin": "^3.0.0",
"css-loader": "^3.1.0",
"extract-loader": "^3.1.0",
"file-loader": "^4.1.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.8.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.2",
"svg-inline-loader": "^0.8.0",
"terser-webpack-plugin": "^1.4.1",
"ts-loader": "^6.0.4",
"ts-node": "^8.3.0",
"typescript": "^3.5.3",
"webpack": "^4.39.1",
"webpack-cli": "^3.3.6",
"webpack-dev-server": "^3.7.2"
}
}

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 487 KiB

View File

@ -1,4 +1,19 @@
export default function initDoorbell(): void { declare global {
// eslint-disable-next-line @typescript-eslint/interface-name-prefix
interface Window {
doorbellOptions: {
tags?: string
id: string
appKey: string
windowLoaded?: boolean
onShow?: () => void
onHide?: () => void
onInitialized?: () => void
}
}
}
export default function initFeedbackButton(): void {
window.doorbellOptions = { window.doorbellOptions = {
id: '9657', id: '9657',
appKey: 'z1scfSY8hpBNiIFWxBg50tkhjvFKhHMdhfGNMp6YCUZVttoLOqtrlhk4ca9asDCy', appKey: 'z1scfSY8hpBNiIFWxBg50tkhjvFKhHMdhfGNMp6YCUZVttoLOqtrlhk4ca9asDCy',

View File

@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="initial-scale=1, maximum-scale=1, user-scalable=no, minimum-scale=1, width=device-width, height=device-height"
/>
<meta
name="description"
content="A feature-rich Factorio Blueprint Editor. You can now edit your blueprints in the browser!"
/>
<title>Factorio Blueprint Editor</title>
<link rel="icon" href="assets/favicon.png" />
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css?family=Cookie" rel="stylesheet" />
</head>
<body oncontextmenu="return false;">
<div id="loadingScreen" class="active">
<p id="mobileError">This application is not compatible with mobile devices!</p>
<img id="logo" src="assets/logo.svg" />
<img class="loadingWheel" src="assets/loadingWheel.svg" />
</div>
<div id="corner-panel">
<!-- prettier-ignore -->
<svg viewBox="0 0 154 60" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.41421"><g fill="#fe7520"><path d="M134.156 58.027h-31.954l-5.021-5.223V18.555l5.021-5.223h31.954l5.02 5.223v6.132h-27.905v6.648h19.68v8.689h-19.68v6.648h27.905v6.132l-5.02 5.223zM15.711 58.027H6.638l-5.021-5.223V18.555l5.021-5.223h31.953l5.021 5.223v6.132H15.706v6.679h19.681v8.69h-19.68l.004 17.971zM85.662 58.027H53.708l-5.02-5.223V18.555l5.02-5.223h23.601l5.02 5.223v6.132h-.003v6.639l8.357 8.737v12.741l-5.021 5.223zM75.266 40.024H62.777v6.648H79.71l.001-2.023-4.445-4.625zm-3.749-15.337h-8.74v6.648h8.74v-6.648zM146.876 46.672l5.406 5.228v6.127h-11.741V46.672h6.335zM49.843 14.755h-7.645l-4.479-4.48 8.302-8.302 8.302 8.302-4.48 4.48z"/><path d="M89.311 36.143l-5.406-5.406v-6.335h11.741v11.741h-6.335z"/></g></svg>
<span>Press I for info</span>
</div>
<div id="buttons">
<a class="discord-button" target="_blank" href="https://discord.gg/c5eXyBU">
<span>Community</span>
<!-- prettier-ignore -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><path fill="#FFF" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zm36.5 0c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path fill="#FFF" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>
</a>
<a class="bmc-button" target="_blank" href="https://www.buymeacoffee.com/teoxoy">
<span>Donate!</span>
<!-- prettier-ignore -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 6 6"><path fill="#fff" d="M5.476.393c-.022.023-.036.055-.036.09v.093h-.093c-.072 0-.13.058-.13.13 0 .072.058.13.13.13h.093v.093c0 .072.058.13.13.13.072 0 .13-.058.13-.13V.836h.093c.072 0 .13-.058.13-.13 0-.072-.058-.13-.13-.13H5.7V.483c0-.072-.058-.13-.13-.13-.037 0-.07.016-.094.04z"/><path fill="#fff" fill-opacity=".2" d="M.83.893c.416 0 .753.338.753.753 0 .416-.337.753-.753.753-.415 0-.753-.337-.753-.753 0-.415.338-.753.753-.753zm0 .649c.058 0 .104.047.104.104 0 .058-.046.104-.104.104-.057 0-.104-.046-.104-.104 0-.057.047-.104.104-.104zM5.268 3.125c.111 0 .201-.091.201-.202 0-.111-.09-.202-.201-.202-.111 0-.201.091-.201.202 0 .111.09.202.201.202z"/><path fill="#fff" d="M1.497 4.284c.111 0 .201-.091.201-.202 0-.111-.09-.201-.201-.201-.111 0-.201.09-.201.201 0 .111.09.202.201.202z"/><path fill="#ff9100" d="M4.675 1.14H2.137l.572 3.642h1.394l.572-3.642z"/><path fill="#fd0" d="M4.383 1.14H2.137l.572 3.642h1.102l.572-3.642z"/><path fill="#fff" d="M1.803.804h3.006v.336H1.803z"/><path d="M1.701.702h3.21v.54h-3.21v-.54zm.204.204v.132h2.802V.906H1.905z"/><path fill="#fff" d="M4.229.258H2.367l-.219.504h2.299L4.229.258z"/><path fill="#050505" d="M4.296.156l.306.708H1.993L2.3.156h1.996zM2.434.36l-.131.3h1.989l-.13-.3H2.434z"/><path d="M4.794 1.037l-.605 3.847H2.413l-.604-3.847h2.985zm-.239.205H2.047l.54 3.438h1.428l.54-3.438z"/><path fill="#fff" d="M4.717 2.168h-2.84l.256 1.449h2.328l.256-1.449z"/><path d="M4.839 2.066L4.546 3.72H2.048l-.292-1.654h3.083zm-.464 1.448l.22-1.244H1.999l.22 1.244h2.156z"/></svg>
</a>
<a class="github-button" target="_blank" href="https://github.com/Teoxoy/factorio-blueprint-editor">
<span>Source</span>
<!-- prettier-ignore -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path fill="#181616" d="M320 7.9C143.3 7.9 0 151.17 0 327.9c0 141.39 91.68 261.33 218.85 303.65 16 2.93 21.84-6.95 21.84-15.43 0-7.59-.27-27.72-.43-54.42-89 19.34-107.8-42.9-107.8-42.9-14.55-36.96-35.52-46.8-35.52-46.8-29.06-19.85 2.19-19.45 2.19-19.45 32.12 2.26 49 33 49 33 28.54 48.9 74.89 34.78 93.13 26.59 2.9-20.68 11.17-34.78 20.31-42.78-71-8.08-145.76-35.53-145.76-158.16 0-34.93 12.47-63.5 32.94-85.86-3.29-8.1-14.27-40.64 3.14-84.69 0 0 26.87-8.61 88 32.8a303.32 303.32 0 0 1 160.22 0C461.22 132 488 140.62 488 140.62c17.46 44 6.48 76.59 3.19 84.69 20.51 22.36 32.89 50.93 32.89 85.86 0 122.93-74.82 150-146.11 157.91 11.49 9.88 21.73 29.41 21.73 59.26 0 42.78-.39 77.29-.39 87.78 0 8.56 5.76 18.52 22 15.39C548.4 589.11 640 469.25 640 327.92 640 151.17 496.71 7.9 320 7.9z"/></svg>
</a>
</div>
<div id="info-panel">
<h1>INFO</h1>
<ul class="info-list">
<li>To access the inventory and start building press <mark>E</mark></li>
<li>To import/export a blueprint string use <mark>ctrl/cmd + C/V</mark></li>
<li>Check out the settings area for an in depth look at the settings and <mark>keybinds</mark></li>
<li>
Leave your suggestions, ideas, new features or bug reports inside the app via the Feedback button or
on Github
</li>
</ul>
<h2>Features</h2>
<ul>
<li>rendering and editing blueprints</li>
<li>history (undo/redo)</li>
<li>copy and delete selections</li>
<li>
import blueprints and books from multiple sources (direct bp string, pastebin, hastebin, gist,
gitlab, factorioprints, google docs)
</li>
<li>generating blueprint images</li>
<li>oil outpost generator</li>
<li>customizable keybinds</li>
<li>"creative" entities</li>
</ul>
<h2>Planned Features</h2>
<ul>
<li>more blueprint tools</li>
<li>more entity editors</li>
<li>full rails support</li>
<li>trains support</li>
<li>mod support</li>
</ul>
<h3>Avalible url query parameters</h3>
<ul>
<li>source=&lt;BPSTRING_OR_URL_TO_BPSTRING&gt;</li>
<li>index=&lt;INDEX_OF_BP_IN_BOOK&gt;</li>
</ul>
<p class="copyright-notice">
All art assets, spritesheets and other Factorio game data used in this project belong to Wube Software
Ltd and are not for redistribution
</p>
</div>
<canvas id="editor" tabindex="1"></canvas>
</body>
</html>

View File

@ -0,0 +1,261 @@
import { utils as pixiUtils } from 'pixi.js'
import FileSaver from 'file-saver'
import { GUIController } from 'dat.gui'
import EDITOR, { Blueprint, Book, TrainBlueprintError, ModdedBlueprintError } from '@fbe/editor'
import initToasts from './toasts'
import initFeedbackButton from './feedbackButton'
import initSettingsPane from './settingsPane'
const CANVAS = document.getElementById('editor') as HTMLCanvasElement
let bp: Blueprint
let book: Book
const loadingScreen = {
el: document.getElementById('loadingScreen'),
show() {
this.el.classList.add('active')
},
hide() {
this.el.classList.remove('active')
}
}
if (pixiUtils.isMobile.any) {
loadingScreen.el.classList.add('mobileError')
throw new Error('MOBILE DEVICE DETECTED')
}
console.log(
'\n%cLooking for the source?\nhttps://github.com/Teoxoy/factorio-blueprint-editor\n',
'color: #1f79aa; font-weight: bold'
)
initFeedbackButton()
const createToast = initToasts()
const params = window.location.search.slice(1).split('&')
let bpSource: string
let bpIndex = 0
for (const p of params) {
if (p.includes('source')) {
bpSource = p.split('=')[1]
}
if (p.includes('index')) {
bpIndex = Number(p.split('=')[1])
}
}
let guiBPIndex: GUIController
EDITOR.initEditor(CANVAS)
.catch(error => createErrorMessage('Something went wrong.', error))
.then(() => {
if (localStorage.getItem('quickbarItemNames')) {
const quickbarItems = JSON.parse(localStorage.getItem('quickbarItemNames'))
EDITOR.setQuickbarItems(quickbarItems)
}
registerActions()
guiBPIndex = initSettingsPane({
bp,
book
}).guiBPIndex
})
.then(() => EDITOR.bpStringEncodeDecode.getBlueprintOrBookFromSource(bpSource))
.catch(error => createBPImportError(error))
.then(bpOrBook => loadBp(bpOrBook || new Blueprint()))
.then(() => createWelcomeMessage())
.catch(error => createBPImportError(error))
window.addEventListener('unload', () => {
localStorage.setItem('quickbarItemNames', JSON.stringify(EDITOR.getQuickbarItems()))
})
function loadBp(bpOrBook: Blueprint | Book): void {
if (bpOrBook instanceof Book) {
book = bpOrBook
bp = book.getBlueprint(bpIndex ? bpIndex : undefined)
EDITOR.loadBlueprint(bp)
guiBPIndex.max(book.lastBookIndex).setValue(book.activeIndex)
} else {
bp = bpOrBook
EDITOR.loadBlueprint(bpOrBook)
guiBPIndex.setValue(0).max(0)
}
loadingScreen.hide()
const bpIsEmpty = bpOrBook instanceof Blueprint && bpOrBook.isEmpty()
if (!bpIsEmpty) {
createToast({ text: 'Blueprint string loaded successfully', type: 'success' })
}
}
document.addEventListener('copy', (e: ClipboardEvent) => {
if (document.activeElement !== CANVAS) {
return
}
e.preventDefault()
if (bp.isEmpty()) {
return
}
const onSuccess = (): void => {
createToast({ text: 'Blueprint string copied to clipboard', type: 'success' })
}
const onError = (error: Error): void => {
createErrorMessage('Blueprint string could not be generated.', error)
}
const bpOrBook = book ? book : bp
if (navigator.clipboard && navigator.clipboard.writeText) {
EDITOR.bpStringEncodeDecode
.encode(bpOrBook)
.then(s => navigator.clipboard.writeText(s))
.then(onSuccess)
.catch(onError)
} else {
const data = EDITOR.bpStringEncodeDecode.encodeSync(bpOrBook)
if (data.value) {
e.clipboardData.setData('text/plain', data.value)
onSuccess()
} else {
onError(data.error)
}
}
})
document.addEventListener('paste', (e: ClipboardEvent) => {
if (document.activeElement !== CANVAS) {
return
}
e.preventDefault()
loadingScreen.show()
const promise =
navigator.clipboard && navigator.clipboard.readText
? navigator.clipboard.readText()
: Promise.resolve(e.clipboardData.getData('text'))
promise
.then(EDITOR.bpStringEncodeDecode.getBlueprintOrBookFromSource)
.then(loadBp)
.catch(error => {
loadingScreen.hide()
createBPImportError(error)
})
})
function registerActions(): void {
EDITOR.registerAction('clear', 'shift+n').bind({
press: () => {
loadBp(new Blueprint())
}
})
EDITOR.registerAction('generateOilOutpost', 'g').bind({
press: () => {
const errorMessage = bp.generatePipes()
if (errorMessage) {
createToast({ text: errorMessage, type: 'warning' })
}
}
})
EDITOR.registerAction('info', 'i').bind({
press: () => {
const infoPanel = document.getElementById('info-panel')
if (infoPanel.classList.contains('active')) {
infoPanel.classList.remove('active')
} else {
infoPanel.classList.add('active')
}
}
})
EDITOR.registerAction('takePicture', 'modifier+s').bind({
press: () => {
if (bp.isEmpty()) {
return
}
EDITOR.getPicture().then(blob => {
FileSaver.saveAs(blob, `${bp.name}.png`)
createToast({ text: 'Blueprint image successfully generated', type: 'success' })
})
}
})
EDITOR.importKeybinds(JSON.parse(localStorage.getItem('keybinds')))
window.addEventListener('unload', () => {
const keybinds = EDITOR.exportKeybinds()
if (Object.keys(keybinds).length) {
localStorage.setItem('keybinds', JSON.stringify(keybinds))
} else {
localStorage.removeItem('keybinds')
}
})
}
function createWelcomeMessage(): void {
const notFirstRun = localStorage.getItem('firstRun') === 'false'
if (notFirstRun) {
return
}
localStorage.setItem('firstRun', 'false')
// Wait a bit just to capture the users attention
// This way they will see the toast animation
setTimeout(() => {
createToast({
text:
'> To access the inventory and start building press E<br>' +
'> To import/export a blueprint string use ctrl/cmd + C/V<br>' +
'> For more info press I<br>' +
'> Also check out the settings area',
timeout: 30000
})
}, 1000)
}
function createErrorMessage(text: string, error: unknown): void {
console.error(error)
createToast({
text:
`${text}<br>` +
'Please check out the console (F12) for an error message and ' +
'report this bug on github or using the feedback button.',
type: 'error',
timeout: 10000
})
}
function createBPImportError(error: Error | TrainBlueprintError | ModdedBlueprintError): void {
if (error instanceof TrainBlueprintError) {
createErrorMessage(
'Blueprint with train entities not supported yet. If you think this is a mistake:',
error.errors
)
return
}
if (error instanceof ModdedBlueprintError) {
createErrorMessage(
'Blueprint with modded items not supported yet. If you think this is a mistake:',
error.errors
)
return
}
createErrorMessage('Blueprint string could not be loaded.', error)
}

View File

@ -1,14 +1,18 @@
import { GUI, GUIController } from 'dat.gui' import { GUI, GUIController } from 'dat.gui'
import FD from 'factorio-data' import FD from 'factorio-data'
import actions from './actions' import EDITOR, { Blueprint, Book } from '@fbe/editor'
import G from './common/globals'
import spritesheetsLoader from './spritesheetsLoader'
import { oilOutpostSettings } from './factorio-data/blueprint'
GUI.TEXT_CLOSED = 'Close Settings' GUI.TEXT_CLOSED = 'Close Settings'
GUI.TEXT_OPEN = 'Open Settings' GUI.TEXT_OPEN = 'Open Settings'
export default function initDatGui(): { const COLOR_DARK = 0x303030
const COLOR_LIGHT = 0xc9c9c9
const isDarkColor = (color: number): boolean => color === COLOR_DARK
export default function initSettingsPane(shared: {
bp: Blueprint
book: Book
}): {
guiBPIndex: GUIController guiBPIndex: GUIController
} { } {
const gui = new GUI({ const gui = new GUI({
@ -34,67 +38,33 @@ export default function initDatGui(): {
.add({ bpIndex: 0 }, 'bpIndex', 0, 0, 1) .add({ bpIndex: 0 }, 'bpIndex', 0, 0, 1)
.name('BP Index') .name('BP Index')
.onFinishChange((value: number) => { .onFinishChange((value: number) => {
if (G.book) { if (shared.book) {
G.bp = G.book.getBlueprint(value) shared.bp = shared.book.getBlueprint(value)
G.BPC.clearData() EDITOR.loadBlueprint(shared.bp)
G.BPC.initBP()
} }
}) })
if (localStorage.getItem('moveSpeed')) { if (localStorage.getItem('moveSpeed')) {
G.moveSpeed = Number(localStorage.getItem('moveSpeed')) const moveSpeed = Number(localStorage.getItem('moveSpeed'))
EDITOR.setMoveSpeed(moveSpeed)
} }
gui.add(G, 'moveSpeed', 5, 20) gui.add({ moveSpeed: EDITOR.getMoveSpeed() }, 'moveSpeed', 5, 20)
.name('Move Speed') .name('Move Speed')
.onChange((val: boolean) => localStorage.setItem('moveSpeed', val.toString())) .onChange((speed: number) => {
localStorage.setItem('moveSpeed', speed.toString())
if (localStorage.getItem('quickbarRows')) { EDITOR.setMoveSpeed(speed)
G.quickbarRows = Number(localStorage.getItem('quickbarRows'))
}
gui.add(G, 'quickbarRows', 1, 5, 1)
.name('Quickbar Rows')
.onChange((rows: number) => {
localStorage.setItem('quickbarRows', rows.toString())
G.UI.changeQuickbarRows(rows)
}) })
window.addEventListener('unload', () => { if (localStorage.getItem('debug')) {
localStorage.setItem('quickbarItemNames', JSON.stringify(G.UI.quickbarContainer.serialize())) const debug = Boolean(localStorage.getItem('debug'))
}) EDITOR.setDebugging(debug)
const entitiesQuality = {
'Low. Res PNG 8 (1.52 MB)': 0,
'High Res PNG 8 (5.52 MB)': 1,
'Low. Res PNG 32 (5.55 MB)': 2,
'High Res PNG 32 (18.20 MB)': 3
} }
const setQuality = (quality: number): void => { gui.add(
G.quality.hr = quality % 2 === 1 {
G.quality.compressed = quality < 2 debug: EDITOR.isDebuggingOn()
} },
'debug'
const gl = document.createElement('canvas').getContext('webgl') )
if (gl.getParameter(gl.MAX_TEXTURE_SIZE) < 8192) {
delete entitiesQuality['High Res PNG 8 (5.52 MB)']
delete entitiesQuality['High Res PNG 32 (18.20 MB)']
G.quality.hr = false
}
const quality = localStorage.getItem('quality')
? Number(localStorage.getItem('quality'))
: (G.quality.hr ? 1 : 0) + (G.quality.compressed ? 0 : 2)
setQuality(quality)
gui.add({ quality }, 'quality', entitiesQuality)
.name('Entities Quality')
.onChange((quality: number) => {
localStorage.setItem('quality', quality.toString())
setQuality(quality)
spritesheetsLoader.changeQuality(G.quality.hr, G.quality.compressed)
})
G.debug = Boolean(localStorage.getItem('debug'))
gui.add(G, 'debug')
.name('Debug') .name('Debug')
.onChange((debug: boolean) => { .onChange((debug: boolean) => {
if (debug) { if (debug) {
@ -102,46 +72,52 @@ export default function initDatGui(): {
} else { } else {
localStorage.removeItem('debug') localStorage.removeItem('debug')
} }
// TODO: find a nice way to do this EDITOR.setDebugging(debug)
G.bp.history.logging = debug
G.UI.showDebuggingLayer = debug
}) })
// Theme folder // Grid theme folder
const themeFolder = gui.addFolder('Theme') const themeFolder = gui.addFolder('Grid Theme')
if (localStorage.getItem('darkTheme')) { if (localStorage.getItem('darkTheme')) {
G.colors.darkTheme = localStorage.getItem('darkTheme') === 'true' const darkTheme = localStorage.getItem('darkTheme') === 'true'
EDITOR.setGridColor(darkTheme ? COLOR_DARK : COLOR_LIGHT)
} }
themeFolder themeFolder
.add(G.colors, 'darkTheme') .add({ darkTheme: isDarkColor(EDITOR.getGridColor()) }, 'darkTheme')
.name('Dark Mode') .name('Dark Mode')
.onChange((val: boolean) => localStorage.setItem('darkTheme', val.toString())) .onChange((darkTheme: boolean) => {
localStorage.setItem('darkTheme', darkTheme.toString())
EDITOR.setGridColor(darkTheme ? COLOR_DARK : COLOR_LIGHT)
})
if (localStorage.getItem('pattern')) { if (localStorage.getItem('pattern')) {
G.colors.pattern = localStorage.getItem('pattern') as 'checker' | 'grid' const pattern = localStorage.getItem('pattern') as 'checker' | 'grid'
EDITOR.setGridPattern(pattern)
} }
themeFolder themeFolder
.add(G.colors, 'pattern', ['checker', 'grid']) .add({ pattern: EDITOR.getGridPattern() }, 'pattern', ['checker', 'grid'])
.name('Pattern') .name('Pattern')
.onChange((val: 'checker' | 'grid') => { .onChange((pattern: 'checker' | 'grid') => {
G.BPC.generateGrid(val) localStorage.setItem('pattern', pattern)
localStorage.setItem('pattern', val) EDITOR.setGridPattern(pattern)
}) })
if (localStorage.getItem('oilOutpostSettings')) { if (localStorage.getItem('oilOutpostSettings')) {
const settings = JSON.parse(localStorage.getItem('oilOutpostSettings')) const settings = JSON.parse(localStorage.getItem('oilOutpostSettings'))
Object.keys(oilOutpostSettings).forEach(k => { EDITOR.setOilOutpostSettings(settings)
if (settings[k]) {
const S = oilOutpostSettings as Record<string, string | boolean | number>
S[k] = settings[k]
}
})
} }
window.addEventListener('unload', () => window.addEventListener('unload', () =>
localStorage.setItem('oilOutpostSettings', JSON.stringify(oilOutpostSettings)) localStorage.setItem('oilOutpostSettings', JSON.stringify(EDITOR.getOilOutpostSettings()))
) )
const oilOutpostSettings = new Proxy(EDITOR.getOilOutpostSettings(), {
set: (settings, key, value) => {
settings[key as string] = value
EDITOR.setOilOutpostSettings(settings)
return true
}
})
const oilOutpostFolder = gui.addFolder('Oil Outpost Generator Settings') const oilOutpostFolder = gui.addFolder('Oil Outpost Generator Settings')
oilOutpostFolder.add(oilOutpostSettings, 'DEBUG').name('Debug') oilOutpostFolder.add(oilOutpostSettings, 'DEBUG').name('Debug')
oilOutpostFolder.add(oilOutpostSettings, 'PUMPJACK_MODULE', getModulesObjFor('pumpjack')).name('Pumpjack Modules') oilOutpostFolder.add(oilOutpostSettings, 'PUMPJACK_MODULE', getModulesObjFor('pumpjack')).name('Pumpjack Modules')
@ -149,8 +125,14 @@ export default function initDatGui(): {
oilOutpostFolder.add(oilOutpostSettings, 'BEACONS').name('Beacons') oilOutpostFolder.add(oilOutpostSettings, 'BEACONS').name('Beacons')
oilOutpostFolder.add(oilOutpostSettings, 'MIN_AFFECTED_ENTITIES', 1, 12, 1).name('Min Affect. Pumpjacks') oilOutpostFolder.add(oilOutpostSettings, 'MIN_AFFECTED_ENTITIES', 1, 12, 1).name('Min Affect. Pumpjacks')
oilOutpostFolder.add(oilOutpostSettings, 'BEACON_MODULE', getModulesObjFor('beacon')).name('Beacon Modules') oilOutpostFolder.add(oilOutpostSettings, 'BEACON_MODULE', getModulesObjFor('beacon')).name('Beacon Modules')
oilOutpostFolder.add(actions.generateOilOutpost, 'call').name('Generate (g)') oilOutpostFolder
.add(
{
generate: () => EDITOR.callAction('generateOilOutpost')
},
'generate'
)
.name('Generate')
function getModulesObjFor(entityName: string): Record<string, string> { function getModulesObjFor(entityName: string): Record<string, string> {
return ( return (
Object.keys(FD.items) Object.keys(FD.items)
@ -177,11 +159,8 @@ export default function initDatGui(): {
// Keybinds folder // Keybinds folder
const keybindsFolder = gui.addFolder('Keybinds') const keybindsFolder = gui.addFolder('Keybinds')
actions.forEachAction((action, actionName) => { EDITOR.forEachAction(action => {
const name = actionName const name = action.prettyName
.split(/(?=[A-Z1-9])/)
.join(' ')
.replace(/(\b\w)/, c => c.toUpperCase())
if (name.includes('Quickbar')) { if (name.includes('Quickbar')) {
return return
} }
@ -193,11 +172,8 @@ export default function initDatGui(): {
const quickbarFolder = keybindsFolder.addFolder('Quickbar') const quickbarFolder = keybindsFolder.addFolder('Quickbar')
actions.forEachAction((action, actionName) => { EDITOR.forEachAction(action => {
const name = actionName const name = action.prettyName
.split(/(?=[A-Z1-9])/)
.join(' ')
.replace(/(\b\w)/, c => c.toUpperCase())
if (!name.includes('Quickbar')) { if (!name.includes('Quickbar')) {
return return
} }
@ -210,7 +186,7 @@ export default function initDatGui(): {
keybindsFolder keybindsFolder
.add( .add(
{ {
resetDefaults: () => actions.forEachAction(action => action.resetKeyCombo()) resetDefaults: () => EDITOR.resetKeybinds()
}, },
'resetDefaults' 'resetDefaults'
) )

View File

@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "dist",
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"lib": ["dom", "es6", "es2016.array.include"],
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
}
}

View File

@ -0,0 +1,164 @@
import path from 'path'
import webpack from 'webpack'
import { CleanWebpackPlugin } from 'clean-webpack-plugin'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
import TerserJSPlugin from 'terser-webpack-plugin'
import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin'
import WebpackDevServer from 'webpack-dev-server'
const config = (_: unknown, { mode }: { mode: 'production' | 'development' }): webpack.Configuration => {
const DEV = mode !== 'production'
const sourceMapType = DEV ? 'inline-source-map' : 'source-map'
const sourceMap = !!sourceMapType
// hmr only works for css in the current config
const hmr = true
return {
target: 'web',
mode: DEV ? 'development' : 'production',
devtool: sourceMapType,
devServer: {
contentBase: './dist',
hot: hmr,
before(_, server) {
devServer = server
}
},
stats: DEV ? 'minimal' : 'normal',
entry: ['./src/index.ts', './src/index.styl'],
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: true,
experimentalWatchApi: true
}
}
},
{
test: /\.styl$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
hmr: DEV,
sourceMap
}
},
{
loader: 'css-loader',
options: {
sourceMap
}
},
{
loader: 'postcss-loader',
options: {
sourceMap
}
},
{
loader: 'stylus-loader',
options: {
sourceMap
}
}
]
},
{
test: /\.(jpg|png|svg)$/,
use: {
loader: 'file-loader',
options: {
name: DEV ? '[name].[ext]' : '[name].[contenthash:10].[ext]'
}
}
},
// {
// test: /\.svg$/,
// loader: 'svg-inline-loader'
// }
{
test: /\.html$/,
use: {
loader: 'html-loader',
options: {
minimize: !DEV,
attrs: ['img:src', 'link:href']
}
}
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/index.html'
}),
reloadHtml,
new MiniCssExtractPlugin({
filename: DEV ? '[name].css' : '[name].[contenthash:10].css'
})
],
optimization: {
moduleIds: 'hashed',
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
cacheGroups: {
factorio_data: {
priority: 1,
test: /[\\/]node_modules[\\/]factorio-data[\\/]/,
name: 'factorio_data'
},
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor'
}
}
},
minimizer: [
new TerserJSPlugin({ sourceMap }),
new OptimizeCSSAssetsPlugin({ cssProcessorOptions: { map: sourceMap } })
]
},
resolve: {
extensions: ['.js', '.ts', '.styl', '.html']
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: DEV ? '[name].js' : '[name].[contenthash:10].js'
}
}
}
/*
Reload devServer when HTML changes (this doesn't happen automatically because we use HtmlWebpackPlugin)
From https://github.com/jantimon/html-webpack-plugin/issues/100#issuecomment-368303060
*/
let devServer: WebpackDevServer
function reloadHtml(): void {
const cache: Record<string, string> = {}
const plugin = { name: 'CustomHtmlReloadPlugin' }
// @ts-ignore
this.hooks.compilation.tap(plugin, compilation => {
// @ts-ignore
compilation.hooks.htmlWebpackPluginAfterEmit.tap(plugin, data => {
const orig = cache[data.outputName]
const html = data.html.source()
// plugin seems to emit on any unrelated change?
if (orig && orig !== html) {
// @ts-ignore
devServer.sockWrite(devServer.sockets, 'content-changed')
}
cache[data.outputName] = html
})
})
}
export default config

View File

@ -1,17 +1,15 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["dom", "es6", "dom.iterable", "es2016.array.include", "es2017.object"], "pretty": true,
"target": "es6",
"module": "es6", "sourceMap": true,
"moduleResolution": "node", "alwaysStrict": true,
"allowSyntheticDefaultImports": true, "removeComments": true,
"noImplicitAny": true, "noImplicitAny": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"removeComments": true,
"preserveConstEnums": true, "preserveConstEnums": true
"sourceMap": true,
"skipLibCheck": true,
"alwaysStrict": true
} }
} }

4986
yarn.lock

File diff suppressed because it is too large Load Diff