1
0
mirror of https://github.com/teoxoy/factorio-blueprint-editor.git synced 2025-11-23 22:15:01 +02:00

first commit

This commit is contained in:
Ralimist
2018-03-29 07:54:56 +02:00
commit 8206d24c7e
57 changed files with 106850 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
node_modules
.wwp-cache
dist
*.log
parser/temp
parser/temp.*

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Tanasoaia Teodor Andrei
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

26
README.md Normal file
View File

@@ -0,0 +1,26 @@
# factorio-blueprint-editor
A [Factorio](https://www.factorio.com/) blueprint editor and renderer webapp
# TODO:
- ctrl + click to add modules
- implement more entity settings (filters, conditions)
- edit bp label and icons
- put entityInfo icon backgrounds on another layer
- pipe window
- implement the other cursorBoxes
- overlay for turrets
- show electricity-icon-unplugged for entities that are not connected to a power pole
- bp manager (manage bps and books in an editor + placement of new blueprint in an allready loaded bp)
- show bp inputs (show icons for belts)
- throughput calculator/bp analyzer/bottleneck detector
- highlight lone underground pipes/belts
- train-stop station name
- tiles support
- poles range, wires and rotations
- rotate bp
- implement circuit_wire_max_distance with visualization ((x - center_x)^2 + (y - center_y)^2 <= radius^2)
- rail endings
- rail custom bounding box
- rail rotations
- belt endings

14985
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

53
package.json Normal file
View File

@@ -0,0 +1,53 @@
{
"name": "factorio-blueprint-editor",
"version": "0.1.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server --config webpack.dev.js --host 0.0.0.0 --open --useLocalIp",
"build": "webpack --config webpack.prod.js"
},
"author": "Teoxoy",
"license": "MIT",
"browserslist": [
"> 5%",
"last 3 versions"
],
"dependencies": {
"@pixi/filter-adjustment": "^2.5.0",
"ajv": "^6.4.0",
"gown": "^0.1.6",
"immutable": "^3.8.2",
"keyboardjs": "^2.4.1",
"normalize.css": "^8.0.0",
"pixi.js": "^4.7.1"
},
"devDependencies": {
"@babel/core": "^7.0.0-beta.42",
"@babel/preset-env": "^7.0.0-beta.42",
"@types/keyboardjs": "^2.2.31",
"@types/pixi.js": "^4.7.2",
"babel-loader": "^8.0.0-beta.2",
"clean-webpack-plugin": "^0.1.19",
"copy-webpack-plugin": "^4.5.1",
"css-loader": "^0.28.11",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"fork-ts-checker-webpack-plugin": "^0.4.1",
"fs-extra": "^5.0.0",
"html-webpack-plugin": "3.1.0",
"jimp": "^0.2.28",
"node-sprite-generator": "^0.10.2",
"optimize-css-assets-webpack-plugin": "^4.0.0",
"ts-loader": "^4.1.0",
"tslint": "^5.9.1",
"typescript": "^2.8.1",
"webapp-webpack-plugin": "^1.3.1",
"webpack": "^4.3.0",
"webpack-bundle-analyzer": "^2.11.1",
"webpack-cli": "^2.0.13",
"webpack-closure-compiler": "^2.1.6",
"webpack-dev-server": "^3.1.1",
"webpack-merge": "^4.1.2",
"webpack-visualizer-plugin": "^0.1.11"
}
}

598
parser/defines.lua Normal file
View File

@@ -0,0 +1,598 @@
defines = {
alert_type = {
custom = 6,
entity_destroyed = 0,
entity_under_attack = 1,
no_material_for_construction = 3,
no_storage = 7,
not_enough_construction_robots = 2,
not_enough_repair_packs = 4,
turret_fire = 5
},
chain_signal_state = {
all_open = 1,
none = 0,
none_open = 3,
partially_open = 2
},
chunk_generated_status = {
basic_tiles = 20,
corrected_tiles = 30,
custom_tiles = 10,
entities = 50,
nothing = 0,
tiles = 40
},
circuit_condition_index = {
arithmetic_combinator = 1,
constant_combinator = 1,
decider_combinator = 1,
inserter_circuit = 1,
inserter_logistic = 2,
lamp = 1,
offshore_pump = 1,
pump = 1
},
circuit_connector_id = {
accumulator = 1,
combinator_input = 1,
combinator_output = 2,
constant_combinator = 1,
container = 1,
electric_pole = 1,
inserter = 1,
lamp = 1,
offshore_pump = 1,
programmable_speaker = 1,
pump = 1,
rail_chain_signal = 1,
rail_signal = 1,
roboport = 1,
storage_tank = 1,
wall = 1
},
command = {
attack = 1,
attack_area = 5,
build_base = 7,
compound = 3,
flee = 8,
go_to_location = 2,
group = 4,
wander = 6
},
compound_command = {
logical_and = 0,
logical_or = 1,
return_last = 2
},
control_behavior = {
inserter = {
circuit_mode_of_operation = {
enable_disable = 0,
none = 3,
read_hand_contents = 2,
set_filters = 1,
set_stack_size = 4
},
hand_read_mode = {
hold = 1,
pulse = 0
}
},
lamp = {
circuit_mode_of_operation = {
use_colors = 0
}
},
logistic_container = {
circuit_mode_of_operation = {
send_contents = 0,
set_requests = 1
}
},
mining_drill = {
resource_read_mode = {
entire_patch = 1,
this_miner = 0
}
},
roboport = {
circuit_mode_of_operation = {
read_logistics = 0,
read_robot_stats = 1
}
},
train_stop = {
circuit_mode_of_operation = {
enable_disable = 0,
read_from_train = 2,
read_stopped_train = 3,
send_to_train = 1
}
},
transport_belt = {
content_read_mode = {
hold = 1,
pulse = 0
}
},
type = {
accumulator = 13,
arithmetic_combinator = 10,
constant_combinator = 11,
container = 1,
decider_combinator = 9,
generic_on_off = 2,
inserter = 3,
lamp = 4,
logistic_container = 5,
mining_drill = 16,
programmable_speaker = 17,
rail_chain_signal = 18,
rail_signal = 14,
roboport = 6,
storage_tank = 7,
train_stop = 8,
transport_belt = 12,
wall = 15
}
},
controllers = {
character = 1,
ghost = 0,
god = 2
},
deconstruction_item = {
entity_filter_mode = {
blacklist = 1,
whitelist = 0
},
tile_filter_mode = {
blacklist = 1,
whitelist = 0
},
tile_selection_mode = {
always = 1,
never = 2,
normal = 0,
only = 3
}
},
difficulty = {
easy = 0,
hard = 2,
normal = 1
},
difficulty_settings = {
recipe_difficulty = {
expensive = 1,
normal = 0
},
technology_difficulty = {
expensive = 1,
normal = 0
}
},
direction = {
east = 2,
north = 0,
northeast = 1,
northwest = 7,
south = 4,
southeast = 3,
southwest = 5,
west = 6
},
distraction = {
by_anything = 3,
by_damage = 4,
by_enemy = 1,
none = 0
},
events = {
on_biter_base_built = 55,
on_built_entity = 6,
on_canceled_deconstruction = 21,
on_character_corpse_expired = 90,
on_chunk_charted = 98,
on_chunk_generated = 12,
on_combat_robot_expired = 80,
on_console_chat = 71,
on_console_command = 72,
on_difficulty_settings_changed = 60,
on_entity_damaged = 97,
on_entity_died = 4,
on_entity_renamed = 57,
on_entity_settings_pasted = 31,
on_force_created = 27,
on_forces_merging = 28,
on_gui_checked_state_changed = 3,
on_gui_click = 1,
on_gui_closed = 84,
on_gui_elem_changed = 67,
on_gui_opened = 83,
on_gui_selection_state_changed = 58,
on_gui_text_changed = 2,
on_gui_value_changed = 85,
on_marked_for_deconstruction = 20,
on_market_item_purchased = 53,
on_mod_item_opened = 82,
on_picked_up_item = 5,
on_player_alt_selected_area = 50,
on_player_ammo_inventory_changed = 36,
on_player_armor_inventory_changed = 35,
on_player_built_tile = 45,
on_player_cancelled_crafting = 96,
on_player_changed_force = 56,
on_player_changed_position = 81,
on_player_changed_surface = 51,
on_player_cheat_mode_disabled = 89,
on_player_cheat_mode_enabled = 88,
on_player_configured_blueprint = 70,
on_player_crafted_item = 13,
on_player_created = 24,
on_player_cursor_stack_changed = 29,
on_player_deconstructed_area = 69,
on_player_demoted = 76,
on_player_died = 41,
on_player_display_resolution_changed = 93,
on_player_display_scale_changed = 94,
on_player_driving_changed_state = 26,
on_player_dropped_item = 54,
on_player_gun_inventory_changed = 37,
on_player_joined_game = 43,
on_player_left_game = 44,
on_player_main_inventory_changed = 32,
on_player_mined_entity = 65,
on_player_mined_item = 8,
on_player_mined_tile = 46,
on_player_muted = 86,
on_player_pipette = 92,
on_player_placed_equipment = 38,
on_player_promoted = 75,
on_player_quickbar_inventory_changed = 33,
on_player_removed = 73,
on_player_removed_equipment = 39,
on_player_respawned = 42,
on_player_rotated_entity = 19,
on_player_selected_area = 49,
on_player_setup_blueprint = 68,
on_player_tool_inventory_changed = 34,
on_player_unmuted = 87,
on_player_used_capsule = 74,
on_pre_entity_settings_pasted = 30,
on_pre_ghost_deconstructed = 91,
on_pre_player_crafted_item = 95,
on_pre_player_died = 40,
on_pre_player_mined_item = 11,
on_pre_surface_deleted = 63,
on_put_item = 9,
on_research_finished = 18,
on_research_started = 17,
on_resource_depleted = 25,
on_robot_built_entity = 14,
on_robot_built_tile = 47,
on_robot_mined = 16,
on_robot_mined_entity = 64,
on_robot_mined_tile = 48,
on_robot_pre_mined = 15,
on_rocket_launched = 10,
on_runtime_mod_setting_changed = 59,
on_sector_scanned = 7,
on_selected_entity_changed = 52,
on_surface_created = 61,
on_surface_deleted = 62,
on_tick = 0,
on_train_changed_state = 23,
on_train_created = 66,
on_trigger_created_entity = 22,
script_raised_built = 77,
script_raised_destroy = 78,
script_raised_revive = 79
},
group_state = {
attacking_distraction = 2,
attacking_target = 3,
finished = 4,
gathering = 0,
moving = 1
},
gui_type = {
achievement = 8,
blueprint_library = 9,
bonus = 6,
controller = 3,
custom = 16,
entity = 1,
equipment = 10,
item = 5,
kills = 13,
logistic = 11,
none = 0,
other_player = 12,
permissions = 14,
production = 4,
research = 2,
trains = 7,
tutorials = 15
},
input_action = {
add_permission_group = 177,
alt_select_area = 129,
alt_select_blueprint_entities = 94,
begin_mining = 2,
begin_mining_terrain = 46,
build_item = 44,
build_rail = 125,
build_terrain = 120,
cancel_craft = 64,
cancel_deconstruct = 114,
cancel_new_blueprint = 22,
cancel_research = 126,
change_active_item_group_for_crafting = 78,
change_active_item_group_for_filters = 89,
change_active_quick_bar = 17,
change_arithmetic_combinator_parameters = 115,
change_blueprint_book_record_label = 109,
change_decider_combinator_parameters = 116,
change_item_label = 124,
change_picking_state = 150,
change_programmable_speaker_alert_parameters = 118,
change_programmable_speaker_circuit_parameters = 119,
change_programmable_speaker_parameters = 117,
change_riding_state = 47,
change_shooting_state = 58,
change_single_blueprint_record_label = 100,
change_train_stop_station = 77,
change_train_wait_condition = 121,
change_train_wait_condition_data = 122,
clean_cursor_stack = 13,
clear_selected_blueprint = 131,
clear_selected_deconstruction_item = 132,
connect_rolling_stock = 10,
copy_entity_settings = 24,
craft = 56,
create_blueprint_like = 104,
cursor_split = 52,
cursor_transfer = 51,
custom_input = 123,
cycle_blueprint_book_backwards = 36,
cycle_blueprint_book_forwards = 35,
deconstruct = 92,
delete_blueprint_record = 103,
delete_custom_tag = 175,
delete_permission_group = 176,
destroy_opened_item = 26,
disconnect_rolling_stock = 11,
drag_train_schedule = 142,
drag_train_wait_condition = 143,
drop_blueprint_record = 102,
drop_item = 43,
drop_to_blueprint_book = 174,
edit_custom_tag = 137,
edit_permission_group = 138,
edit_train_schedule = 76,
export_blueprint = 111,
fast_entity_split = 164,
fast_entity_transfer = 163,
grab_blueprint_record = 101,
gui_checked_state_changed = 80,
gui_click = 73,
gui_elem_changed = 140,
gui_selection_state_changed = 81,
gui_text_changed = 79,
gui_value_changed = 82,
import_blueprint = 112,
import_blueprint_string = 139,
inventory_split = 63,
inventory_transfer = 54,
launch_rocket = 16,
market_offer = 75,
mod_settings_changed = 135,
open_achievements_gui = 33,
open_blueprint_library_gui = 18,
open_blueprint_record = 98,
open_bonus_gui = 31,
open_character_gui = 9,
open_equipment = 50,
open_gui = 7,
open_item = 48,
open_kills_gui = 20,
open_logistic_gui = 41,
open_mod_item = 49,
open_production_gui = 19,
open_technology_gui = 15,
open_train_gui = 170,
open_train_station_gui = 127,
open_trains_gui = 32,
open_tutorials_gui = 34,
paste_entity_settings = 25,
place_equipment = 83,
remove_cables = 110,
reset_assembling_machine = 14,
reverse_rotate_entity = 5,
rotate_entity = 4,
select_area = 128,
select_blueprint_entities = 93,
select_entity_slot = 145,
select_gun = 156,
select_item = 144,
select_tile_slot = 146,
set_auto_launch_rocket = 158,
set_autosort_inventory = 157,
set_behavior_mode = 162,
set_car_weapons_control = 179,
set_circuit_condition = 67,
set_circuit_mode_of_operation = 72,
set_deconstruction_item_tile_selection_mode = 173,
set_deconstruction_item_trees_and_rocks_only = 172,
set_entity_color = 171,
set_entity_energy_property = 136,
set_filter = 65,
set_infinity_container_filter_item = 134,
set_infinity_container_remove_unfiltered_items = 178,
set_inserter_max_stack_size = 169,
set_inventory_bar = 88,
set_logistic_filter_item = 70,
set_logistic_filter_signal = 71,
set_logistic_trash_filter_item = 133,
set_request_from_buffers = 180,
set_research_finished_stops_game = 168,
set_signal = 68,
set_single_blueprint_record_icon = 97,
set_splitter_priority = 149,
set_train_stopped = 165,
setup_assembling_machine = 59,
setup_blueprint = 95,
setup_single_blueprint_record = 96,
shortcut_quick_bar_transfer = 155,
smart_pipette = 61,
stack_split = 62,
stack_transfer = 53,
start_repair = 91,
start_research = 69,
start_walking = 45,
switch_connect_to_logistic_network = 161,
switch_constant_combinator_state = 159,
switch_power_switch_state = 160,
switch_to_rename_stop_gui = 30,
take_equipment = 84,
toggle_deconstruction_item_entity_filter_mode = 39,
toggle_deconstruction_item_tile_filter_mode = 40,
toggle_driving = 6,
toggle_enable_vehicle_logistics_while_moving = 38,
toggle_show_entity_info = 27,
use_ability = 85,
use_artillery_remote = 87,
use_item = 86,
wire_dragging = 57,
write_to_console = 74
},
inventory = {
assembling_machine_input = 2,
assembling_machine_modules = 4,
assembling_machine_output = 3,
beacon_modules = 1,
burnt_result = 6,
car_ammo = 3,
car_trunk = 2,
cargo_wagon = 1,
chest = 1,
fuel = 1,
furnace_modules = 4,
furnace_result = 3,
furnace_source = 2,
god_main = 2,
god_quickbar = 1,
item_main = 1,
lab_input = 2,
lab_modules = 3,
mining_drill_modules = 2,
player_ammo = 4,
player_armor = 5,
player_guns = 3,
player_main = 1,
player_quickbar = 2,
player_tools = 6,
player_trash = 8,
player_vehicle = 7,
roboport_material = 2,
roboport_robot = 1,
robot_cargo = 1,
robot_repair = 2,
rocket_silo_result = 6,
rocket_silo_rocket = 5,
turret_ammo = 1
},
logistic_member_index = {
character_provider = 2,
character_requester = 0,
character_storage = 1,
generic_on_off_behavior = 0,
logistic_container = 0,
vehicle_storage = 1
},
logistic_mode = {
active_provider = 1,
buffer = 5,
none = 0,
passive_provider = 4,
requester = 3,
storage = 2
},
mouse_button_type = {
left = 2,
middle = 8,
none = 1,
right = 4
},
rail_connection_direction = {
left = 0,
none = 3,
right = 2,
straight = 1
},
rail_direction = {
back = 1,
front = 0
},
riding = {
acceleration = {
accelerating = 1,
braking = 2,
nothing = 0,
reversing = 3
},
direction = {
left = 0,
right = 2,
straight = 1
}
},
shooting = {
not_shooting = 0,
shooting_enemies = 1,
shooting_selected = 2
},
signal_state = {
closed = 1,
open = 0,
reserved = 2,
reserved_by_circuit_network = 3
},
train_state = {
arrive_signal = 4,
arrive_station = 6,
manual_control = 9,
manual_control_stop = 8,
no_path = 3,
no_schedule = 2,
on_the_path = 0,
path_lost = 1,
wait_signal = 5,
wait_station = 7
},
transport_line = {
left_line = 1,
left_split_line = 5,
left_underground_line = 3,
right_line = 2,
right_split_line = 6,
right_underground_line = 4,
secondary_left_line = 3,
secondary_left_split_line = 7,
secondary_right_line = 4,
secondary_right_split_line = 8
},
wire_connection_id = {
electric_pole = 0,
power_switch_left = 0,
power_switch_right = 1
},
wire_type = {
copper = 1,
green = 3,
red = 2
}
}

109
parser/exportRawData.js Normal file
View File

@@ -0,0 +1,109 @@
const fse = require('fs-extra')
const lua_parser = require('./luajs/lua_parser_umd').parser
const execSync = require('child_process').execSync
//const factorioDirectory = 'C:/SteamLibrary/steamapps/common/Factorio/data/'
const factorioDirectory = 'C:/_Programs/Steam/steamapps/common/Factorio/data/'
//run /c game.write_file("defines.lua", serpent.block(_G.defines, {comments=false}))
//_tree_data_320 -> _tree_data_1
// Load Order:
// data.lua
// data-updates.lua
// data-final-fixes.lua
const reqLualibRegex = /.*?require\s*\(*['"]([^.]+?)['"]\)*/g
const reqRegex = /require\s*\(*['"](.+?)['"]\)*/g
let loadedModules = []
function searchLoadRemoveDependencies(contents, regex, baseFolder) {
let newModules = []
let match = regex.exec(contents)
while (match !== null) {
let dep = match[1]
if (!loadedModules.includes(dep)) {
//load module
loadedModules.push(dep)
newModules.push({
index: match.index,
name: dep
})
}
match = regex.exec(contents)
}
let offset = 0
for (let i = 0; i < newModules.length; i++) {
let startPart = contents.slice(0, newModules[i].index + offset)
let endPart = contents.slice(newModules[i].index + offset)
let depData = readRequireOfFile(baseFolder, newModules[i].name.replace(/\./g, '/') + '.lua') + '\n'
contents = startPart + depData + endPart
offset += depData.length
}
// remove all requires
contents = contents.replace(regex, '')
return contents
}
function readRequireOfFile(baseFolder, pathCon) {
let contents = fse.readFileSync(factorioDirectory + baseFolder + pathCon).toString()
contents = searchLoadRemoveDependencies(contents, reqLualibRegex, 'core/lualib/')
contents = searchLoadRemoveDependencies(contents, reqRegex, baseFolder)
// remove last return
contents = contents.replace(/return\s*\b.+?\b\s*$/g, '')
// if a return is an obj, convert the return with the filename
contents = contents.replace(/return\s(\{(.|\n)+?\})\s*$/g, function(match, capture){
let split = pathCon.split('/')
return split[split.length - 1].replace('.lua', '') + ' = ' + capture
})
if (pathCon.includes('autoplace_utils')) {
contents = contents.replace(/M/g, 'autoplace_utils')
}
return contents
}
const fileOrder = [
'core/lualib/dataloader.lua',
'core/data.lua',
'base/data.lua',
'base/data-updates.lua'
]
let mainFileData = ''
for (let i = 0; i < fileOrder.length; i++) {
let splitPath = fileOrder[i].split('/')
let data = readRequireOfFile(splitPath[0] + '/', splitPath.slice(1).join('/'))
mainFileData += data + '\n'
}
mainFileData = mainFileData
// var = require(...) results in var = var = {}
.replace(/\b[a-zA-Z_-]+?\b\s*(=\s*\b[a-zA-Z_-]+?\b\s*)=\s*\{/g, function(match, capture){
return match.replace(capture, '')
})
mainFileData = fse.readFileSync('./defines.lua').toString() + mainFileData
fse.writeFileSync('./temp.lua', mainFileData)
let parsedData = lua_parser.parse(mainFileData).replace(/_tree_data_320/g, '_tree_data_1')
let script = "var fs = require('fs');\n" +
fse.readFileSync('./luajs/lua.js').toString() + "\n" +
"var lua_script = (function() {\n" +
" " + parsedData.split("\n").join("\n ") + "\n" +
"})()[0];\n" +
"fs.writeFileSync('./temp.json', JSON.stringify(lua_tabletoJson(lua_tableget(lua_tableget(lua_script, 'data'), 'raw')), null, 2))"
fse.writeFileSync('./temp.js', script)
execSync('node temp.js')

11
parser/json-entity.tpl Normal file
View File

@@ -0,0 +1,11 @@
{
"frames": {
<% layout.images.forEach(function (image, idx)
{ %>"<%= image.className %>": {
"frame": { "x": <%= image.x %>, "y": <%= image.y %>, "w": <%= image.width %>, "h": <%= image.height %> },
"sourceSize": { "w": <%= image.width %>, "h": <%= image.height %> }
}<% if (idx !== layout.images.length - 1) { %>,<% } %>
<% }); %>
},
"meta": { "image": "entitySprites.png" }
}

11
parser/json-icon.tpl Normal file
View File

@@ -0,0 +1,11 @@
{
"frames": {
<% layout.images.forEach(function (image, idx)
{ %>"<%= image.className %>": {
"frame": { "x": <%= image.x %>, "y": <%= image.y %>, "w": <%= image.width %>, "h": <%= image.height %> },
"sourceSize": { "w": <%= image.width %>, "h": <%= image.height %> }
}<% if (idx !== layout.images.length - 1) { %>,<% } %>
<% }); %>
},
"meta": { "image": "iconSprites.png" }
}

1800
parser/luajs/lua.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

466
parser/processRawData.js Normal file
View File

@@ -0,0 +1,466 @@
const fse = require('fs-extra')
const nsg = require('node-sprite-generator')
const Jimp = require('jimp')
const util = require('util')
//const factorioDirectory = 'C:/SteamLibrary/steamapps/common/Factorio/data/'
const factorioDirectory = 'C:/_Programs/Steam/steamapps/common/Factorio/data/'
const outDir = '../src/bundles/'
const spritesheetsOutDir = '../src/spritesheets/'
function nameMapping(imagePath) {
const sP = imagePath.split('/')
return sP.splice(sP.length - 2).join('/').split('.')[0]
}
let rawData = JSON.parse(fse.readFileSync('./temp.json').toString()
.replace(/"(up|down|left|right|north|south|west|east)"/g, function(match, capture) {
if (capture === 'north' || capture === 'up') return '"0"'
if (capture === 'east' || capture === 'left') return '"2"'
if (capture === 'south' || capture === 'down') return '"4"'
if (capture === 'west' || capture === 'right') return '"6"'
}))
let tiles = {}
for (const k in rawData.tile) {
if (rawData.tile[k].minable) tiles[k] = rawData.tile[k]
}
console.log('Tiles: ' + Object.keys(tiles).length)
fse.writeFileSync(outDir + 'tileBundle.json', JSON.stringify(tiles, null, 2).replace(/__base__|__core__/g, 'factorio-data'))
console.log('Recipes: ' + Object.keys(rawData.recipe).length)
fse.writeFileSync(outDir + 'recipeBundle.json', JSON.stringify(rawData.recipe, null, 2).replace(/__base__|__core__/g, 'factorio-data'))
let inventory = []
let items = {}
let placeableEntities = ['curved-rail']
const blacklistedGroups = [
'environment',
'enemies',
'other'
]
for (const k in rawData['item-group']) {
const group = rawData['item-group'][k]
if (!blacklistedGroups.includes(group.name)) {
group.subgroups = []
inventory.push(group)
}
}
for (const k in rawData['item-subgroup']) {
const subgroup = rawData['item-subgroup'][k]
subgroup.items = []
for (const group of inventory) {
if (group.name === subgroup.group) {
group.subgroups.push(subgroup)
break
}
}
}
function findAllItems(data) {
if (data.constructor === Object) {
if (data.hasOwnProperty('subgroup')) {
addItem(data)
} else {
for (const k in data) {
if (data.hasOwnProperty(k)) {
findAllItems(data[k])
}
}
}
}
}
findAllItems(rawData)
for (const k in rawData['fluid']) {
const fluid = rawData['fluid'][k]
fluid.subgroup = 'fluid'
addItem(fluid)
}
function addItem(item) {
if ((item.flags && item.flags.includes('hidden')) || !(item.icon || item.icons) || !item.order || item.collision_box) return
for (let j = 0; j < inventory.length; j++) {
for (let k = 0; k < inventory[j].subgroups.length; k++) {
if (inventory[j].subgroups[k].name === item.subgroup) {
inventory[j].subgroups[k].items.push(item)
if (item.place_result) placeableEntities.push(item.place_result)
items[item.name] = item
return
}
}
}
}
console.log('Items: ' + Object.keys(items).length)
fse.writeFileSync(outDir + 'itemBundle.json', JSON.stringify(items, null, 2).replace(/"((__base__|__core__)\/.+?)"/g, function(match, capture) {
return '"icon:' + nameMapping(capture) + '"'
}))
// sort and remove extra info from inventoryBundle
inventory.sort(sortByOrder)
for (let i = 0; i < inventory.length; i++) {
inventory[i].subgroups.sort(sortByOrder)
for (let j = 0; j < inventory[i].subgroups.length; j++) {
inventory[i].subgroups[j].items.sort(sortByOrder)
for (let k = 0; k < inventory[i].subgroups[j].items.length; k++) {
removeExtraInfo(inventory[i].subgroups[j].items[k])
}
removeExtraInfo(inventory[i].subgroups[j])
}
removeExtraInfo(inventory[i])
}
function sortByOrder(a, b) {
// https://forums.factorio.com/viewtopic.php?f=25&t=3236#p23818
// https://forums.factorio.com/viewtopic.php?f=25&t=24163#p152955
if (a.order < b.order) return -1
if (a.order > b.order) return 1
return 0
}
function removeExtraInfo(obj) {
for (const k of Object.keys(obj)) {
if (!['subgroups', 'items', 'name', 'icon', 'icons'].includes(k)) delete obj[k]
}
}
fse.writeFileSync(outDir + 'inventoryBundle.json', JSON.stringify(inventory, null, 2).replace(/"((__base__|__core__)\/.+?)"/g, function(match, capture) {
return '"icon:' + nameMapping(capture) + '"'
}))
let paths = []
for (let i = 0, l = inventory.length; i < l; i++) {
paths.push(factorioDirectory + inventory[i].icon.replace(/__base__/g, 'base').replace(/__core__/g, 'core'))
for (let j = 0, l2 = inventory[i].subgroups.length; j < l2; j++) {
for (let k = 0, l3 = inventory[i].subgroups[j].items.length; k < l3; k++) {
const item = inventory[i].subgroups[j].items[k]
if (item.icon) {
paths.push(factorioDirectory + item.icon.replace(/__base__/g, 'base').replace(/__core__/g, 'core'))
} else {
for (let l = 0; l < item.icons.length; l++) {
paths.push(factorioDirectory + item.icons[l].icon.replace(/__base__/g, 'base').replace(/__core__/g, 'core'))
}
}
}
}
}
paths = Array.from(new Set(paths).values())
console.log('Icon sprites: ' + paths.length)
nsg({
src: paths,
spritePath: spritesheetsOutDir + 'iconSpritesheet.png',
stylesheet: './json-icon.tpl',
stylesheetPath: spritesheetsOutDir + 'iconSpritesheet.json',
stylesheetOptions: {
prefix: 'icon:',
nameMapping: nameMapping
},
compositor: 'jimp',
layout: 'packed',
layoutOptions: {
padding: 2
}
}, function(err) {
if (err)
console.log(err)
else
console.log('Icon sprite atlas generated!')
})
let entities = {}
function findAllEntities(data) {
if (data.constructor === Object) {
if (placeableEntities.includes(data.name) && data.hasOwnProperty('collision_box') && (!data.flags.includes('placeable-off-grid') || data.name === 'land-mine')) {
entities[data.name] = data
} else {
for (let k in data) {
if (data.hasOwnProperty(k)) {
findAllEntities(data[k])
}
}
}
}
}
findAllEntities(rawData)
const regexNameMatches = [
'combinator',
'underground-belt',
'transport-belt',
'splitter',
'inserter',
'turret',
'mining-drill',
'pump'
]
let nameMatches = [
'assembling-machine-2',
'assembling-machine-3',
'pipe-to-ground',
'oil-refinery',
'chemical-plant',
'heat-exchanger',
'boiler',
'train-stop'
]
for (let k in entities) {
// Size
const box = entities[k].selection_box
entities[k].size = {
width: Math.ceil(Math.abs(box[0][0]) + Math.abs(box[1][0])),
height: Math.ceil(Math.abs(box[0][1]) + Math.abs(box[1][1]))
}
// Move out splitters and underground-belts from transport-belt fast_replaceable_group
if (k.search('splitter') !== -1) {
entities[k].fast_replaceable_group = 'splitter'
}
if (k.search('underground-belt') !== -1) {
entities[k].fast_replaceable_group = 'underground-belt'
}
// Possible Rotations
for (let j = 0; j < regexNameMatches.length; j++) {
if (k.includes(regexNameMatches[j])) {
nameMatches.push(k)
}
}
}
// Actual land size of the offshore pump
entities['offshore-pump'].size = { width: 1, height: 1 }
for (let i = 0; i < nameMatches.length; i++) {
entities[nameMatches[i]].possible_rotations = [0, 2, 4, 6]
}
entities['storage-tank'].possible_rotations = [0, 2]
entities['gate'].possible_rotations = [0, 2]
entities['steam-engine'].possible_rotations = [0, 2]
entities['steam-turbine'].possible_rotations = [0, 2]
entities['straight-rail'].possible_rotations = [0, 2]
entities['rail-signal'].possible_rotations = [0, 1, 2, 3, 4, 5, 6, 7]
entities['rail-chain-signal'].possible_rotations = [0, 1, 2, 3, 4, 5, 6, 7]
// End Possible Rotations
// switch dir 2 and 6 for pipe-to-ground
let dir2 = Object.assign({}, entities['pipe-to-ground'].pictures['2'])
entities['pipe-to-ground'].pictures['2'] = entities['pipe-to-ground'].pictures['6']
entities['pipe-to-ground'].pictures['6'] = dir2
// shift.y-1 for dir 4 wall patch of gate
let wp4 = entities['gate'].wall_patch['4'].layers[0]
wp4.shift = [wp4.shift[0], wp4.shift[1] - 1]
if (wp4.hr_version) {
wp4.hr_version.shift = [wp4.hr_version.shift[0], wp4.hr_version.shift[1] - 1]
}
// fix shifts
entities['storage-tank'].pictures.window_background.shift = [0, 1]
entities['storage-tank'].pictures.window_background.hr_version.shift = [0, 1]
add_to_shift([0, -0.6875], entities['artillery-turret'].base_picture.layers[0])
add_to_shift([0, -0.6875], entities['artillery-turret'].cannon_barrel_pictures.layers[0])
add_to_shift([0, -0.6875], entities['artillery-turret'].cannon_base_pictures.layers[0])
function add_to_shift(shift, tab) {
if (tab.shift) {
tab.shift = [shift[0] + tab.shift[0], shift[1] + tab.shift[1]]
} else {
tab.shift = shift
}
if (tab.hr_version) {
if (tab.hr_version.shift) {
tab.hr_version.shift = [shift[0] + tab.hr_version.shift[0], shift[1] + tab.hr_version.shift[1]]
} else {
tab.hr_version.shift = shift
}
}
return tab
}
console.log('Entities: ' + Object.keys(entities).length)
fse.writeFileSync(outDir + 'entityBundle.json', JSON.stringify(entities, null, 2).replace(/"((__base__|__core__)\/.+?)"/g, function(match, capture) {
return '"entity:' + nameMapping(capture) + '"'
}))
graphicsBundle()
async function graphicsBundle() {
let paths = []
let hrPaths = []
let re = /"filename":\s*"([^.]+?\.png)"/g
let str = JSON.stringify(entities)
let match
const excludeKeywords = [
'explosion',
'cloud',
'smoke',
'fire',
'muzzle-flash',
'-light\.padding',
'steam\.png',
'-shadow\.png',
'-shadow-',
'load-standup',
'flamethrower-turret-gun(-[^e]|[^-])',
'pump-[a-z]+?-liquid',
'pump-[a-z]+?-glass',
'accumulator-[a-z]+?-animation',
'connector\/(hr-)?.-.-',
'heated',
'gun-turret-gun-[m12]',
'roboport-recharging',
'segment-visualisation',
'graphics\/[^/]*$',
'-light\.png',
'-lights-color',
'boiling-green',
'power-switch-electricity',
'electric-furnace-heater',
'integration',
'arrows',
'hole',
'rocket-over',
'working',
'hand-closed'
]
const excludeKeywordsRegex = new RegExp(excludeKeywords.join('|'), 'g')
while ((match = re.exec(str)) !== null) {
let path = match[1].replace(/__base__/g, 'base').replace(/__core__/g, 'core')
if (match[1].search(excludeKeywordsRegex) === -1) {
if (match[1].search(/\/hr-/g) === -1) {
if (!paths.includes(path)) {
paths.push(path)
}
} else {
if (!hrPaths.includes(path)) {
hrPaths.push(path)
}
}
}
}
paths.push('base/graphics/entity/artillery-wagon/artillery-wagon-cannon-barrel-1.png')
paths.push('base/graphics/entity/artillery-wagon/artillery-wagon-cannon-barrel-5.png')
paths.push('base/graphics/entity/artillery-wagon/artillery-wagon-cannon-barrel-9.png')
paths.push('base/graphics/entity/artillery-wagon/artillery-wagon-cannon-barrel-13.png')
hrPaths.push('base/graphics/entity/artillery-wagon/hr-artillery-wagon-cannon-barrel-1.png')
hrPaths.push('base/graphics/entity/artillery-wagon/hr-artillery-wagon-cannon-barrel-5.png')
hrPaths.push('base/graphics/entity/artillery-wagon/hr-artillery-wagon-cannon-barrel-9.png')
hrPaths.push('base/graphics/entity/artillery-wagon/hr-artillery-wagon-cannon-barrel-13.png')
paths.push('base/graphics/entity/artillery-wagon/artillery-wagon-cannon-base-1.png')
paths.push('base/graphics/entity/artillery-wagon/artillery-wagon-cannon-base-5.png')
paths.push('base/graphics/entity/artillery-wagon/artillery-wagon-cannon-base-9.png')
paths.push('base/graphics/entity/artillery-wagon/artillery-wagon-cannon-base-13.png')
hrPaths.push('base/graphics/entity/artillery-wagon/hr-artillery-wagon-cannon-base-1.png')
hrPaths.push('base/graphics/entity/artillery-wagon/hr-artillery-wagon-cannon-base-5.png')
hrPaths.push('base/graphics/entity/artillery-wagon/hr-artillery-wagon-cannon-base-9.png')
hrPaths.push('base/graphics/entity/artillery-wagon/hr-artillery-wagon-cannon-base-13.png')
console.log('Entity images: ' + paths.length)
console.log('Entity HR images: ' + hrPaths.length)
let cropImages = [
['artillery-wagon-cannon', 4, 4],
['flamethrower-turret-gun-extension', 5, 1],
['gun-turret-gun-extension', 5, 1],
['laser-turret-gun-start', 15, 1],
['burner-mining-drill', 4, 8],
['electric-mining-drill', 8, 8],
['pumpjack-horsehead', 8, 5],
['assembling-machine-[1-3]\.png', 8, 4],
['centrifuge', 8, 8],
['lab.png', 11, 3],
['[^e]-pump-', 8, 4],
['splitter', 8, 4],
['radar', 8, 8],
['steam-engine', 8, 4],
['steam-turbine', 4, 2],
['transport-belt', 16, 1],
['laser-turret-gun', 8, 1],
['beacon-antenna', 8, 4],
['roboport-door-', 16, 1],
['gate(-rail(-base)?)?-[a-z]+?(-(left|right))?\.png', 8, 2],
['arm', 4, 3],
['rail-signal\.png', 3, 1],
['rail-chain-signal\.png', 4, 1],
['power-switch', 2, 3]
]
let addedHrPaths = []
let imagesToCrop = []
for (let i = 0; i < paths.length; i++) {
let pArr = paths[i].split('/')
if (pArr[pArr.length - 1] === 'electric-furnace-base.png') {
pArr[pArr.length - 1] = 'electric-furnace.png'
}
pArr[pArr.length - 1] = 'hr-' + pArr[pArr.length - 1]
let hrVersion = pArr.join('/')
if (hrPaths.includes(hrVersion)) {
paths[i] = hrVersion
addedHrPaths.push(hrVersion)
}
paths[i] = factorioDirectory + paths[i]
// Crop spritesheet
for (let j = 0, len2 = cropImages.length; j < len2; j++) {
if (paths[i].search(new RegExp(cropImages[j][0], 'g')) !== -1) {
let p = './temp/' + nameMapping(paths[i]) + '.png'
imagesToCrop.push({
path: paths[i],
outPath: p,
cropImgIndex: j
})
paths[i] = p
break
}
}
}
for (let i = 0; i < hrPaths.length; i++) {
if (!addedHrPaths.includes(hrPaths[i])) {
paths.push(factorioDirectory + hrPaths[i])
}
}
//Jison.write doesn't return a Promise
//https://github.com/oliver-moran/jimp/issues/90
Jimp.prototype.writeAsync = util.promisify(Jimp.prototype.write)
let res = Promise.all(imagesToCrop.map(data => Jimp.read(data.path).then(img =>
img
.crop(0, 0, img.bitmap.width / cropImages[data.cropImgIndex][1], img.bitmap.height / cropImages[data.cropImgIndex][2])
.writeAsync(data.outPath)
)))
res.then(() => {
console.log('Final entity images: ' + paths.length)
nsg({
src: paths,
spritePath: spritesheetsOutDir + 'entitySpritesheet.png',
stylesheet: './json-entity.tpl',
stylesheetPath: spritesheetsOutDir + 'entitySpritesheet.json',
stylesheetOptions: {
prefix: 'entity:',
nameMapping: nameMapping
},
compositor: 'jimp',
layout: 'packed',
layoutOptions: {
padding: 2
}
}, function(err) {
if (err) {
console.log(err)
} else {
fse.remove('./temp')
console.log('Entity sprite atlas generated!')
}
})
})
}

289
src/app.ts Normal file
View File

@@ -0,0 +1,289 @@
// tslint:disable:no-import-side-effect
import 'normalize.css'
import * as PIXI from 'pixi.js'
import keyboardJS from 'keyboardjs'
import { Book } from './factorio-data/book'
import BPString from './factorio-data/BPString'
import sampleBP from './sample-blueprint'
import util from './util'
import { InventoryContainer } from './containers/inventory'
import G from './globals'
import { EntityContainer } from './containers/entity'
import { PaintContainer } from './containers/paint'
import { BlueprintContainer } from './containers/blueprint'
import { ToolbarContainer } from './containers/toolbar'
import { isNumber } from 'util'
import { Blueprint } from './factorio-data/blueprint'
import { EditEntityContainer } from './containers/editEntity'
import { InfoContainer } from './containers/info'
G.renderOnly = window.location.search.slice(1).split('&').includes('renderOnly')
G.app = new PIXI.Application({
autoStart: false,
antialias: true,
resolution: window.devicePixelRatio
// roundPixels: true
})
// https://github.com/pixijs/pixi.js/issues/3928
G.app.renderer.plugins.interaction.moveWhenInside = true
G.app.renderer.view.style.position = 'absolute'
G.app.renderer.view.style.display = 'none'
G.app.renderer.autoResize = true
G.app.renderer.resize(window.innerWidth, window.innerHeight)
window.addEventListener('resize', () => {
G.app.renderer.resize(window.innerWidth, window.innerHeight)
G.BPC.zoomPan.setViewPortSize(G.app.renderer.width, G.app.renderer.height)
G.BPC.zoomPan.updateTransform()
G.BPC.updateViewportCulling()
}, false)
document.body.appendChild(G.app.view)
// PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST
// PIXI.settings.GC_MODE = PIXI.GC_MODES.MANUAL
G.BPC = new BlueprintContainer()
G.app.stage.addChild(G.BPC)
G.editEntityContainer = new EditEntityContainer()
G.app.stage.addChild(G.editEntityContainer)
G.inventoryContainer = new InventoryContainer()
G.app.stage.addChild(G.inventoryContainer)
G.toolbarContainer = new ToolbarContainer()
G.app.stage.addChild(G.toolbarContainer)
const infoContainer = new InfoContainer()
G.app.stage.addChild(infoContainer)
PIXI.loader
.add([
{ name: 'extra_iconSprites', url: 'spritesheets/extra_iconSpritesheet.json' },
{ name: 'iconSprites', url: 'spritesheets/iconSpritesheet.json' },
{ name: 'entitySprites', url: 'spritesheets/entitySpritesheet.json' }
])
.load((_: any, resources: any) => {
G.app.renderer.plugins.prepare
.add(resources.extra_iconSprites.spritesheet.baseTexture)
.add(resources.iconSprites.spritesheet.baseTexture)
.add(resources.entitySprites.spritesheet.baseTexture)
.upload(setup)
})
function setup() {
let initialSource: string
for (const a of window.location.search.slice(1).split('&')) {
if (a.includes('source')) {
initialSource = a.split('=')[1]
break
}
}
loadBpFromSource(initialSource ? initialSource : sampleBP).then(() => {
if (!G.bp) G.bp = new Blueprint()
G.BPC.centerViewport()
G.BPC.updateCursorPosition({
x: G.app.renderer.width / 2,
y: G.app.renderer.height / 2
})
G.app.start()
G.app.renderer.view.style.display = 'block'
})
}
function loadBpFromSource(source: string) {
return util.findBPString(source).then(loadBp).catch(error => {
console.error(error)
})
function loadBp(bpString: string) {
const res = BPString.decode(bpString)
// TODO: Handle decode errors
if ((res as {error: any}).error) throw (res as {error: any}).error
G.bp = res instanceof Book ? res.getBlueprint() : res
G.BPC.clearData()
G.BPC.initBP()
console.log('Loaded BP String')
}
}
window.addEventListener('copy', e => {
e.preventDefault()
e.clipboardData.setData('text/plain', BPString.encode(G.bp))
console.log('Copied BP String')
})
window.addEventListener('paste', e => {
e.preventDefault()
G.app.renderer.view.style.display = 'none'
loadBpFromSource(e.clipboardData.getData('text')).then(() => G.app.renderer.view.style.display = 'block')
})
keyboardJS.bind('shift + n', () => {
G.BPC.clearData()
G.bp = new Blueprint()
G.BPC.initBP()
})
keyboardJS.bind('modifier + s', e => {
e.preventDefault()
G.BPC.centerViewport()
if (G.renderOnly) G.BPC.cacheAsBitmap = false
const t = G.app.renderer.generateTexture(G.BPC)
if (G.renderOnly) G.BPC.cacheAsBitmap = true
t.frame = G.BPC.entitySprites.getLocalBounds()
t._updateUvs()
const s = new PIXI.Sprite(t)
const image = G.app.renderer.plugins.extract.image(s)
const w = window.open()
w.focus()
w.document.write(image.outerHTML)
console.log('Saved BP Image')
})
keyboardJS.bind('shift', () => G.keyboard.shift = true, () => G.keyboard.shift = false)
keyboardJS.bind('alt', e => {
e.preventDefault()
G.BPC.overlayContainer.overlay.visible = !G.BPC.overlayContainer.overlay.visible
})
keyboardJS.bind('i', () => infoContainer.toggle())
keyboardJS.bind('esc', () => { if (G.openedGUIWindow) G.openedGUIWindow.close() })
keyboardJS.bind('e', () => {
if (G.currentMouseState !== G.mouseStates.MOVING && G.currentMouseState !== G.mouseStates.PAINTING && !G.renderOnly) {
if (G.openedGUIWindow) {
G.openedGUIWindow.close()
} else {
G.inventoryContainer.toggle()
}
}
})
keyboardJS.bind('f', () => G.BPC.centerViewport())
keyboardJS.bind('r', () => {
if (G.BPC.hoverContainer &&
(G.currentMouseState === G.mouseStates.NONE || G.currentMouseState === G.mouseStates.MOVING)
) {
G.BPC.hoverContainer.rotate()
} else if (G.currentMouseState === G.mouseStates.PAINTING) {
G.BPC.paintContainer.rotate()
}
})
keyboardJS.bind('q', () => {
if (G.BPC.hoverContainer && G.currentMouseState === G.mouseStates.NONE) {
G.currentMouseState = G.mouseStates.PAINTING
const hoverContainer = G.BPC.hoverContainer
G.BPC.hoverContainer.pointerOutEventHandler()
const entity = G.bp.entity(hoverContainer.entity_number)
G.BPC.paintContainer = new PaintContainer(entity.name,
entity.directionType === 'output' ? (entity.direction + 4) % 8 : entity.direction,
hoverContainer.position)
G.BPC.paintContainer.moveTo({
x: G.gridCoordsOfCursor.x * 32,
y: G.gridCoordsOfCursor.y * 32
})
G.BPC.addChild(G.BPC.paintContainer)
} else if (G.currentMouseState === G.mouseStates.PAINTING) {
G.BPC.paintContainer.destroy()
G.BPC.paintContainer = undefined
G.currentMouseState = G.mouseStates.NONE
}
})
keyboardJS.bind('modifier + z', () => {
G.bp.undo(
hist => pre(hist, 'add'),
hist => post(hist, 'del')
)
})
keyboardJS.bind('modifier + y', () => {
G.bp.redo(
hist => pre(hist, 'del'),
hist => post(hist, 'add')
)
})
function pre(hist: any, addDel: string) {
switch (hist.type) {
case 'mov':
case addDel:
const e = EntityContainer.mappings.get(hist.entity_number)
e.redrawSurroundingEntities()
if (hist.type === addDel) {
G.BPC.wiresContainer.remove(hist.entity_number)
e.destroy()
}
if (hist.type === 'mov') G.BPC.wiresContainer.update(hist.entity_number)
}
}
function post(hist: any, addDel: string) {
function redrawEntityAndSurroundingEntities(entnr: number) {
const e = EntityContainer.mappings.get(entnr)
e.redraw()
e.redrawSurroundingEntities()
}
switch (hist.type) {
case 'mov':
redrawEntityAndSurroundingEntities(hist.entity_number)
const entity = G.bp.entity(hist.entity_number)
const e = EntityContainer.mappings.get(hist.entity_number)
e.position.set(
entity.position.x * 32,
entity.position.y * 32
)
e.updateVisualStuff()
break
case 'upd':
if (isNumber(hist.entity_number)) {
const e = EntityContainer.mappings.get(hist.entity_number)
e.redrawEntityInfo()
redrawEntityAndSurroundingEntities(hist.entity_number)
G.BPC.wiresContainer.update(hist.entity_number)
if (G.editEntityContainer.visible) {
if (G.inventoryContainer.visible) G.inventoryContainer.close()
G.editEntityContainer.create(hist.entity_number)
}
} else {
for (const entnr of hist.entity_number) {
redrawEntityAndSurroundingEntities(entnr)
}
}
break
case addDel:
const ec = new EntityContainer(hist.entity_number)
G.BPC.entities.addChild(ec)
ec.redrawSurroundingEntities()
G.BPC.wiresContainer.update(hist.entity_number)
}
console.log(`${addDel === 'del' ? 'Undo' : 'Redo'} ${hist.entity_number} ${hist.annotation}`)
G.BPC.updateOverlay()
G.BPC.updateViewportCulling()
}
keyboardJS.bind('w', () => G.keyboard.w = true, () => G.keyboard.w = false)
keyboardJS.bind('a', () => G.keyboard.a = true, () => G.keyboard.a = false)
keyboardJS.bind('s', () => G.keyboard.s = true, () => G.keyboard.s = false)
keyboardJS.bind('d', () => G.keyboard.d = true, () => G.keyboard.d = false)

475
src/blueprintSchema.json Normal file
View File

@@ -0,0 +1,475 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "blueprintSchema.json",
"type": "object",
"oneOf": [
{
"required": [ "blueprint" ],
"additionalProperties": false,
"properties": {
"blueprint": { "$ref": "blueprintSchema.json#/definitions/blueprint" }
}
},
{
"required": [ "blueprint_book" ],
"additionalProperties": false,
"properties": {
"blueprint_book": {
"type": "object",
"required": ["version", "item", "active_index", "blueprints"],
"additionalProperties": false,
"properties": {
"version": { "type": "integer" },
"item": {
"type": "string",
"const": "blueprint-book"
},
"label": { "type": "string" },
"active_index": { "type": "integer" },
"blueprints": {
"type": "array",
"minItems": 1,
"maxITems": 1000,
"items": {
"type": "object",
"required": ["index", "blueprint"],
"additionalProperties": false,
"properties": {
"index": { "type": "integer" },
"blueprint": { "$ref": "blueprintSchema.json#/definitions/blueprint" }
}
}
}
}
}
}
}
],
"definitions": {
"position": {
"type": "object",
"required": ["x", "y"],
"additionalProperties": false,
"properties": {
"x": { "type": "number" },
"y": { "type": "number" }
}
},
"signal": {
"type": "object",
"required": ["name", "type"],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"itemName": true
},
"type": {
"type": "string",
"enum": ["item", "virtual", "fluid"]
}
}
},
"wireColor": {
"type": "array",
"items": {
"type": "object",
"required": ["entity_id"],
"additionalProperties": false,
"properties": {
"entity_id": { "type": "integer" },
"circuit_id": { "type": "integer" }
}
}
},
"side": {
"type": "object",
"additionalProperties": false,
"properties": {
"red": { "$ref": "blueprintSchema.json#/definitions/wireColor" },
"green": { "$ref": "blueprintSchema.json#/definitions/wireColor" }
}
},
"blueprint": {
"type": "object",
"required": ["version", "item", "icons"],
"additionalProperties": false,
"properties": {
"version": { "type": "integer" },
"item": {
"type": "string",
"const": "blueprint"
},
"label": { "type": "string" },
"icons": {
"type": "array",
"minItems": 1,
"maxItems": 4,
"items": {
"type": "object",
"required": ["index", "signal"],
"additionalProperties": false,
"properties": {
"index": {
"type": "integer",
"enum": [1, 2, 3, 4]
},
"signal": { "$ref": "blueprintSchema.json#/definitions/signal" }
}
}
},
"entities": {
"type": "array",
"items": {
"type": "object",
"required": ["entity_number", "name", "position"],
"additionalProperties": false,
"properties": {
"entity_number": { "type": "integer" },
"name": {
"type": "string",
"entityName": true
},
"position": { "$ref": "blueprintSchema.json#/definitions/position" },
"direction": {
"type": "integer",
"enum": [0, 1, 2, 3, 4, 5, 6, 7],
"$comment": "direction, can be ommited if 0"
},
"type": {
"type": "string",
"enum": ["input", "output"],
"$comment": "direction type, only present if entity is of type underground-belt"
},
"recipe": {
"type": "string",
"recipeName": true,
"$comment": "recipe name, only present if entity is of type assembling-machine or has fixed_recipe"
},
"bar": {
"type": "integer",
"$comment": "inventory size limitation, only present if entity has inventory_size"
},
"items": {
"type": "object",
"objectWithItemNames": true,
"$comment": "object, keys are module names and value nr of modules, only present if entity has module_specification"
},
"input_priority": {
"type": "string",
"enum": ["left", "right"],
"$comment": "splitter input priority, only present if entity is of type splitter"
},
"output_priority": {
"type": "string",
"enum": ["left", "right"],
"$comment": "splitter output priority, only present if entity is of type splitter"
},
"filter": {
"type": "string",
"itemName": true,
"$comment": "splitter filter for output priority, only present if entity is of type splitter"
},
"station": {
"type": "string",
"$comment": "train stop station name, only present if entity is train-stop"
},
"color": {
"type": "object",
"required": ["r", "g", "b", "a"],
"additionalProperties": false,
"$comment": "train stop color, only present if entity is train-stop",
"properties": {
"r": { "type": "number" },
"g": { "type": "number" },
"b": { "type": "number" },
"a": { "type": "number" }
}
},
"auto_launch": {
"type": "boolean",
"$comment": "auto launch, only present if entity is rocket-silo"
},
"override_stack_size": {
"type": "integer",
"$comment": "override stack size, only present if entity is of type inserter"
},
"request_from_buffers": {
"type": "boolean",
"$comment": "only present if entity is logistic-chest-requester"
},
"filters": {
"type": "array",
"$comment": "only present if entity is filter-inserter or stack-filter-inserter",
"items": {
"type": "object",
"required": ["index", "name"],
"additionalProperties": false,
"properties": {
"index": { "type": "integer" },
"name": {
"type": "string",
"itemName": true
}
}
}
},
"request_filters": {
"type": "array",
"$comment": "only present if entity is logistic-chest-storage, logistic-chest-buffer or logistic-chest-requester",
"items": {
"type": "object",
"required": ["index", "name", "count"],
"additionalProperties": false,
"properties": {
"index": { "type": "integer" },
"name": {
"type": "string",
"itemName": true
},
"count": { "type": "integer" }
}
}
},
"alert_parameters": {
"type": "object",
"$comment": "only present if entity is programmable-speaker",
"additionalProperties": false,
"properties": {
"alert_message": { "type": "string" },
"icon_signal_id": { "$ref": "blueprintSchema.json#/definitions/signal" },
"show_alert": { "type": "boolean" },
"show_on_map": { "type": "boolean" }
}
},
"parameters": {
"type": "object",
"$comment": "only present if entity is programmable-speaker",
"additionalProperties": false,
"properties": {
"playback_volume": { "type": "number" },
"playback_globally": { "type": "boolean" },
"allow_polyphony": { "type": "boolean" }
}
},
"connections": {
"type": "object",
"$comment": "wire connections",
"additionalProperties": false,
"properties": {
"1": { "$ref": "blueprintSchema.json#/definitions/side" },
"2": { "$ref": "blueprintSchema.json#/definitions/side" }
}
},
"control_behavior": {
"type": "object",
"additionalProperties": false,
"properties": {
"is_on": {
"type": "boolean",
"enum": [false],
"$comment": "only present if entity is constant-combinator"
},
"filters": {
"type": "array",
"$comment": "only present if entity is constant-combinator",
"items": {
"type": "object",
"required": ["index", "count", "signal"],
"additionalProperties": false,
"properties": {
"index": { "type": "integer" },
"count": { "type": "integer" },
"signal": { "$ref": "blueprintSchema.json#/definitions/signal" }
}
}
},
"use_colors": {
"type": "boolean",
"enum": [true],
"$comment": "only present if entity is small-lamp"
},
"circuit_enable_disable": {
"type": "boolean",
"$comment": "only present if entity is of type mining-drill or transport-belt or train-stop"
},
"circuit_read_hand_contents": {
"type": "boolean",
"$comment": "only present if entity is of type inserter or transport-belt"
},
"circuit_hand_read_mode": {
"type": "integer",
"enum": [1],
"$comment": "0 = pulse, 1 = hold, only present if entity is of type inserter and circuit_read_hand_contents is true"
},
"circuit_set_stack_size": {
"type": "boolean",
"enum": [true],
"$comment": "only present if entity is of type inserter and override_stack_size is not set"
},
"stack_control_input_signal": { "$ref": "blueprintSchema.json#/definitions/signal" },
"circuit_contents_read_mode": {
"type": "integer",
"enum": [0, 1],
"$comment": "0 = pulse, 1 = hold, only present if entity is of type transport-belt and circuit_read_hand_contents is true"
},
"circuit_mode_of_operation": {
"type": "integer",
"$comment": "only present if entity is roboport or logistic-chest-buffer or logistic-chest-requester or of type inserter(3)????????????????"
},
"available_logistic_output_signal": { "$ref": "blueprintSchema.json#/definitions/signal" },
"total_logistic_output_signal": { "$ref": "blueprintSchema.json#/definitions/signal" },
"available_construction_output_signal": { "$ref": "blueprintSchema.json#/definitions/signal" },
"total_construction_output_signal": { "$ref": "blueprintSchema.json#/definitions/signal" },
"circuit_read_resources": {
"type": "boolean",
"$comment": "only present if entity is of type mining-drill"
},
"circuit_resource_read_mode": {
"type": "integer",
"enum": [0,1],
"$comment": "only present if entity is burner-mining-drill or electric-mining-drill and circuit_read_resources is true"
},
"circuit_open_gate": {
"type": "boolean",
"$comment": "only present if entity is stone-wall"
},
"circuit_read_sensor": {
"type": "boolean",
"$comment": "only present if entity is stone-wall"
},
"send_to_train": {
"type": "boolean",
"enum": [false],
"$comment": "only present if entity is train-stop"
},
"read_from_train": {
"type": "boolean",
"enum": [true],
"$comment": "only present if entity is train-stop"
},
"read_stopped_train": {
"type": "boolean",
"enum": [true],
"$comment": "only present if entity is train-stop"
},
"train_stopped_signal": { "$ref": "blueprintSchema.json#/definitions/signal" },
"circuit_close_signal": {
"type": "boolean",
"$comment": "only present if entity is rail-signal"
},
"circuit_read_signal": {
"type": "boolean",
"$comment": "only present if entity is rail-signal, for chain signals: you have the same signals"
},
"red_output_signal": { "$ref": "blueprintSchema.json#/definitions/signal" },
"orange_output_signal": { "$ref": "blueprintSchema.json#/definitions/signal" },
"green_output_signal": { "$ref": "blueprintSchema.json#/definitions/signal" },
"blue_output_signal": { "$ref": "blueprintSchema.json#/definitions/signal" },
"output_signal": {
"$comment": "only present if entity is stone-wall or accumulator",
"$ref": "blueprintSchema.json#/definitions/signal"
},
"circuit_parameters": {
"type": "object",
"$comment": "only present if entity is programmable-speaker",
"additionalProperties": false,
"properties": {
"instrument_id": { "type": "integer" },
"note_id": { "type": "integer" },
"signal_value_is_pitch": { "type": "boolean" }
}
},
"decider_conditions": {
"type": "object",
"$comment": "only present if entity is decider-combinator",
"additionalProperties": false,
"properties": {
"comparator": { "type": "string" },
"constant": { "type": "integer" },
"copy_count_from_input": { "type": "boolean" },
"first_signal": { "$ref": "blueprintSchema.json#/definitions/signal" },
"second_signal": { "$ref": "blueprintSchema.json#/definitions/signal" },
"output_signal": { "$ref": "blueprintSchema.json#/definitions/signal" }
}
},
"arithmetic_conditions": {
"type": "object",
"$comment": "only present if entity is arithmetic-combinator",
"additionalProperties": false,
"properties": {
"operation": { "type": "string" },
"constant": { "type": "integer" },
"first_constant": { "type": "integer" },
"second_constant": { "type": "integer" },
"first_signal": { "$ref": "blueprintSchema.json#/definitions/signal" },
"second_signal": { "$ref": "blueprintSchema.json#/definitions/signal" },
"output_signal": { "$ref": "blueprintSchema.json#/definitions/signal" }
}
},
"circuit_condition": {
"type": "object",
"$comment": "only present if entity is pump, offshore-pump, rail-signal, train-stop, small-lamp, power-switch, stone-wall, programmable-speaker or of type: inserter, transport-belt or mining-drill",
"additionalProperties": false,
"properties": {
"comparator": { "type": "string" },
"constant": { "type": "integer" },
"first_signal": { "$ref": "blueprintSchema.json#/definitions/signal" },
"second_signal": { "$ref": "blueprintSchema.json#/definitions/signal" }
}
},
"connect_to_logistic_network": {
"type": "boolean",
"enum": [true],
"$comment": "only present if entity is pump, offshore-pump, train-stop, small-lamp, power-switch or of type: inserter, transport-belt or mining-drill"
},
"logistic_condition": {
"type": "object",
"$comment": "only present if entity is pump, offshore-pump, train-stop, small-lamp, power-switch or of type: inserter, transport-belt or mining-drill",
"additionalProperties": false,
"properties": {
"comparator": { "type": "string" },
"constant": { "type": "integer" },
"first_signal": { "$ref": "blueprintSchema.json#/definitions/signal" },
"second_signal": { "$ref": "blueprintSchema.json#/definitions/signal" }
}
}
}
}
}
}
},
"tiles": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "position"],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"tileName": false
},
"position": { "$ref": "blueprintSchema.json#/definitions/position" }
}
}
}
}
}
}
}

62266
src/bundles/entityBundle.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

5976
src/bundles/itemBundle.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2287
src/bundles/tileBundle.json Normal file

File diff suppressed because it is too large Load Diff

342
src/containers/blueprint.ts Normal file
View File

@@ -0,0 +1,342 @@
import G from '../globals'
import { ZoomPan } from '../zoomPan'
import { WiresContainer } from './wires'
import { UnderlayContainer } from './underlay'
import { EntitySprite } from '../entitySprite'
import { AdjustmentFilter } from '@pixi/filter-adjustment'
import { EntityContainer } from './entity'
import { OverlayContainer } from './overlay'
import { PaintContainer } from './paint'
export class BlueprintContainer extends PIXI.Container {
holdingLeftClick: boolean
grid: PIXI.Sprite
wiresContainer: WiresContainer
overlayContainer: OverlayContainer
underlayContainer: UnderlayContainer
entities: PIXI.Container
movingEntityFilter: AdjustmentFilter
entitySprites: PIXI.Container
movementSpeed: number
zoomPan: ZoomPan
holdingRightClick: boolean
lastCursorPos: IPoint
pgOverlay: PIXI.Graphics
hoverContainer: undefined | EntityContainer
movingContainer: undefined | EntityContainer
paintContainer: undefined | PaintContainer
constructor() {
super()
this.interactive = true
this.holdingLeftClick = false
this.holdingRightClick = false
this.lastCursorPos = { x: 0, y: 0 }
this.movementSpeed = 10
this.zoomPan = new ZoomPan(this, G.sizeBPContainer, G.positionBPContainer, {
width: G.app.renderer.width,
height: G.app.renderer.height
}, 10)
this.movingEntityFilter = new AdjustmentFilter({ red: 0.4, blue: 0.4, green: 1 })
const ggrid = new PIXI.Graphics()
for (let i = 0, l = G.sizeBPContainer.width; i < l; i += G.cellSize) {
for (let j = 0, l2 = G.sizeBPContainer.height; j < l2; j += G.cellSize) {
if ((i + j) / G.cellSize % 2) {
ggrid.beginFill(0x303030)
} else {
ggrid.beginFill(0x181818)
}
ggrid.drawRect(i, j, G.cellSize, G.cellSize)
ggrid.endFill()
}
}
this.grid = new PIXI.Sprite(G.app.renderer.generateTexture(ggrid))
this.grid.interactive = false
this.addChild(this.grid)
this.pgOverlay = new PIXI.Graphics()
this.pgOverlay.alpha = 0.2
// this.addChild(this.pgOverlay)
this.underlayContainer = new UnderlayContainer()
this.addChild(this.underlayContainer)
this.entitySprites = new PIXI.Container()
this.entitySprites.interactive = false
this.entitySprites.interactiveChildren = false
this.addChild(this.entitySprites)
this.entities = new PIXI.Container()
this.entities.interactive = false
this.entities.interactiveChildren = true
this.addChild(this.entities)
this.wiresContainer = new WiresContainer()
this.addChild(this.wiresContainer)
this.overlayContainer = new OverlayContainer()
this.addChild(this.overlayContainer)
this.on('pointerdown', this.pointerDownEventHandler)
this.on('pointerup', this.pointerUpEventHandler)
this.on('pointerupoutside', this.pointerUpEventHandler)
this.on('pointermove', this.pointerMoveEventHandler)
document.addEventListener('wheel', e => {
e.preventDefault()
this.zoomPan.setScaleCenter(G.gridCoordsOfCursor.x * 32, G.gridCoordsOfCursor.y * 32)
const z = Math.sign(-e.deltaY) * 0.1
this.zoomPan.zoomBy(z, z)
this.zoomPan.updateTransform()
this.updateViewportCulling()
}, false)
G.app.ticker.add(() => {
const WSXOR = G.keyboard.w !== G.keyboard.s
const ADXOR = G.keyboard.a !== G.keyboard.d
if (WSXOR || ADXOR) {
this.zoomPan.translateBy(
ADXOR ? (G.keyboard.a ? this.movementSpeed : -this.movementSpeed) : 0,
WSXOR ? (G.keyboard.w ? this.movementSpeed : -this.movementSpeed) : 0
)
this.zoomPan.updateTransform()
if (this.updateCursorPosition() && (this.movingContainer || this.paintContainer)) {
(this.movingContainer || this.paintContainer).moveTo({
x: G.gridCoordsOfCursor.x * 32,
y: G.gridCoordsOfCursor.y * 32
})
}
this.updateViewportCulling()
}
})
if (G.renderOnly) {
this.interactiveChildren = false
}
}
initBP() {
// TODO: maybe check for curved rails as well
for (const entity_number of G.bp.rawEntities.keys()) {
const entity = G.bp.entity(entity_number)
if (entity.name === 'straight-rail') {
const x = Math.abs(entity.position.x)
const y = Math.abs(entity.position.y)
G.railMoveOffset = {
x: x % 2 + 1,
y: y % 2 + 1
}
break
}
}
// Render Bp
for (const entity_number of G.bp.rawEntities.keys()) {
this.entities.addChild(new EntityContainer(entity_number, false))
}
this.sortEntities()
this.wiresContainer.drawWires()
this.updateOverlay()
this.centerViewport()
if (G.renderOnly) {
this.cacheAsBitmap = false
this.cacheAsBitmap = true
}
}
clearData() {
const opt = { children: true }
this.underlayContainer.destroy(opt)
this.entitySprites.destroy(opt)
this.entities.destroy(opt)
this.wiresContainer.destroy(opt)
this.overlayContainer.destroy(opt)
this.removeChildren()
this.holdingLeftClick = false
this.holdingRightClick = false
this.hoverContainer = undefined
this.movingContainer = undefined
this.paintContainer = undefined
this.underlayContainer = new UnderlayContainer()
this.entitySprites = new PIXI.Container()
this.entitySprites.interactive = false
this.entitySprites.interactiveChildren = false
this.entities = new PIXI.Container()
this.entities.interactive = false
this.entities.interactiveChildren = true
this.wiresContainer = new WiresContainer()
this.overlayContainer = new OverlayContainer()
this.addChild(this.grid, this.underlayContainer, this.entitySprites, this.entities, this.wiresContainer, this.overlayContainer)
G.currentMouseState = G.mouseStates.NONE
}
sortEntities() {
(this.entities.children as EntityContainer[]).sort((a, b) =>
((b.hitArea as PIXI.Rectangle).height - (a.hitArea as PIXI.Rectangle).height)
);
(this.entitySprites.children as EntitySprite[]).sort((a, b) => {
if (a.isMoving && !b.isMoving) return 1
if (b.isMoving && !a.isMoving) return -1
const dZ = a.zIndex - b.zIndex
if (dZ !== 0) return dZ
const dY = (a.y - a.shift.y) - (b.y - b.shift.y)
if (dY !== 0) return dY
const dO = a.zOrder - b.zOrder
if (dO !== 0) return dO
const dX = (a.x - a.shift.x) - (b.x - b.shift.x)
if (dX !== 0) return dX
return a.id - b.id
})
}
updateCursorPosition(mousePosition?: IPoint) {
const mousePositionInBP = {
x: Math.abs(this.position.x - (mousePosition ? mousePosition.x : G.app.renderer.plugins.interaction.mouse.global.x))
/ this.zoomPan.getCurrentScale(),
y: Math.abs(this.position.y - (mousePosition ? mousePosition.y : G.app.renderer.plugins.interaction.mouse.global.y))
/ this.zoomPan.getCurrentScale()
}
const newGridCoordsOfCursor = {
x: (mousePositionInBP.x - mousePositionInBP.x % 32) / 32,
y: (mousePositionInBP.y - mousePositionInBP.y % 32) / 32
}
if (newGridCoordsOfCursor.x !== G.gridCoordsOfCursor.x || newGridCoordsOfCursor.y !== G.gridCoordsOfCursor.y) {
this.lastCursorPos = { ...(mousePosition ? mousePosition : G.app.renderer.plugins.interaction.mouse.global) }
G.gridCoordsOfCursor = newGridCoordsOfCursor
G.toolbarContainer.updateGridPos(G.gridCoordsOfCursor)
return true
}
}
updateOverlay() {
return
const TEMP = G.bp.entityPositionGrid.getAllPositions()
this.pgOverlay.clear()
for (const t of TEMP) {
this.pgOverlay.beginFill(0x0080FF)
this.pgOverlay.drawRect(t.x * 32, t.y * 32, G.cellSize, G.cellSize)
this.pgOverlay.endFill()
}
}
centerViewport() {
if (G.bp.rawEntities.size === 0) {
this.zoomPan.setPosition(-G.sizeBPContainer.width / 2, -G.sizeBPContainer.height / 2)
this.zoomPan.updateTransform()
return
}
const TL = G.bp.topLeft()
const TR = G.bp.topRight()
const BL = G.bp.bottomLeft()
const W = G.bpArea.width / 2
const H = G.bpArea.height / 2
const hor1 = Math.abs(TL.x - W)
const hor2 = TR.x - W
const ver1 = Math.abs(TL.y - H)
const ver2 = BL.y - H
this.zoomPan.centerViewPort({
x: (hor1 + hor2) * 32,
y: (ver1 + ver2) * 32
}, {
x: (hor1 - hor2) * 16,
y: (ver1 - ver2) * 16
})
this.updateViewportCulling()
}
updateViewportCulling() {
cullChildren(this.entitySprites.children)
cullChildren(this.overlayContainer.overlay.children)
function cullChildren(children: PIXI.DisplayObject[]) {
for (const c of children) {
const b = c.getBounds()
c.renderable =
b.x + b.width > G.positionBPContainer.x &&
b.y + b.height > G.positionBPContainer.y &&
b.x < G.app.renderer.width &&
b.y < G.app.renderer.height
}
}
}
pointerMoveEventHandler(e: PIXI.interaction.InteractionEvent) {
// Update the position here to avoid calling all pointermove eventHandlers with
// G.app.renderer.plugins.interaction.moveWhenInside set to false
const newCursorPos = e.data.getLocalPosition(e.currentTarget)
if (this.movingContainer || this.paintContainer) {
(this.movingContainer || this.paintContainer).moveTo(newCursorPos)
}
if (G.keyboard.w !== G.keyboard.s || G.keyboard.a !== G.keyboard.d) return
const newGridCoordsOfCursor = {
x: (newCursorPos.x - newCursorPos.x % 32) / 32,
y: (newCursorPos.y - newCursorPos.y % 32) / 32
}
if (newGridCoordsOfCursor.x !== G.gridCoordsOfCursor.x || newGridCoordsOfCursor.y !== G.gridCoordsOfCursor.y) {
if (this.hoverContainer) {
if (this.holdingRightClick) this.hoverContainer.removeContainer()
if (this.holdingLeftClick && G.keyboard.shift) this.hoverContainer.pasteRecipe()
}
G.gridCoordsOfCursor = newGridCoordsOfCursor
G.toolbarContainer.updateGridPos(G.gridCoordsOfCursor)
}
if (G.currentMouseState === G.mouseStates.PANNING) {
const dX = G.app.renderer.plugins.interaction.mouse.global.x - this.lastCursorPos.x
const dY = G.app.renderer.plugins.interaction.mouse.global.y - this.lastCursorPos.y
this.zoomPan.translateBy(dX, dY)
this.zoomPan.updateTransform()
this.updateViewportCulling()
}
this.lastCursorPos = { ...G.app.renderer.plugins.interaction.mouse.global }
}
pointerDownEventHandler(e: PIXI.interaction.InteractionEvent) {
if (G.currentMouseState === G.mouseStates.NONE) {
if (e.data.button === 0) {
if (!G.openedGUIWindow && !G.keyboard.shift) {
G.currentMouseState = G.mouseStates.PANNING
}
this.holdingLeftClick = true
} else if (e.data.button === 2) {
this.holdingRightClick = true
}
}
}
pointerUpEventHandler(e: PIXI.interaction.InteractionEvent) {
if (e.data.button === 0) {
if (G.currentMouseState === G.mouseStates.PANNING) {
G.currentMouseState = G.mouseStates.NONE
}
this.holdingLeftClick = false
} else if (e.data.button === 2) {
this.holdingRightClick = false
}
}
}

View File

@@ -0,0 +1,167 @@
import factorioData from '../factorio-data/factorioData'
import G from '../globals'
import { InventoryContainer } from './inventory'
import { EntityContainer } from './entity'
export class EditEntityContainer extends PIXI.Container {
content: PIXI.Container
itemTooltip: PIXI.Text
iconGutter = 32
inventoryActiveGroup: PIXI.Sprite
inventoryGroup: Map<PIXI.Sprite, PIXI.Container> = new Map()
iWidth = 32 * 12
iHeight = 32 * 13
constructor() {
super()
this.visible = false
this.interactive = true
this.setPosition()
window.addEventListener('resize', () => this.setPosition(), false)
const background = new PIXI.Sprite(PIXI.Texture.WHITE)
background.width = this.iWidth
background.height = this.iHeight
background.tint = 0x3A3A3A
background.alpha = 0.9
this.addChild(background)
this.content = new PIXI.Container()
this.addChild(this.content)
}
setPosition() {
this.position.set(
G.app.renderer.width / 2 - this.iWidth / 2,
G.app.renderer.height / 2 - this.iHeight / 2
)
}
// TODO: Refactor, optimize and make a layout system for this
create(entity_number: number) {
this.content.removeChildren()
const entity = G.bp.entity(entity_number)
const cc = entity.entityData.crafting_categories
if (cc && !cc.includes('rocket-building') && !cc.includes('smelting')) {
const recipeContainer = new PIXI.Container()
const background = new PIXI.Sprite(PIXI.Texture.WHITE)
background.anchor.set(0.5, 0.5)
background.width = 32
background.height = 32
background.tint = 0x9E9E9E
recipeContainer.addChild(background)
if (entity.recipe) recipeContainer.addChild(InventoryContainer.createIcon(factorioData.getItem(entity.recipe)))
recipeContainer.position.set(
this.iWidth / 2 + 16,
this.iHeight / 2 - 18
)
recipeContainer.interactive = true
recipeContainer.buttonMode = true
recipeContainer.on('pointerdown', (e: PIXI.interaction.InteractionEvent) => {
e.stopPropagation()
if (e.data.button === 0) {
G.inventoryContainer.toggle(entity.acceptedRecipes, name => {
G.openedGUIWindow = this
if (entity.recipe !== name) {
EntityContainer.mappings.get(entity_number).changeRecipe(name)
this.create(entity_number)
}
})
} else if (e.data.button === 2) {
if (entity.recipe) {
EntityContainer.mappings.get(entity_number).changeRecipe(undefined)
this.create(entity_number)
}
}
})
this.content.addChild(recipeContainer)
const recipeText = new PIXI.Text('Recipe ')
recipeText.anchor.set(1, 0.5)
recipeText.position.set(
this.iWidth / 2,
this.iHeight / 2 - 18
)
recipeText.style.fill = 0xFFFFFF
this.content.addChild(recipeText)
}
if (entity.entityData.module_specification) {
const moduleContainer = new PIXI.Container()
moduleContainer.position.set(
this.iWidth / 2 + 16,
this.iHeight / 2 + 18
)
const slots = entity.entityData.module_specification.module_slots
const modules = entity.modulesList
for (let i = 0; i < slots; i++) {
const slot = new PIXI.Container()
slot.position.set(i * 36, 0)
slot.interactive = true
slot.buttonMode = true
slot.on('pointerdown', (e: PIXI.interaction.InteractionEvent) => {
e.stopPropagation()
if (e.data.button === 0) {
G.inventoryContainer.toggle(entity.acceptedModules, name => {
G.openedGUIWindow = this
if (modules && modules[i] !== name) {
modules[modules.length] = name
entity.modulesList = modules
} else {
entity.modulesList = [name]
}
EntityContainer.mappings.get(entity_number).redrawEntityInfo()
this.create(entity_number)
})
} else if (e.data.button === 2) {
if (modules && modules[i]) {
modules.splice(i, 1)
entity.modulesList = modules
EntityContainer.mappings.get(entity_number).redrawEntityInfo()
this.create(entity_number)
}
}
})
const background = new PIXI.Sprite(PIXI.Texture.WHITE)
background.anchor.set(0.5, 0.5)
background.width = 32
background.height = 32
background.tint = 0x9E9E9E
slot.addChild(background)
if (modules && modules[i]) slot.addChild(InventoryContainer.createIcon(factorioData.getItem(modules[i])))
moduleContainer.addChild(slot)
}
this.content.addChild(moduleContainer)
const recipeText = new PIXI.Text('Modules ')
recipeText.anchor.set(1, 0.5)
recipeText.position.set(
this.iWidth / 2,
this.iHeight / 2 + 18
)
recipeText.style.fill = 0xFFFFFF
this.content.addChild(recipeText)
}
if (this.content.children.length !== 0) {
this.visible = true
G.openedGUIWindow = this
}
}
close() {
if (this.visible && G.openedGUIWindow !== this) {
G.openedGUIWindow.close()
}
this.visible = false
G.openedGUIWindow = undefined
}
}

427
src/containers/entity.ts Normal file
View File

@@ -0,0 +1,427 @@
import G from '../globals'
import factorioData from '../factorio-data/factorioData'
import { updateGroups } from '../updateGroups'
import { isNumber } from 'util'
import { EntitySprite } from '../entitySprite'
import { UnderlayContainer } from './underlay'
export class EntityContainer extends PIXI.Container {
static mappings: Map<number, EntityContainer> = new Map()
static getGridPosition(containerPosition: IPoint) {
return {
x: Math.round(containerPosition.x / 32 * 10) / 10,
y: Math.round(containerPosition.y / 32 * 10) / 10
}
}
static getPositionFromData(currentPos: IPoint, size: IPoint) {
const res = { x: 0, y: 0 }
if (size.x % 2 === 0) {
const npx = currentPos.x - currentPos.x % 16
res.x = npx + (npx % 32 === 0 ? 0 : 16)
} else {
res.x = currentPos.x - currentPos.x % 32 + 16
}
if (size.y % 2 === 0) {
const npy = currentPos.y - currentPos.y % 16
res.y = npy + (npy % 32 === 0 ? 0 : 16)
} else {
res.y = currentPos.y - currentPos.y % 32 + 16
}
return res
}
static isContainerOutOfBpArea(newPos: IPoint, size: IPoint) {
return newPos.x - size.x / 2 < 0 ||
newPos.y - size.y / 2 < 0 ||
newPos.x + size.x / 2 > G.bpArea.width ||
newPos.y + size.y / 2 > G.bpArea.height
}
static getParts(entity: any, hr: boolean, ignore_connections?: boolean): EntitySprite[] {
const anims = factorioData.getSpriteData(entity, hr, ignore_connections ? undefined : G.bp)
// const icon = new PIXI.Sprite(G.iconSprites['icon:' + factorioData.getEntity(entity.name).icon.split(':')[1]])
// icon.x -= 16
// icon.y -= 16
// return [icon]
const parts: EntitySprite[] = []
for (let i = 0, l = anims.length; i < l; i++) {
const img = new EntitySprite(anims[i])
if (entity.name === 'straight-rail' || entity.name === 'curved-rail') {
if (i < 2) {
img.zIndex = -10
} else if (i < 4) {
img.zIndex = -9
} else {
img.zIndex = -8
}
} else if (entity.type === 'transport-belt' || entity.name === 'heat-pipe') {
img.zIndex = i === 0 ? -7 : -6
} else {
img.zIndex = 0
}
img.zOrder = i
parts.push(img)
}
return parts
}
entity_number: number
areaVisualization: PIXI.Sprite | PIXI.Sprite[] | undefined
entityInfo: PIXI.Container
entitySprites: EntitySprite[]
constructor(entity_number: number, sort = true) {
super()
this.entity_number = entity_number
EntityContainer.mappings.set(entity_number, this)
const entity = G.bp.entity(entity_number)
this.position.set(
entity.position.x * 32,
entity.position.y * 32
)
this.interactive = true
this.interactiveChildren = false
this.buttonMode = true
this.entitySprites = []
this.areaVisualization = G.BPC.underlayContainer.createNewArea(entity.name, this.position)
this.entityInfo = G.BPC.overlayContainer.createEntityInfo(this.entity_number, this.position)
this.on('pointerdown', this.pointerDownEventHandler)
// this.on('pointermove', this.pointerMoveEventHandler)
this.on('pointerover', this.pointerOverEventHandler)
this.on('pointerout', this.pointerOutEventHandler)
this.redraw(false, sort)
}
destroy() {
if (G.editEntityContainer.visible) G.editEntityContainer.close()
for (const s of this.entitySprites) s.destroy()
super.destroy()
EntityContainer.mappings.delete(this.entity_number)
UnderlayContainer.modifyVisualizationArea(this.areaVisualization, s => s.destroy())
G.BPC.overlayContainer.hideCursorBox()
G.BPC.overlayContainer.hideUndergroundLines()
if (this.entityInfo) this.entityInfo.destroy()
}
checkBuildable() {
const position = EntityContainer.getGridPosition(this.position)
const entity = G.bp.entity(this.entity_number)
if (!EntityContainer.isContainerOutOfBpArea(position, entity.size) &&
G.bp.entityPositionGrid.checkNoOverlap(entity.name, entity.direction, position)
) {
G.BPC.movingEntityFilter.red = 0.4
G.BPC.movingEntityFilter.green = 1
} else {
G.BPC.movingEntityFilter.red = 1
G.BPC.movingEntityFilter.green = 0.4
}
}
rotate() {
const offset = {
x: (this.x / 16 - G.gridCoords16.x) === 0 ? 0.5 : -0.5,
y: (this.y / 16 - G.gridCoords16.y) === 0 ? 0.5 : -0.5
}
const entity = G.bp.entity(this.entity_number)
let otherEntity
if (G.currentMouseState === G.mouseStates.NONE && entity.type === 'underground-belt') {
otherEntity = G.bp.entityPositionGrid.findEntityWithSameNameAndDirection(
entity.name, entity.direction, entity.position,
entity.directionType === 'input' ? entity.direction : (entity.direction + 4) % 8,
entity.entityData.max_distance
)
if (isNumber(otherEntity)) {
const oe = G.bp.entity(otherEntity)
if (oe.directionType === entity.directionType) {
otherEntity = undefined
} else {
oe.rotate(G.currentMouseState === G.mouseStates.NONE, { x: 0, y: 0 }, false)
EntityContainer.mappings.get(otherEntity).redraw()
}
}
}
if (G.bp.entity(this.entity_number).rotate(G.currentMouseState === G.mouseStates.NONE, offset, true, otherEntity)) {
const entity = G.bp.entity(this.entity_number)
if (G.currentMouseState === G.mouseStates.MOVING && entity.size.x !== entity.size.y) {
this.x += offset.x * 32
this.y += offset.y * 32
const pos = EntityContainer.getPositionFromData(this.position, entity.size)
this.position.set(pos.x, pos.y)
G.BPC.overlayContainer.updateCursorBoxPosition(this.position)
}
this.redraw(G.currentMouseState === G.mouseStates.MOVING)
if (G.currentMouseState === G.mouseStates.NONE) this.redrawSurroundingEntities()
G.BPC.overlayContainer.updateCursorBoxSize(entity.size.x, entity.size.y)
this.updateUndergroundLines()
if (G.BPC.movingContainer === this) this.checkBuildable()
this.redrawEntityInfo()
G.BPC.wiresContainer.update(this.entity_number)
}
}
updateUndergroundLines() {
const entity = G.bp.entity(this.entity_number)
G.BPC.overlayContainer.updateUndergroundLines(
entity.name,
{ x: this.position.x / 32, y: this.position.y / 32 },
entity.direction,
entity.directionType === 'output' || entity.name === 'pipe-to-ground' ? (entity.direction + 4) % 8 : entity.direction
)
}
pointerDownEventHandler(e: PIXI.interaction.InteractionEvent) {
console.log(G.bp.entity(this.entity_number).toJS())
if (e.data.button === 0) {
if (G.currentMouseState === G.mouseStates.NONE && !G.openedGUIWindow && !G.keyboard.shift) {
G.editEntityContainer.create(this.entity_number)
}
if (G.keyboard.shift) this.pasteRecipe()
} else if (e.data.button === 1) {
if (this !== G.BPC.movingContainer && G.currentMouseState === G.mouseStates.NONE) {
G.bp.entityPositionGrid.removeTileData(this.entity_number, false)
this.redraw(true)
this.redrawSurroundingEntities()
G.BPC.movingContainer = this
G.currentMouseState = G.mouseStates.MOVING
// Move container to cursor
const newPosition = e.data.getLocalPosition(this.parent)
const pos = EntityContainer.getPositionFromData(newPosition, G.bp.entity(this.entity_number).size)
if (this.position.x !== pos.x || this.position.y !== pos.y) {
this.position.set(pos.x, pos.y)
this.updateVisualStuff()
}
G.gridCoords16 = {
x: (newPosition.x - newPosition.x % 16) / 16,
y: (newPosition.y - newPosition.y % 16) / 16
}
for (const s of this.entitySprites) s.moving = true
G.BPC.sortEntities()
G.BPC.underlayContainer.activateRelatedAreas(G.bp.entity(this.entity_number).name)
G.BPC.updateOverlay()
return
}
if (this === G.BPC.movingContainer && G.currentMouseState === G.mouseStates.MOVING) {
this.placeEntityContainerDown()
}
} else if (e.data.button === 2 && G.currentMouseState === G.mouseStates.NONE) {
if (G.keyboard.shift) {
G.copyData.recipe = G.bp.entity(this.entity_number).recipe
} else {
G.BPC.holdingRightClick = true
this.removeContainer()
}
}
}
changeRecipe(recipeName: string) {
const entity = G.bp.entity(this.entity_number)
entity.recipe = recipeName
this.redrawEntityInfo()
if (entity.name === 'chemical-plant' || entity.assemblerCraftsWithFluid || G.bp.entity(this.entity_number).assemblerCraftsWithFluid) {
this.redraw()
this.redrawSurroundingEntities()
}
}
pasteRecipe() {
const entity = G.bp.entity(this.entity_number)
if (!entity.entityData.crafting_categories) return
const RECIPE = G.copyData.recipe && entity.acceptedRecipes.includes(G.copyData.recipe) ? G.copyData.recipe : undefined
if (entity.recipe !== RECIPE) this.changeRecipe(RECIPE)
}
redrawEntityInfo() {
const entity = G.bp.entity(this.entity_number)
if (entity.entityData.module_specification || entity.type === 'splitter' ||
entity.entityData.crafting_categories || entity.type === 'mining-drill' ||
entity.type === 'boiler' || entity.type === 'generator' ||
entity.name === 'pump' || entity.name === 'offshore-pump' ||
entity.name === 'arithmetic-combinator' || entity.name === 'decider-combinator'
) {
if (this.entityInfo) this.entityInfo.destroy()
this.entityInfo = G.BPC.overlayContainer.createEntityInfo(this.entity_number, this.position)
}
}
updateVisualStuff() {
for (const s of this.entitySprites) s.setPosition(this.position)
UnderlayContainer.modifyVisualizationArea(this.areaVisualization, s => s.position.copy(this.position))
if (this.entityInfo) this.entityInfo.position = this.position
G.BPC.overlayContainer.updateCursorBoxPosition(this.position)
G.BPC.overlayContainer.updateUndergroundLinesPosition(this.position)
this.updateUndergroundLines()
G.BPC.wiresContainer.update(this.entity_number)
this.checkBuildable()
}
removeContainer() {
G.BPC.wiresContainer.remove(this.entity_number)
G.bp.entityPositionGrid.removeTileData(this.entity_number, false)
this.redrawSurroundingEntities()
G.bp.removeEntity(this.entity_number,
entity_number => EntityContainer.mappings.get(entity_number).redraw()
)
G.BPC.hoverContainer = undefined
G.BPC.updateOverlay()
this.destroy()
}
// pointerMoveEventHandler(e: PIXI.interaction.InteractionEvent) {
// this.moveTo(e.data.getLocalPosition(this.parent))
// }
moveTo(newPosition: IPoint) {
if (G.BPC.movingContainer === this && G.currentMouseState === G.mouseStates.MOVING) {
const newCursorPos = {
x: (newPosition.x - newPosition.x % 16) / 16,
y: (newPosition.y - newPosition.y % 16) / 16
}
if (newCursorPos.x !== G.gridCoords16.x || newCursorPos.y !== G.gridCoords16.y) {
const entity = G.bp.entity(this.entity_number)
switch (entity.name) {
case 'straight-rail':
case 'curved-rail':
case 'train-stop':
this.x = newPosition.x - (newPosition.x + G.railMoveOffset.x * 32) % 64 + 32
this.y = newPosition.y - (newPosition.y + G.railMoveOffset.y * 32) % 64 + 32
break
default:
const pos = EntityContainer.getPositionFromData(newPosition, entity.size)
this.position.set(pos.x, pos.y)
}
this.updateVisualStuff()
G.gridCoords16 = newCursorPos
}
}
}
pointerOverEventHandler() {
// Pointer over is sometimes getting called before pointer out
if (G.BPC.hoverContainer && G.BPC.hoverContainer !== this) G.BPC.hoverContainer.pointerOutEventHandler()
if (!G.BPC.movingContainer && !G.BPC.paintContainer) {
G.BPC.hoverContainer = this
const entity = G.bp.entity(this.entity_number)
G.BPC.overlayContainer.updateCursorBoxSize(entity.size.x, entity.size.y)
G.BPC.overlayContainer.updateCursorBoxPosition(this.position)
G.BPC.overlayContainer.showCursorBox()
G.BPC.overlayContainer.updateUndergroundLinesPosition(this.position)
this.updateUndergroundLines()
UnderlayContainer.modifyVisualizationArea(this.areaVisualization, s => s.visible = true)
}
}
pointerOutEventHandler() {
if (!G.BPC.movingContainer && !G.BPC.paintContainer && G.BPC.hoverContainer === this) {
G.BPC.hoverContainer = undefined
G.BPC.overlayContainer.hideCursorBox()
G.BPC.overlayContainer.hideUndergroundLines()
UnderlayContainer.modifyVisualizationArea(this.areaVisualization, s => s.visible = false)
}
}
placeEntityContainerDown() {
const entity = G.bp.entity(this.entity_number)
const position = EntityContainer.getGridPosition(this.position)
if (EntityContainer.isContainerOutOfBpArea(position, entity.size)) return
if (G.currentMouseState === G.mouseStates.MOVING && entity.move(position)) {
G.BPC.movingContainer = undefined
G.currentMouseState = G.mouseStates.NONE
for (const s of this.entitySprites) s.moving = false
this.redraw(false)
this.redrawSurroundingEntities()
G.BPC.underlayContainer.deactivateActiveAreas()
G.BPC.updateOverlay()
}
}
redrawSurroundingEntities() {
const entity = G.bp.entity(this.entity_number)
const redrawnEntities: number[] = []
for (const updateGroup of updateGroups) {
const j = updateGroup.is.indexOf(entity.name)
if (j !== -1) {
if (entity.name === 'straight-rail') {
G.bp.entityPositionGrid.foreachOverlap(entity.getArea(), (entnr: number) => {
const ent = G.bp.entity(entnr)
if (ent.name === 'gate' && !redrawnEntities.includes(entnr)) {
EntityContainer.mappings.get(ent.entity_number).redraw()
redrawnEntities.push(entnr)
}
})
} else {
G.bp.entityPositionGrid.getSurroundingEntities(entity.getArea(), (entnr: number) => {
const ent = G.bp.entity(entnr)
if (updateGroup.updates.includes(ent.name) && !redrawnEntities.includes(entnr)) {
EntityContainer.mappings.get(ent.entity_number).redraw()
redrawnEntities.push(entnr)
}
})
}
}
}
}
redraw(ignore_connections?: boolean, sort = true) {
const entity = G.bp.entity(this.entity_number)
for (const s of this.entitySprites) s.destroy()
this.entitySprites = []
for (const s of EntityContainer.getParts(entity, true, ignore_connections)) {
if (G.BPC.movingContainer === this) s.moving = true
s.setPosition(this.position)
this.entitySprites.push(s)
G.BPC.entitySprites.addChild(s)
}
if (sort) G.BPC.sortEntities()
this.hitArea = new PIXI.Rectangle(
-entity.size.x * 16,
-entity.size.y * 16,
entity.size.x * 32,
entity.size.y * 32
)
}
}

135
src/containers/info.ts Normal file
View File

@@ -0,0 +1,135 @@
import G from '../globals'
export class InfoContainer extends PIXI.Container {
iWidth = 32 * 18
iHeight = 32 * 24
constructor() {
super()
this.visible = false
this.interactive = false
this.interactiveChildren = false
this.setPosition()
window.addEventListener('resize', () => this.setPosition(), false)
const background = new PIXI.Sprite(PIXI.Texture.WHITE)
background.width = this.iWidth
background.height = this.iHeight
background.tint = 0x3A3A3A
background.alpha = 0.9
this.addChild(background)
const text = new PIXI.Text('KEYBINDS')
text.position.set(this.iWidth / 2, 4)
text.style.fontSize = 24
text.style.fontWeight = 'bold'
text.style.fill = 0xFFFFFF
text.anchor.set(0.5, 0)
this.addChild(text)
this.writeColumn([
'While hovering over an entity',
'',
'',
'',
'',
'',
'In editor window',
'',
'',
'Others'
], { x: this.iWidth / 2, y: 40 }, 0.5, true)
this.writeColumn([
'',
'left click',
'middle click',
'right click',
'R',
'Q',
'',
'left click recipe/module',
'right click recipe/module',
'',
'ctrl + Z/Y',
'ctrl + C/V',
'ctrl + S',
'shift + N',
'shift + right/left click',
'alt',
'esc',
'E',
'F',
'W/A/S/D',
'click + drag in blueprint area',
'mouse wheel'
], { x: this.iWidth / 2 - 4, y: 40 }, 1)
this.writeColumn([
'',
'open editor window',
'move',
'remove',
'rotate',
'pippete tool/clear cursor',
'',
'choose',
'remove',
'',
'undo/redo changes',
'copy/paste bpstring',
'generate bp picture',
'clear bp',
'copy/paste recipe',
'toggle overlay',
'close active window',
'open inventory or close active window',
'focuses viewport on blueprint',
'move',
'move',
'zoom in/out'
], { x: this.iWidth / 2 + 4, y: 40 })
this.writeColumn([
'You can load a blueprint from a bp string, pastebin, hastebin, gist, gitlab,',
' factorioprints, google docs or text webpages.',
'You can also add ?source=<BPSTRING_OR_URL_TO_BPSTRING> to the url',
' to make sharing easier.',
'Adding renderOnly as an url query parameter will only render the bp.',
'I don\'t show network or parsing errors in the app yet, you can open the console',
' (F12) to check if something is wrong.',
'Entities with placeable-off-grid flag will not be added to the positionGrid',
' (ex. landmine).',
'',
'Factorio assets come directly from the Factorio game files, and are subject to',
' all copyright policies associated with the game.'
], { x: 4, y: 500 })
}
writeColumn(data: string[], offset: IPoint, anchorX = 0, bold = false) {
let nextY = 0
for (const str of data) {
const text = new PIXI.Text(str)
text.position.set(offset.x, nextY++ * 20 + offset.y)
text.style.fontSize = 16
if (bold) text.style.fontWeight = 'bold'
text.style.fill = 0xFFFFFF
text.anchor.set(anchorX, 0)
this.addChild(text)
}
}
setPosition() {
this.position.set(
G.app.renderer.width / 2 - this.iWidth / 2,
G.app.renderer.height / 2 - this.iHeight / 2
)
}
toggle() {
this.visible = !this.visible
}
}

248
src/containers/inventory.ts Normal file
View File

@@ -0,0 +1,248 @@
import inventoryBundle from '../bundles/inventoryBundle.json'
import factorioData from '../factorio-data/factorioData'
import { AdjustmentFilter } from '@pixi/filter-adjustment'
import util from '../util'
import G from '../globals'
import { PaintContainer } from './paint'
import { isArray } from 'util'
export class InventoryContainer extends PIXI.Container {
static createIcon(item: any) {
if (item.icon) {
const icon = PIXI.Sprite.fromFrame(item.icon)
icon.anchor.set(0.5, 0.5)
return icon
}
if (item.icons) {
const img = new PIXI.Container()
for (const icon of item.icons) {
const sprite = PIXI.Sprite.fromFrame(icon.icon)
if (icon.scale) sprite.scale.set(icon.scale, icon.scale)
if (icon.shift) sprite.position.set(icon.shift[0], icon.shift[1])
if (icon.tint) {
const t = icon.tint
sprite.filters = [new AdjustmentFilter({
red: t.r,
green: t.g,
blue: t.b,
alpha: t.a
})]
}
sprite.anchor.set(0.5, 0.5)
img.addChild(sprite)
}
return img
}
}
recipeVisualization: PIXI.Container
inventoryContents: PIXI.Container
itemTooltip: PIXI.Text
iconGutter = 36
inventoryActiveGroup: PIXI.Sprite
inventoryGroup: Map<PIXI.Sprite, PIXI.Container> = new Map()
iWidth = 32 * 12
iHeight = 32 * 13
constructor() {
super()
this.visible = false
this.interactive = true
this.setPosition()
window.addEventListener('resize', () => this.setPosition(), false)
const background = new PIXI.Sprite(PIXI.Texture.WHITE)
background.width = this.iWidth
background.height = this.iHeight
background.tint = 0x3A3A3A
background.alpha = 0.9
this.addChild(background)
this.inventoryContents = new PIXI.Container()
this.addChild(this.inventoryContents)
this.itemTooltip = new PIXI.Text('')
this.itemTooltip.style.fill = 0xFFFFFF
this.itemTooltip.y = 352
this.addChild(this.itemTooltip)
this.recipeVisualization = new PIXI.Container()
this.recipeVisualization.position.set(16, 384 + 16)
this.addChild(this.recipeVisualization)
}
setPosition() {
this.position.set(
G.app.renderer.width / 2 - this.iWidth / 2,
G.app.renderer.height / 2 - this.iHeight / 2
)
}
create(filteredItems?: string[], cb?: (name: string) => void) {
this.itemTooltip.text = ''
this.recipeVisualization.visible = false
this.inventoryContents.removeChildren()
let nextI = 0
let groupHasItem = false
for (let i = 0, l = inventoryBundle.length; i < l; i++) {
const grObj = new PIXI.Container()
let nextK = 0
let nextJ = 0
let subgroupHasItem = false
for (const subgroup of inventoryBundle[i].subgroups) {
for (const item of subgroup.items) {
const placeResult = factorioData.getItem(item.name).place_result
if ((!filteredItems && placeResult && factorioData.getEntity(placeResult)) ||
filteredItems && filteredItems.includes(item.name)
) {
const img = InventoryContainer.createIcon(item)
if (nextK > 9) {
nextJ++
nextK = 0
}
img.x = nextK * this.iconGutter + 16
img.y = 64 + nextJ * this.iconGutter + 16
img.interactive = true
img.buttonMode = true
if (filteredItems && filteredItems.includes(item.name)) {
img.on('pointerdown', (e: PIXI.interaction.InteractionEvent) => {
if (e.data.button === 0) {
cb(item.name)
this.visible = false
}
})
} else {
img.on('pointerdown', (e: PIXI.interaction.InteractionEvent) => {
if (e.data.button === 0) {
G.currentMouseState = G.mouseStates.PAINTING
const newPosition = e.data.getLocalPosition(G.BPC)
const size = util.switchSizeBasedOnDirection(factorioData.getEntity(placeResult).size, 0)
G.BPC.paintContainer = new PaintContainer(placeResult, 0, {
x: newPosition.x - newPosition.x % 32 + (size.x % 2 * 16),
y: newPosition.y - newPosition.y % 32 + (size.y % 2 * 16)
})
G.BPC.addChild(G.BPC.paintContainer)
this.visible = false
}
})
}
img.on('pointerover', () => {
this.itemTooltip.text = item.name.split('-').map((s: any) => s.charAt(0).toUpperCase() + s.slice(1)).join(' ')
this.createRecipeVisualization(item.name)
})
img.on('pointerout', () => {
this.itemTooltip.text = ''
this.recipeVisualization.visible = false
})
grObj.addChild(img)
groupHasItem = true
subgroupHasItem = true
nextK++
}
}
if (subgroupHasItem) nextJ++
subgroupHasItem = false
nextK = 0
}
if (groupHasItem) {
const img = PIXI.Sprite.fromFrame(inventoryBundle[i].icon)
img.x = nextI * 64
img.y = 0
img.interactive = true
img.buttonMode = true
img.on('pointerdown', (e: PIXI.interaction.InteractionEvent) => {
if (e.data.button === 0) {
if (img !== this.inventoryActiveGroup) {
this.inventoryGroup.get(this.inventoryActiveGroup).visible = false
this.inventoryActiveGroup = img
this.inventoryGroup.get(img).visible = true
}
}
})
if (nextI === 0) this.inventoryActiveGroup = img
else grObj.visible = false
this.inventoryGroup.set(img, grObj)
this.inventoryContents.addChild(img, grObj)
nextI++
groupHasItem = false
}
}
}
toggle(filteredItems?: string[], cb?: (name: string) => void) {
if (!this.visible) {
this.create(filteredItems, cb)
this.visible = true
G.openedGUIWindow = this
} else {
this.close()
}
}
close() {
this.visible = false
G.openedGUIWindow = G.editEntityContainer.visible ? G.editEntityContainer : undefined
}
createRecipeVisualization(recipeName: string) {
const RECIPE = factorioData.getRecipe(recipeName)
if (!RECIPE) return
this.recipeVisualization.removeChildren()
const recipe = RECIPE.normal ? RECIPE.normal : RECIPE
// TODO: maybe normalize the recipeBundle trough script and not here at runtime
const time = (recipe.energy_required !== undefined ? recipe.energy_required : RECIPE.energy_required) || 0.5
const ingredients = recipe.ingredients.map((o: any) => isArray(o) ? o : [o.name, o.amount])
const results = recipe.result ? [[recipe.result, recipe.result_count || 1]] :
recipe.results.map((o: any) => [o.name, o.probability ? o.probability * o.amount : o.amount])
let nextX = 0
for (const i of ingredients) {
const s = InventoryContainer.createIcon(factorioData.getItem(i[0]))
s.x = nextX * 36
this.recipeVisualization.addChild(s, createAmountText(i[1]))
nextX++
}
const text = new PIXI.Text(`=${time}s>`)
text.style.fontSize = 13
text.style.fontWeight = 'bold'
text.style.fill = 0xFFFFFF
text.anchor.set(0.5, 0.5)
text.x = nextX++ * 36
this.recipeVisualization.addChild(text)
for (const r of results) {
const s = InventoryContainer.createIcon(factorioData.getItem(r[0]))
s.x = nextX * 36
this.recipeVisualization.addChild(s, createAmountText(r[1]))
nextX++
}
function createAmountText(amount: string) {
const text = new PIXI.Text(amount)
text.style.fontSize = 13
text.style.fontWeight = 'bold'
text.style.fill = 0xFFFFFF
text.anchor.set(1, 1)
text.position.set(nextX * 36 + 16, 16)
return text
}
this.recipeVisualization.visible = true
}
}

387
src/containers/overlay.ts Normal file
View File

@@ -0,0 +1,387 @@
import factorioData from '../factorio-data/factorioData'
import { InventoryContainer } from './inventory'
import G from '../globals'
import util from '../util'
export class OverlayContainer extends PIXI.Container {
undergroundLines: PIXI.Container
cursorBox: PIXI.Container
overlay: PIXI.Container
constructor() {
super()
this.interactive = false
this.interactiveChildren = false
this.overlay = new PIXI.Container()
this.cursorBox = new PIXI.Container()
this.cursorBox.scale.set(0.5, 0.5)
this.cursorBox.visible = false
this.undergroundLines = new PIXI.Container()
this.addChild(this.overlay, this.cursorBox, this.undergroundLines)
}
createEntityInfo(entity_number: number, position: IPoint) {
const entity = G.bp.entity(entity_number)
const entityInfo = new PIXI.Container()
if (entity.recipe && entity.recipe !== 'rocket-part') {
const recipeInfo = new PIXI.Container()
createIconWithBackground(recipeInfo, entity.recipe)
const S = entity.name === 'oil-refinery' ? 1.5 : 0.9
recipeInfo.scale.set(S, S)
recipeInfo.position.set(0, -10)
entityInfo.addChild(recipeInfo)
const fluidIcons = new PIXI.Container()
const recipeData = factorioData.getRecipe(entity.recipe)
const rD = recipeData.normal ? recipeData.normal : recipeData
switch (recipeData.category) {
case 'oil-processing':
case 'chemistry':
const inputPositions: IPoint[] = []
const outputPositions: IPoint[] = []
for (const fb of entity.entityData.fluid_boxes) {
(fb.production_type === 'input' ? inputPositions : outputPositions).push({
x: fb.pipe_connections[0].position[0],
y: fb.pipe_connections[0].position[1]
})
}
function createIconsForType(type: string) {
const iconNames: string[] = []
for (const io of type === 'input' ? rD.ingredients : rD.results) {
if (io.type === 'fluid') {
iconNames.push(io.name)
}
}
if (iconNames.length !== 0) {
const positions = type === 'input' ? inputPositions : outputPositions
for (let i = 0; i < positions.length; i++) {
const position = util.transformConnectionPosition(positions[i], entity.direction)
createIconWithBackground(
fluidIcons,
i > iconNames.length - 1 ? iconNames[0] : iconNames[i],
{ x: position.x * 64, y: position.y * 64 }
)
}
}
}
createIconsForType('input')
if (rD.results) createIconsForType('output')
break
case 'crafting-with-fluid':
function createIconForType(type: string) {
for (const io of type === 'input' ? rD.ingredients : rD.results) {
if (io.type === 'fluid') {
const position = util.rotatePointBasedOnDir(entity.entityData.fluid_boxes.find(
(fb: any) => fb.production_type === type).pipe_connections[0].position,
entity.direction
)
createIconWithBackground(
fluidIcons,
io.name,
{ x: position.x * 32, y: position.y * 32 }
)
return true
}
}
}
createIconForType(entity.assemblerPipeDirection)
}
fluidIcons.scale.set(0.5, 0.5)
if (fluidIcons.children.length !== 0) entityInfo.addChild(fluidIcons)
}
if (entity.modules) {
const moduleInfo = new PIXI.Container()
const shift = entity.entityData.module_specification.module_info_icon_shift
const mL = entity.modulesList
for (let i = 0; i < mL.length; i++) {
createIconWithBackground(moduleInfo, mL[i], { x: i * 32, y: 0 })
}
moduleInfo.scale.set(0.5, 0.5)
moduleInfo.position.set((shift ? shift[0] : 0) * 32 - mL.length * 8 + 8, (shift ? shift[1] : 0.75) * 32)
entityInfo.addChild(moduleInfo)
}
const filters = entity.inserterFilters || entity.logisticChestFilters || entity.constantCombinatorFilters
if (filters) {
const filterInfo = new PIXI.Container()
for (let i = 0; i < filters.length; i++) {
if (i === 4) break
createIconWithBackground(
filterInfo,
filters[i].name || filters[i].signal.name,
{ x: i % 2 * 32 - (filters.length !== 1 ? 16 : 0), y: filters.length < 3 ? 0 : (i < 2 ? -16 : 16)}
)
}
let S = 0.5
if (entity.inserterFilters && filters.length !== 1) S = 0.4
if (entity.logisticChestFilters && filters.length === 1) S = 0.6
filterInfo.scale.set(S, S)
entityInfo.addChild(filterInfo)
}
const combinatorConditions = entity.deciderCombinatorConditions || entity.arithmeticCombinatorConditions
if (combinatorConditions) {
const filterInfo = new PIXI.Container()
const cFS = combinatorConditions.first_signal
const cSS = combinatorConditions.second_signal
const cOS = combinatorConditions.output_signal
if (cFS) createIconWithBackground(filterInfo, cFS.name, { x: cSS ? -16 : 0, y: -16 })
if (cSS) createIconWithBackground(filterInfo, cSS.name, { x: 16, y: -16 })
if (cOS) createIconWithBackground(filterInfo, cOS.name, { x: 0, y: 16 })
filterInfo.scale.set(0.5, 0.5)
if (filterInfo.children.length !== 0) entityInfo.addChild(filterInfo)
}
if (entity.type === 'boiler' || entity.type === 'generator') {
const filteredFluidInputs = new PIXI.Container()
function generateIconsForFluidBox(fluidBox: any) {
for (const c of fluidBox.pipe_connections) {
const position = util.transformConnectionPosition({ x: c.position[0], y: c.position[1] }, entity.direction)
createIconWithBackground(
filteredFluidInputs,
fluidBox.filter,
{ x: position.x * 64, y: position.y * 64 }
)
}
}
generateIconsForFluidBox(entity.entityData.fluid_box)
if (entity.entityData.output_fluid_box) generateIconsForFluidBox(entity.entityData.output_fluid_box)
filteredFluidInputs.scale.set(0.5, 0.5)
entityInfo.addChild(filteredFluidInputs)
}
if (entity.splitterInputPriority || entity.splitterOutputPriority) {
const filterInfo = new PIXI.Container()
if (entity.splitterFilter) {
createIconWithBackground(
filterInfo,
entity.splitterFilter,
util.rotatePointBasedOnDir({ x: entity.splitterOutputPriority === 'right' ? 32 : -32, y: 0 }, entity.direction)
)
} else if (entity.splitterOutputPriority) createArrowForDirection(entity.splitterOutputPriority, -16)
if (entity.splitterInputPriority) createArrowForDirection(entity.splitterInputPriority, 16)
function createArrowForDirection(direction: string, offsetY: number) {
const arrow = createArrow(util.rotatePointBasedOnDir({ x: direction === 'right' ? 32 : -32, y: offsetY }, entity.direction))
arrow.scale.set(0.75, 0.75)
arrow.rotation = entity.direction * Math.PI * 0.25
filterInfo.addChild(arrow)
}
filterInfo.scale.set(0.5, 0.5)
entityInfo.addChild(filterInfo)
}
if (entity.name === 'arithmetic-combinator' || entity.name === 'decider-combinator') {
const arrows = new PIXI.Container()
arrows.addChild(createArrow({ x: 0, y: -48 }), createArrow({ x: 0, y: 48 }))
arrows.rotation = entity.direction * Math.PI * 0.25
arrows.scale.set(0.5, 0.5)
entityInfo.addChild(arrows)
}
if (entity.type === 'mining-drill' && entity.name !== 'pumpjack') {
const arrows = new PIXI.Container()
arrows.addChild(createArrow({
x: entity.entityData.vector_to_place_result[0] * 64,
y: entity.entityData.vector_to_place_result[1] * 64 + 18
}))
arrows.rotation = entity.direction * Math.PI * 0.25
arrows.scale.set(0.5, 0.5)
entityInfo.addChild(arrows)
}
if (entity.name === 'pumpjack' || entity.name === 'pump' || entity.name === 'offshore-pump' || entity.type === 'boiler' ||
entity.type === 'generator' || entity.name === 'oil-refinery' || entity.name === 'chemical-plant' || entity.assemblerCraftsWithFluid
) {
const arrows = new PIXI.Container()
if (entity.entityData.fluid_boxes) {
if (entity.assemblerCraftsWithFluid) {
const c = entity.entityData.fluid_boxes[entity.assemblerPipeDirection === 'input' ? 1 : 0]
f({
x: c.pipe_connections[0].position[0],
y: c.pipe_connections[0].position[1]
})
} else {
const dontConnectOutput = entity.name === 'chemical-plant' && entity.chemicalPlantDontConnectOutput
for (const c of entity.entityData.fluid_boxes) {
// fluid_boxes are reversed
if (!(c.production_type === 'input' && dontConnectOutput)) {
f({
x: c.pipe_connections[0].position[0],
y: c.pipe_connections[0].position[1]
})
}
}
}
} else {
if (entity.entityData.fluid_box) {
for (const p of entity.entityData.fluid_box.pipe_connections) {
if (entity.name === 'pump' && p === entity.entityData.fluid_box.pipe_connections[1]) break
f({
x: p.position[0],
y: p.position[1]
}, entity.entityData.fluid_box.production_type === 'input-output' ? 2 : 1)
}
}
if (entity.entityData.output_fluid_box) {
for (const p of entity.entityData.output_fluid_box.pipe_connections) {
f({
x: p.position ? p.position[0] : p.positions[entity.direction / 2][0],
y: p.position ? p.position[1] : p.positions[entity.direction / 2][1]
})
}
}
}
function f(position: IPoint, type = 1) {
const offset = 0.5
if (entity.name === 'offshore-pump') position.y -= 2
const dir = Math.abs(position.x) > Math.abs(position.y) ?
(Math.sign(position.x) === 1 ? 2 : 6) :
(Math.sign(position.y) === 1 ? 4 : 0)
switch (dir) {
case 0: position.y += offset; break
case 2: position.x -= offset; break
case 4: position.y -= offset; break
case 6: position.x += offset
}
const arrow = createArrow({
x: position.x * 64,
y: position.y * 64
}, type)
if (entity.type === 'boiler' && type === 2) arrow.rotation = 0.5 * Math.PI
if (entity.name === 'pumpjack') arrow.rotation = entity.direction * Math.PI * 0.25
arrows.addChild(arrow)
}
if (entity.name !== 'pumpjack') {
arrows.rotation = (entity.name === 'oil-refinery' || entity.name === 'pump' || entity.type === 'boiler' ?
entity.direction : (entity.direction + 4) % 8) * Math.PI * 0.25
}
arrows.scale.set(0.5, 0.5)
entityInfo.addChild(arrows)
}
if (entityInfo.children.length !== 0) {
entityInfo.position.set(position.x, position.y)
this.overlay.addChild(entityInfo)
return entityInfo
}
function createIconWithBackground(container: PIXI.Container, itemName: string, position?: IPoint) {
const icon = InventoryContainer.createIcon(factorioData.getItem(itemName))
const background = PIXI.Sprite.fromFrame('extra-icon:entity-info-dark-background')
background.anchor.set(0.5, 0.5)
if (position) {
icon.position.set(position.x , position.y)
background.position.set(position.x , position.y)
}
const lastLength = container.children.length
container.addChild(background, icon)
if (lastLength !== 0) {
container.swapChildren(container.getChildAt(lastLength / 2), container.getChildAt(lastLength))
}
}
function createArrow(position: IPoint, type = 0) {
const arrow = PIXI.Sprite.fromFrame('extra-icon:' +
(type === 0 ? 'indication-arrow' : (type === 1 ? 'fluid-indication-arrow' : 'fluid-indication-arrow-both-ways'))
)
arrow.anchor.set(0.5, 0.5)
arrow.position.set(position.x , position.y)
return arrow
}
}
showCursorBox() {
this.cursorBox.visible = true
}
updateCursorBoxSize(width: number, height: number) {
this.cursorBox.removeChildren()
if (width === 1 && height === 1) {
const s = PIXI.Sprite.fromFrame('extra-icon:cursor-boxes-32x32-0')
s.anchor.set(0.5, 0.5)
this.cursorBox.addChild(s)
} else {
this.cursorBox.addChild(...createCorners(
'extra-icon:cursor-boxes-' + mapMinLengthToSpriteIndex(Math.min(width, height))
))
}
function mapMinLengthToSpriteIndex(minLength: number) {
if (minLength < 0.4) return '4'
if (minLength < 0.7) return '3'
if (minLength < 1.05) return '2'
if (minLength < 3.5) return '1'
return '0'
}
function createCorners(spriteName: string) {
const c0 = PIXI.Sprite.fromFrame(spriteName)
const c1 = PIXI.Sprite.fromFrame(spriteName)
const c2 = PIXI.Sprite.fromFrame(spriteName)
const c3 = PIXI.Sprite.fromFrame(spriteName)
c0.position.set(-width * 32, -height * 32)
c1.position.set(width * 32, -height * 32)
c2.position.set(-width * 32, height * 32)
c3.position.set(width * 32, height * 32)
c1.rotation = Math.PI * 0.5
c2.rotation = Math.PI * 1.5
c3.rotation = Math.PI
return [c0, c1, c2, c3]
}
}
updateCursorBoxPosition(position: IPoint) {
this.cursorBox.position.set(position.x, position.y)
}
hideCursorBox() {
this.cursorBox.visible = false
}
updateUndergroundLines(name: string, position: IPoint, direction: number, searchDirection: number) {
const fd = factorioData.getEntity(name)
if (fd.type === 'underground-belt' || name === 'pipe-to-ground') {
this.undergroundLines.removeChildren()
const otherEntity = G.bp.entityPositionGrid.findEntityWithSameNameAndDirection(
name,
name === 'pipe-to-ground' ? searchDirection : direction,
position,
searchDirection,
fd.max_distance || 10
)
if (otherEntity) {
const oE = G.bp.entity(otherEntity)
// Return if directionTypes are the same
if (fd.type === 'underground-belt' &&
(oE.directionType === 'input' ? oE.direction : (oE.direction + 4 % 8)) === searchDirection) return
const distance = searchDirection % 4 === 0 ? Math.abs(oE.position.y - position.y) :
Math.abs(oE.position.x - position.x)
const sign = searchDirection === 0 || searchDirection === 6 ? -1 : 1
for (let i = 1; i < distance; i++) {
const s = PIXI.Sprite.fromFrame('extra-icon:underground-lines-' + (name === 'pipe-to-ground' ? '0' : '1'))
s.rotation = direction * Math.PI * 0.25
s.scale.set(0.5, 0.5)
s.anchor.set(0.5, 0.5)
if (searchDirection % 4 === 0) s.position.y += sign * i * 32
else s.position.x += sign * i * 32
this.undergroundLines.addChild(s)
}
}
}
}
updateUndergroundLinesPosition(position: IPoint) {
this.undergroundLines.position.set(position.x, position.y)
}
hideUndergroundLines() {
this.undergroundLines.removeChildren()
}
}

257
src/containers/paint.ts Normal file
View File

@@ -0,0 +1,257 @@
import G from '../globals'
import util from '../util'
import factorioData from '../factorio-data/factorioData'
import { EntityContainer } from './entity'
import { AdjustmentFilter } from '@pixi/filter-adjustment'
import { UnderlayContainer } from './underlay'
export class PaintContainer extends PIXI.Container {
areaVisualization: PIXI.Sprite | PIXI.Sprite[] | undefined
holdingRightClick: boolean
directionType: string
direction: number
holdingLeftClick: boolean
filter: AdjustmentFilter
constructor(name: string, direction: number, position: IPoint) {
super()
this.name = name
this.direction = direction
this.directionType = 'input'
this.position.set(position.x, position.y)
this.filter = new AdjustmentFilter({ blue: 0.4 })
this.filters = [this.filter]
this.checkBuildable()
this.interactive = true
this.interactiveChildren = false
this.buttonMode = true
this.holdingLeftClick = false
this.areaVisualization = G.BPC.underlayContainer.createNewArea(this.name)
UnderlayContainer.modifyVisualizationArea(this.areaVisualization, s => {
s.alpha += 0.25
s.visible = true
})
G.BPC.underlayContainer.activateRelatedAreas(this.name)
this.on('pointerdown', this.pointerDownEventHandler)
this.on('pointerup', this.pointerUpEventHandler)
this.on('pointerupoutside', this.pointerUpEventHandler)
// this.on('pointermove', this.pointerMoveEventHandler)
this.redraw()
}
destroy() {
super.destroy()
UnderlayContainer.modifyVisualizationArea(this.areaVisualization, s => s.destroy())
G.BPC.underlayContainer.deactivateActiveAreas()
G.BPC.overlayContainer.hideUndergroundLines()
}
checkBuildable() {
const position = EntityContainer.getGridPosition(this.position)
const size = util.switchSizeBasedOnDirection(factorioData.getEntity(this.name).size, this.direction)
if (!EntityContainer.isContainerOutOfBpArea(position, size) &&
(G.bp.entityPositionGrid.checkFastReplaceableGroup(this.name, this.direction, position) ||
G.bp.entityPositionGrid.checkSameEntityAndDifferentDirection(this.name, this.direction, position) ||
G.bp.entityPositionGrid.checkNoOverlap(this.name, this.direction, position))
) {
this.filter.red = 0.4
this.filter.green = 1
} else {
this.filter.red = 1
this.filter.green = 0.4
}
}
updateUndergroundBeltRotation() {
const fd = factorioData.getEntity(this.name)
if (fd.type === 'underground-belt') {
const otherEntity = G.bp.entityPositionGrid.findEntityWithSameNameAndDirection(
this.name, (this.direction + 4) % 8, {
x: this.x / 32,
y: this.y / 32
},
this.direction,
fd.max_distance
)
if (otherEntity) {
const oe = G.bp.entity(otherEntity)
this.directionType = oe.directionType === 'input' ? 'output' : 'input'
} else {
if (this.directionType === 'output') this.directionType = 'input'
}
this.redraw()
}
}
updateUndergroundLines() {
G.BPC.overlayContainer.updateUndergroundLines(
this.name,
{ x: this.position.x / 32, y: this.position.y / 32 },
this.directionType === 'input' ? this.direction : (this.direction + 4) % 8,
this.name === 'pipe-to-ground' ? (this.direction + 4) % 8 : this.direction
)
}
rotate() {
const pr = factorioData.getEntity(this.name).possible_rotations
if (!pr) return
this.direction = pr[ (pr.indexOf(this.direction) + 1) % pr.length ]
this.redraw()
const size = util.switchSizeBasedOnDirection(factorioData.getEntity(this.name).size, this.direction)
if (size.x !== size.y) {
this.position.set(
this.x + ((this.x / 16 - G.gridCoords16.x) === 0 ? 0.5 : -0.5) * 32,
this.y + ((this.y / 16 - G.gridCoords16.y) === 0 ? 0.5 : -0.5) * 32
)
const pos = EntityContainer.getPositionFromData(this.position, size)
this.position.set(pos.x, pos.y)
}
this.checkBuildable()
this.updateUndergroundBeltRotation()
this.updateUndergroundLines()
}
redraw() {
this.removeChildren()
this.addChild(...EntityContainer.getParts({
name: this.name,
direction: this.directionType === 'output' ? (this.direction + 4) % 8 : this.direction,
directionType: this.directionType
}, true, true))
const size = util.switchSizeBasedOnDirection(factorioData.getEntity(this.name).size, this.direction)
this.hitArea = new PIXI.Rectangle(
-size.x * 16,
-size.y * 16,
size.x * 32,
size.y * 32
)
}
pointerDownEventHandler(e: PIXI.interaction.InteractionEvent) {
if (e.data.button === 0) {
this.holdingLeftClick = true
this.placeEntityContainer()
} else if (e.data.button === 2) {
this.holdingRightClick = true
this.removeContainer()
}
}
pointerUpEventHandler(e: PIXI.interaction.InteractionEvent) {
if (e.data.button === 0) {
this.holdingLeftClick = false
} else if (e.data.button === 2) {
this.holdingRightClick = false
}
}
// pointerMoveEventHandler(e: PIXI.interaction.InteractionEvent) {
// this.moveTo(e.data.getLocalPosition(this.parent))
// }
moveTo(newPosition: IPoint) {
const newCursorPos = {
x: (newPosition.x - newPosition.x % 16) / 16,
y: (newPosition.y - newPosition.y % 16) / 16
}
if (newCursorPos.x !== G.gridCoords16.x || newCursorPos.y !== G.gridCoords16.y) {
if (this.holdingRightClick) this.removeContainer()
switch (this.name) {
case 'straight-rail':
case 'curved-rail':
case 'train-stop':
this.position.set(
newPosition.x - (newPosition.x + G.railMoveOffset.x * 32) % 64 + 32,
newPosition.y - (newPosition.y + G.railMoveOffset.y * 32) % 64 + 32
)
break
default:
const size = util.switchSizeBasedOnDirection(factorioData.getEntity(this.name).size, this.direction)
const pos = EntityContainer.getPositionFromData(newPosition, size)
this.position.set(pos.x, pos.y)
}
this.updateUndergroundBeltRotation()
G.BPC.overlayContainer.updateUndergroundLinesPosition(this.position)
this.updateUndergroundLines()
UnderlayContainer.modifyVisualizationArea(this.areaVisualization, s => s.position.copy(this.position))
if (this.holdingLeftClick) this.placeEntityContainer()
G.gridCoords16 = newCursorPos
this.checkBuildable()
}
}
removeContainer() {
const position = EntityContainer.getGridPosition(this.position)
const c = EntityContainer.mappings.get(G.bp.entityPositionGrid.getCellAtPosition(position))
if (c) {
c.removeContainer()
this.checkBuildable()
}
}
placeEntityContainer() {
const fd = factorioData.getEntity(this.name)
const position = EntityContainer.getGridPosition(this.position)
const size = util.switchSizeBasedOnDirection(fd.size, this.direction)
if (EntityContainer.isContainerOutOfBpArea(position, size)) return
const frgEntNr = G.bp.entityPositionGrid.checkFastReplaceableGroup(this.name, this.direction, position)
if (frgEntNr) {
const frgEnt = G.bp.entity(frgEntNr)
frgEnt.change(this.name, this.direction)
const c = EntityContainer.mappings.get(frgEntNr)
c.redraw()
c.redrawSurroundingEntities()
return
}
const snEntNr = G.bp.entityPositionGrid.checkSameEntityAndDifferentDirection(
this.name, this.direction, position
)
if (snEntNr) {
G.bp.entity(snEntNr).direction = this.direction
const c = EntityContainer.mappings.get(snEntNr)
c.redraw()
c.redrawSurroundingEntities()
return
}
const isUB = fd.type === 'underground-belt'
const res = G.bp.createEntity(this.name, position,
isUB && this.directionType === 'output' ? (this.direction + 4) % 8 : this.direction,
isUB ? this.directionType : undefined
)
if (res) {
const ec = new EntityContainer(res)
if (ec.areaVisualization) {
if (ec.areaVisualization instanceof PIXI.Sprite) {
ec.areaVisualization.visible = true
} else {
for (const s of ec.areaVisualization) s.visible = true
}
}
G.BPC.entities.addChild(ec)
ec.redrawSurroundingEntities()
if (isUB || this.name === 'pipe-to-ground') {
this.direction = (this.direction + 4) % 8
this.redraw()
G.BPC.overlayContainer.hideUndergroundLines()
}
G.BPC.updateOverlay()
}
this.checkBuildable()
}
}

59
src/containers/toolbar.ts Normal file
View File

@@ -0,0 +1,59 @@
import G from '../globals'
export class ToolbarContainer extends PIXI.Container {
info: PIXI.Text
logo: PIXI.Text
fpsGUIText: PIXI.Text
gridposGUIText: PIXI.Text
constructor() {
super()
this.interactive = false
this.interactiveChildren = false
const background = new PIXI.Sprite(PIXI.Texture.WHITE)
background.width = G.app.renderer.width
window.addEventListener('resize', () => {
background.width = G.app.renderer.width
this.fpsGUIText.position.set(G.app.renderer.width, background.height / 2)
this.logo.position.set(G.app.renderer.width / 2, background.height / 2)
this.info.position.set(G.app.renderer.width - 100, background.height)
}, false)
background.height = 32
background.tint = 0x303030
this.addChild(background)
this.gridposGUIText = new PIXI.Text('')
this.gridposGUIText.anchor.set(0, 0.5)
this.gridposGUIText.position.set(0, background.height / 2)
this.gridposGUIText.style.fill = 0xFFFFFF
this.addChild(this.gridposGUIText)
this.fpsGUIText = new PIXI.Text('')
this.fpsGUIText.anchor.set(1, 0.5)
this.fpsGUIText.style.fill = 0xFFFFFF
this.fpsGUIText.position.set(G.app.renderer.width, background.height / 2)
this.addChild(this.fpsGUIText)
this.logo = new PIXI.Text('Factorio Blueprint Editor')
this.logo.anchor.set(0.5, 0.5)
this.logo.style.fill = 0xFFFFFF
this.logo.position.set(G.app.renderer.width / 2, background.height / 2)
this.addChild(this.logo)
this.info = new PIXI.Text('Press I for info')
this.info.anchor.set(1, 1)
this.info.style.fill = 0xFFFFFF
this.info.style.fontSize = 13
this.info.position.set(G.app.renderer.width - 100, background.height)
this.addChild(this.info)
G.app.ticker.add(() => this.fpsGUIText.text = String(Math.round(G.app.ticker.FPS)) + ' FPS')
}
updateGridPos(coords: IPoint) {
this.gridposGUIText.text = `X ${coords.x} Y ${coords.y}`
}
}

136
src/containers/underlay.ts Normal file
View File

@@ -0,0 +1,136 @@
import factorioData from '../factorio-data/factorioData'
import { isArray } from 'util'
import { AdjustmentFilter } from '@pixi/filter-adjustment'
export class UnderlayContainer extends PIXI.Container {
static getDataForVisualizationArea(name: string) {
const type = factorioData.getEntity(name).type
function undoBlendModeColorShift(color0: number, color1: number, alpha: number) {
// https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/blendFunc
// array[BLEND_MODES.NORMAL] = [gl.ONE, gl.ONE_MINUS_SRC_ALPHA]
return color1 - color0 * (1 - alpha)
}
if (name === 'roboport') {
return {
type: ['logistics0', 'logistics1'],
rKey: ['construction_radius', 'logistics_radius'],
color: [0x83D937, undoBlendModeColorShift(0xFF8800, 0x83D937, 0.25)]
}
}
if (type === 'electric-pole') {
return {
type: 'poles',
rKey: 'supply_area_distance',
color: 0x33755D9
}
}
if (name === 'beacon') {
return {
type: 'beacons',
rKey: 'supply_area_distance',
color: 0xD9C037
}
}
if (name === 'electric-mining-drill') {
return {
type: 'drills',
rKey: 'resource_searching_radius',
color: 0x4EAD9F
}
}
}
static modifyVisualizationArea(area: PIXI.Sprite | PIXI.Sprite[] | undefined, fn: (s: PIXI.Sprite) => void) {
if (area) {
if (area instanceof PIXI.Sprite) fn(area)
else for (const s of area) fn(s)
}
}
active: string[]
logistics0: PIXI.Container
logistics1: PIXI.Container
poles: PIXI.Container
beacons: PIXI.Container
drills: PIXI.Container
constructor() {
super()
this.interactive = false
this.interactiveChildren = false
this.active = []
this.logistics0 = new PIXI.Container()
this.logistics1 = new PIXI.Container()
this.poles = new PIXI.Container()
this.beacons = new PIXI.Container()
this.drills = new PIXI.Container()
const filter = new AdjustmentFilter({ alpha: 0.25 })
this.logistics0.filters = [filter]
this.logistics1.filters = [filter]
this.addChild(this.logistics0, this.logistics1, this.poles, this.beacons, this.drills)
}
activateRelatedAreas(entityName: string) {
const ed = factorioData.getEntity(entityName)
const data = UnderlayContainer.getDataForVisualizationArea(entityName)
if (data) if (isArray(data.type)) this.active.push(...data.type); else this.active.push(data.type)
if (ed.type === 'logistic-container') this.active.push('logistics0', 'logistics1')
if (ed.energy_source && ed.energy_source.type === 'electric') this.active.push('poles')
if (ed.module_specification) this.active.push('beacons')
for (const type of this.active) {
for (const s of this[type].children) {
s.visible = true
}
}
}
deactivateActiveAreas() {
for (const type of this.active) {
for (const s of this[type].children) {
s.visible = false
}
}
this.active = []
}
createNewArea(entityName: string, position?: PIXI.Point | PIXI.ObservablePoint) {
const aVData = UnderlayContainer.getDataForVisualizationArea(entityName)
if (aVData) {
const ed = factorioData.getEntity(entityName)
if (isArray(aVData.type)) {
const aVs = []
for (let i = 0; i < aVData.type.length; i++) {
const areaVisualization = createVisualizationArea(ed[aVData.rKey[i]], aVData.color[i], position, 1)
this[aVData.type[i]].addChild(areaVisualization)
aVs.push(areaVisualization)
}
return aVs
} else {
const areaVisualization = createVisualizationArea(ed[aVData.rKey], aVData.color, position)
this[aVData.type].addChild(areaVisualization)
return areaVisualization
}
}
function createVisualizationArea(radius: number, color: number, position?: PIXI.Point | PIXI.ObservablePoint, alpha = 0.25) {
const aV = new PIXI.Sprite(PIXI.Texture.WHITE)
const S = radius * 64
aV.width = S
aV.height = S
aV.tint = color
aV.anchor.set(0.5, 0.5)
aV.alpha = alpha
if (position) {
aV.visible = false
aV.position.copy(position)
}
return aV
}
}
}

111
src/containers/wires.ts Normal file
View File

@@ -0,0 +1,111 @@
import { EntityContainer } from './entity'
import G from '../globals'
export class WiresContainer extends PIXI.Container {
static createWire(p1: IPoint, p2: IPoint, color: string) {
const wire = new PIXI.Graphics()
if (color === 'red') {
wire.lineStyle(1.3, 0xC83718)
} else {
wire.lineStyle(1.3, 0x588C38)
}
const force = 0.25
const minX = Math.min(p1.x, p2.x)
const minY = Math.min(p1.y, p2.y)
const dX = Math.max(p1.x, p2.x) - minX
const dY = Math.max(p1.y, p2.y) - minY
const X = minX + dX / 2
const Y = (dY / dX) * (X - minX) + minY + force * dX
wire.moveTo(p1.x, p1.y)
// TODO: make wires smoother, use 2 points instead of 1
if (p1.x === p2.x) {
wire.lineTo(p2.x, p2.y)
} else {
wire.bezierCurveTo(X, Y, X, Y, p2.x, p2.y)
}
return wire
}
static getFinalPos(entity_number: number, color: string, side: number) {
const point = G.bp.entity(entity_number).getWireConnectionPoint(color, side)
return {
x: EntityContainer.mappings.get(entity_number).position.x + point[0] * 32,
y: EntityContainer.mappings.get(entity_number).position.y + point[1] * 32
}
}
entityWiresMapping: Map<string, PIXI.Graphics[]>
constructor() {
super()
this.interactive = false
this.interactiveChildren = false
this.entityWiresMapping = new Map()
}
remove(entity_number: number) {
this.entityWiresMapping.forEach((v, k) => {
const first = Number(k.split('-')[0])
const second = Number(k.split('-')[1])
if (first === entity_number || second === entity_number) {
for (const g of v) g.destroy()
this.entityWiresMapping.delete(k)
}
})
}
update(entity_number: number) {
if (!G.bp.entity(entity_number).hasConnections) return
this.remove(entity_number)
G.bp.connections.connections.forEach((v, k) => {
const first = Number(k.split('-')[0])
const second = Number(k.split('-')[1])
if (first === entity_number || second === entity_number) {
if (first === entity_number) EntityContainer.mappings.get(second).redraw()
else if (second === entity_number) EntityContainer.mappings.get(first).redraw()
const paths: PIXI.Graphics[] = []
v.forEach(c => {
paths.push(WiresContainer.createWire(
WiresContainer.getFinalPos(c.get('entity_number_1') as number, c.get('color') as string, c.get('entity_side_1') as number),
WiresContainer.getFinalPos(c.get('entity_number_2') as number, c.get('color') as string, c.get('entity_side_2') as number),
c.get('color') as string
))
})
for (const p of paths) {
this.addChild(p)
}
this.entityWiresMapping.set(k, paths)
}
})
}
drawWires() {
G.bp.connections.connections.forEach((v, k) => {
if (this.entityWiresMapping.has(k)) {
for (const p of this.entityWiresMapping.get(k)) {
this.removeChild(p)
}
}
const paths: PIXI.Graphics[] = []
v.forEach(c => {
paths.push(WiresContainer.createWire(
WiresContainer.getFinalPos(c.get('entity_number_1') as number, c.get('color') as string, c.get('entity_side_1') as number),
WiresContainer.getFinalPos(c.get('entity_number_2') as number, c.get('color') as string, c.get('entity_side_2') as number),
c.get('color') as string
))
})
for (const p of paths) {
this.addChild(p)
}
this.entityWiresMapping.set(k, paths)
})
}
}

83
src/entitySprite.ts Normal file
View File

@@ -0,0 +1,83 @@
import G from './globals'
import { AdjustmentFilter } from '@pixi/filter-adjustment'
export class EntitySprite extends PIXI.Sprite {
static nextID = 0
id: number
isMoving: boolean
shift: IPoint
zIndex: number
zOrder: number
constructor(data: any) {
if (!data.shift) data.shift = [0, 0]
if (!data.x) data.x = 0
if (!data.y) data.y = 0
if (!data.divW) data.divW = 1
if (!data.divH) data.divH = 1
const spriteData = PIXI.utils.TextureCache[data.filename]
// TODO: Cache the texture
super(new PIXI.Texture(spriteData.baseTexture, new PIXI.Rectangle(
spriteData.frame.x + data.x,
spriteData.frame.y + data.y,
data.width / data.divW,
data.height / data.divH
)))
this.interactive = false
this.id = EntitySprite.nextID++
this.isMoving = false
this.shift = {
x: data.shift[0] * 32,
y: data.shift[1] * 32
}
this.position.set(this.shift.x, this.shift.y)
if (data.scale) this.scale.set(data.scale, data.scale)
this.anchor.set(0.5, 0.5)
if (data.flipX) this.scale.x *= -1
if (data.flipY) this.scale.y *= -1
if (data.height_divider) this.height /= data.height_divider
if (data.rot) this.rotation = data.rot * Math.PI * 0.5
if (data.color) {
this.filters = [new AdjustmentFilter({
gamma: 1.4,
contrast: 1.4,
brightness: 1.2,
red: data.color.r,
green: data.color.g,
blue: data.color.b,
alpha: data.color.a
})]
}
return this
}
set moving(moving: boolean) {
this.isMoving = moving
if (moving) {
if (this.filters !== null) this.filters.push(G.BPC.movingEntityFilter)
else this.filters = [G.BPC.movingEntityFilter]
} else {
// tslint:disable-next-line:no-null-keyword
if (this.filters.length === 1) this.filters = null
else this.filters.pop()
}
}
setPosition(position: PIXI.Point | PIXI.ObservablePoint) {
this.position.set(
position.x + this.shift.x,
position.y + this.shift.y
)
}
}

View File

@@ -0,0 +1,67 @@
import zlib from 'zlib'
import { Buffer } from 'buffer'
import Ajv from 'ajv'
import blueprintSchema from '../blueprintSchema.json'
import factorioData from './factorioData'
import { Blueprint } from './blueprint'
import { Book } from './book'
const ajv = new Ajv()
ajv.addKeyword('entityName', {
validate: (data: string) => factorioData.checkEntityName(data),
errors: false,
schema: false
})
ajv.addKeyword('itemName', {
validate: (data: string) => factorioData.checkItemName(data),
errors: false,
schema: false
})
ajv.addKeyword('objectWithItemNames', {
validate: (data: object) => {
for (const k in data) {
if (!factorioData.checkItemName(k)) return false
}
return true
},
errors: false,
schema: false
})
ajv.addKeyword('recipeName', {
validate: (data: string) => factorioData.checkRecipeName(data),
errors: false,
schema: false
})
ajv.addKeyword('tileName', {
validate: (data: string) => factorioData.checkTileName(data),
errors: false,
schema: false
})
const validate = ajv.compile(blueprintSchema)
export default {
decode: (str: string) => {
let data
try {
data = JSON.parse(zlib.inflateSync(Buffer.from(str.slice(1), 'base64')).toString('utf8'))
} catch (e) {
return { error: e }
}
console.log(data)
// data.blueprint.entities.forEach(e => {
// // if (e.control_behavior) {
// // let d = e.control_behavior.circuit_condition
// // if (d !== undefined) console.log(e.name + '\t\t\t\t' + d)
// // }
// if (e.filters !== undefined) console.log(e.name + '\t\t\t\t' + e.filters)
// });
if (!validate(data)) return { error: validate.errors }
if (data.blueprint_book === undefined) return new Blueprint(data.blueprint)
return new Book(data)
},
encode: (bPOrBook: any) => ('0' + zlib.deflateSync(JSON.stringify(bPOrBook.toObject())).toString('base64'))
}

View File

@@ -0,0 +1,403 @@
import factorioData from './factorioData'
import { Tile } from './tile'
import { PositionGrid } from './positionGrid'
import Immutable from 'immutable'
import initEntity from './entity'
import G from '../globals'
import { ConnectionsManager } from './connectionsManager'
import { EntityContainer } from '../containers/entity'
export class Blueprint {
name: string
icons: any[]
tiles: Tile[]
tilePositionGrid: any
version: number
connections: ConnectionsManager
next_entity_number: number
historyIndex: number
history: Array<{
entity_number: number | number[];
type: 'init' | 'add' | 'del' | 'mov' | 'upd';
annotation: string;
rawEntities: Immutable.Map<number, any>;
}>
bp: Blueprint
entityPositionGrid: PositionGrid
rawEntities: Immutable.Map<number, any>
constructor(data?: any) {
initEntity(this)
this.name = 'Blueprint'
this.icons = []
this.rawEntities = Immutable.Map()
this.tiles = []
this.tilePositionGrid = {}
this.version = undefined
this.next_entity_number = 1
if (data) {
if (!data.tiles) data.tiles = []
if (!data.icons) data.icons = []
this.name = data.label
this.version = data.version
this.next_entity_number += data.entities.length
this.rawEntities = this.rawEntities.withMutations(map => {
for (const entity of data.entities) {
map.set(entity.entity_number, Immutable.fromJS(entity))
// this.entityPositionGrid.setTileData(entity.entity_number)
}
})
data.tiles.forEach((tile: any) => {
this.createTile(tile.name, tile.position)
})
this.icons = []
data.icons.forEach((icon: any) => {
this.icons[icon.index - 1] = icon.signal.name
})
this.setTileIds()
// TODO: if entity has placeable-off-grid flag then take the next one
const firstEntityTopLeft = this.entity(1).topLeft()
const offsetX = G.sizeBPContainer.width / 64 - (firstEntityTopLeft.x % 1 !== 0 ? -0.5 : 0)
const offsetY = G.sizeBPContainer.height / 64 - (firstEntityTopLeft.y % 1 !== 0 ? -0.5 : 0)
this.rawEntities = this.rawEntities.withMutations(map => {
map.keySeq().forEach(k => {
// tslint:disable-next-line:no-parameter-reassignment
map.updateIn([k, 'position', 'x'], (x: number) => x += offsetX)
// tslint:disable-next-line:no-parameter-reassignment
map.updateIn([k, 'position', 'y'], (y: number) => y += offsetY)
})
})
// tslint:disable-next-line:no-dynamic-delete
this.tiles.forEach(tile => delete this.tilePositionGrid[`${tile.position.x},${tile.position.y}`])
this.tiles.forEach(tile => {
tile.position.x += offsetX
tile.position.y += offsetY
this.tilePositionGrid[`${tile.position.x},${tile.position.y}`] = tile
})
}
this.entityPositionGrid = new PositionGrid(this, [...this.rawEntities.keys()])
this.connections = new ConnectionsManager(this, [...this.rawEntities.keys()])
this.historyIndex = 0
this.history = [{
entity_number: 0,
type: 'init',
annotation: '',
rawEntities: this.rawEntities
}]
return this
}
entity(entity_number: number) {
const e = this.rawEntities.get(entity_number)
if (!e) return undefined
return e.entity()
}
undo(
pre: (hist: any) => void,
post: (hist: any) => void
) {
if (this.historyIndex === 0) return
const hist = this.history[this.historyIndex--]
switch (hist.type) {
case 'add':
case 'del':
case 'mov':
this.entityPositionGrid.undo()
}
pre(hist)
this.rawEntities = this.history[this.historyIndex].rawEntities
switch (hist.type) {
case 'del':
if (this.entity(hist.entity_number as number).hasConnections) this.connections.undo()
}
post(hist)
}
redo(
pre: (hist: any) => void,
post: (hist: any) => void
) {
if (this.historyIndex === this.history.length - 1) return
const hist = this.history[++this.historyIndex]
switch (hist.type) {
case 'add':
case 'del':
case 'mov':
this.entityPositionGrid.redo()
}
pre(hist)
const entity = this.entity(hist.entity_number as number)
switch (hist.type) {
case 'del':
if (entity.hasConnections) this.connections.redo()
}
this.rawEntities = hist.rawEntities
// TODO: Refactor this somehow
if (hist.type === 'del' && entity.hasConnections && entity.connectedEntities) {
for (const entNr of entity.connectedEntities) {
EntityContainer.mappings.get(entNr).redraw()
}
}
post(hist)
}
operation(
entity_number: number | number[],
annotation: string,
fn: (entities: Immutable.Map<number, any>) => Immutable.Map<any, any>,
type: 'add' | 'del' | 'mov' | 'upd' = 'upd',
pushToHistory = true
) {
console.log(`${entity_number} - ${annotation}`)
this.rawEntities = fn(this.rawEntities)
if (pushToHistory) {
if (this.historyIndex < this.history.length) {
this.history = this.history.slice(0, this.historyIndex + 1)
}
this.history.push({
entity_number,
type,
annotation,
rawEntities: this.rawEntities
})
this.historyIndex++
}
}
createEntity(name: string, position: IPoint, direction: number, directionType?: string) {
if (!this.entityPositionGrid.checkNoOverlap(name, direction, position)) return false
const entity_number = this.next_entity_number++
const data = {
entity_number,
name,
position,
direction
}
if (directionType) data.type = directionType
this.operation(entity_number, 'Added new entity',
entities => entities.set(entity_number, Immutable.fromJS(data)),
'add'
)
this.entityPositionGrid.setTileData(entity_number)
return data.entity_number
}
removeEntity(entity_number: number, redrawCb?: (entity_number: number) => void) {
this.entityPositionGrid.removeTileData(entity_number)
let entitiesToModify: any[] = []
if (this.entity(entity_number).hasConnections) {
entitiesToModify = this.connections.removeConnectionData(entity_number)
}
this.operation(entity_number, 'Deleted entity',
entities => entities.withMutations(map => {
for (const i in entitiesToModify) {
const entity_number = entitiesToModify[i].entity_number
const side = entitiesToModify[i].side
const color = entitiesToModify[i].color
const index = entitiesToModify[i].index
const a = map.getIn([entity_number, 'connections']).size === 1
const b = map.getIn([entity_number, 'connections', side]).size === 1
const c = map.getIn([entity_number, 'connections', side, color]).size === 1
if (a && b && c) {
map.removeIn([entity_number, 'connections'])
} else if (b && c) {
map.removeIn([entity_number, 'connections', side])
} else if (c) {
map.removeIn([entity_number, 'connections', side, color])
} else {
map.removeIn([entity_number, 'connections', side, color, index])
}
}
map.delete(entity_number)
}),
'del'
)
for (const i in entitiesToModify) {
redrawCb(entitiesToModify[i].entity_number)
}
}
getFirstRail() {
const fR = this.rawEntities.find(v => v.get('name') === 'straight-rail' || v.get('name') === 'curved-rail')
return fR ? fR.toJS() : undefined
}
// placeBlueprint(bp, position, direction = 0, allowOverlap) { // direction is 0, 1, 2, or 3
// const entitiesCreated = []
// bp.entities.forEach(ent => {
// const data = ent.getData()
// data.direction += direction * 2
// data.direction %= 8
// if (direction === 3) data.position = { x: data.position.y, y: -data.position.x }
// else if (direction === 2) data.position = { x: -data.position.x, y: -data.position.y }
// else if (direction === 1) data.position = { x: -data.position.y, y: data.position.x }
// data.position.x += position.x
// data.position.y += position.y
// entitiesCreated.push(this.createEntityWithData(data, allowOverlap, true, true))
// })
// entitiesCreated.forEach(e => {
// e.place(this.entitiesCreated)
// })
// bp.tiles.forEach(tile => {
// const data = tile.getData()
// if (direction === 3) data.position = { x: data.position.y, y: -data.position.x }
// else if (direction === 2) data.position = { x: -data.position.x, y: -data.position.y }
// else if (direction === 1) data.position = { x: -data.position.y, y: data.position.x }
// data.position.x += position.x
// data.position.y += position.y
// this.createTileWithData(data)
// })
// return this
// }
// createEntityWithData(data: any, allowOverlap: boolean, noPlace: boolean) {
// const ent = new Entity(data, this)
// if (allowOverlap || this.entityPositionGrid.checkNoOverlap(ent)) {
// if (!noPlace) ent.place(this.entities)
// this.entities.push(ent)
// return ent
// } else {
// // const otherEnt = ent.getOverlap(this.entityPositionGrid)
// // throw new Error('Entity ' + data.name + ' overlaps ' + otherEnt.name +
// // ' entity (' + data.position.x + ', ' + data.position.y + ')')
// }
// }
createTile(name: string, position: IPoint) {
return this.createTileWithData({ name, position })
}
createTileWithData(data: any) {
const tile = new Tile(data, this)
const key = `${data.position.x},${data.position.y}`
if (this.tilePositionGrid[key]) this.removeTile(this.tilePositionGrid[key])
this.tilePositionGrid[key] = tile
this.tiles.push(tile)
return tile
}
removeTile(tile: Tile) {
if (!tile) return false
else {
const index = this.tiles.indexOf(tile)
if (index === -1) return tile
this.tiles.splice(index, 1)
return tile
}
}
setTileIds() {
this.tiles.forEach((tile, i) => {
tile.id = i + 1
})
return this
}
// Get corner/center positions
getPosition(f: string, xcomp: any, ycomp: any) {
if (!this.rawEntities.size) return { x: 0, y: 0 }
return {
x: [...this.rawEntities.keys()].reduce(
(best: number, ent: any) => xcomp(best, this.entity(ent)[f]().x),
this.entity(1)[f]().x
),
y: [...this.rawEntities.keys()].reduce(
(best: number, ent: any) => ycomp(best, this.entity(ent)[f]().y),
this.entity(1)[f]().y
)
}
}
center() {
return {
x: (this.topLeft().x + this.topRight().x) / 2,
y: (this.topLeft().y + this.bottomLeft().y) / 2
}
}
topLeft() { return this.getPosition('topLeft', Math.min, Math.min) }
topRight() { return this.getPosition('topRight', Math.max, Math.min) }
bottomLeft() { return this.getPosition('bottomLeft', Math.min, Math.max) }
bottomRight() { return this.getPosition('bottomRight', Math.max, Math.max) }
generateIcons(num = 2) {
// TODO: make this behave more like in factorio 2 icons default, the 2 most used items in the bp
const NUM = Math.min(this.rawEntities.size, Math.min(Math.max(num, 1), 4))
for (let i = 0; i < NUM; i++) {
this.icons[i] = this.rawEntities[i].name
}
return this
}
toObject() {
this.setTileIds()
// if (!this.icons.length) this.generateIcons()
const entityInfo = this.rawEntities.valueSeq().toJS()
const center = this.center()
const fR = this.getFirstRail()
if (fR) {
center.x += (fR.position.x - center.x) % 2
center.y += (fR.position.y - center.y) % 2
}
for (const e of entityInfo) {
e.position.x -= center.x
e.position.y -= center.y
}
const tileInfo = this.tiles.map(tile => tile.getData())
for (const t of tileInfo) {
t.position.x -= center.x
t.position.y -= center.y
}
const iconData = this.icons.map((icon, i) => (
{ signal: { type: factorioData.getItemTypeForBp(icon), name: icon }, index: i + 1 }
))
return {
blueprint: {
icons: iconData,
entities: this.rawEntities.size ? entityInfo : undefined,
tiles: this.tiles.length ? tileInfo : undefined,
item: 'blueprint',
version: this.version || 0,
label: this.name
}
}
}
}

53
src/factorio-data/book.ts Normal file
View File

@@ -0,0 +1,53 @@
import { Blueprint } from './blueprint'
export class Book {
active_index: number
blueprints: any[]
constructor(data: any) {
if (data) {
this.active_index = data.blueprint_book.active_index
this.blueprints = data.blueprint_book.blueprints
} else {
this.active_index = 0
this.blueprints = []
}
}
addBlueprint(blueprint: Blueprint) {
this.blueprints.push(blueprint)
}
getBlueprint(index?: number) {
const INDEX = index ? index : this.active_index
this.active_index = INDEX
if (this.blueprints[INDEX].loaded) return this.blueprints[INDEX].loaded
const bp = new Blueprint(this.blueprints[INDEX].blueprint)
this.blueprints[INDEX].loaded = bp
return bp
}
toObject() {
const blueprints = []
for (let i = 0; i < this.blueprints.length; i++) {
blueprints.push({
index: i,
// TODO: modified instead of loaded
blueprint: this.blueprints[i].loaded ?
this.blueprints[i].loaded.toObject() :
this.blueprints[i].blueprint
})
}
return {
blueprint_book: {
blueprints,
item: 'blueprint-book',
active_index: this.active_index,
version: 0
}
}
}
}

View File

@@ -0,0 +1,132 @@
import { Blueprint } from './blueprint'
import Immutable from 'immutable'
export class ConnectionsManager {
bp: Blueprint
connections: Immutable.Map<string, Immutable.List<Immutable.Map<
'color' | 'circuit_id' | 'entity_number_1' | 'entity_number_2' | 'entity_side_1' | 'entity_side_2'
, string | number | undefined>>>
historyIndex: number
history: Array<Immutable.Map<string, Immutable.List<Immutable.Map<
'color' | 'circuit_id' | 'entity_number_1' | 'entity_number_2' | 'entity_side_1' | 'entity_side_2'
, string | number | undefined>>>>
constructor(bp: Blueprint, entity_numbers?: number[]) {
this.bp = bp
this.connections = Immutable.Map()
// Set Bulk
if (entity_numbers) {
this.connections = this.connections.withMutations(map => {
const connections = new Map()
for (const entity_number of entity_numbers) {
const entity = this.bp.entity(entity_number)
if (entity.hasConnections) connections.set(entity_number, entity.connections)
}
connections.forEach((conn, k) => {
for (const side in conn) {
for (const color in conn[side]) {
for (const c of conn[side][color]) {
if (!map.has(`${(c as any).entity_id}-${k}`)) {
let side2
const conn2 = connections.get((c as any).entity_id)
let found = false
for (side2 in conn2) {
for (const color2 in conn2[side2]) {
for (const c2 of conn2[side2][color2]) {
if (color === color2 && (c2 as any).entity_id === k) {
found = true
break
}
}
if (found) break
}
if (found) break
}
const key = `${k}-${(c as any).entity_id}`
if (!map.has(key)) map.set(key, Immutable.List())
map.set(key, map.get(key).push(Immutable.fromJS({
color,
circuit_id: (c as any).circuit_id,
entity_number_1: k,
entity_number_2: (c as any).entity_id,
entity_side_1: Number(side),
entity_side_2: Number(side2)
})))
}
}
}
}
})
})
}
this.history = [this.connections]
this.historyIndex = 0
}
undo() {
if (this.historyIndex === 0) return
this.connections = this.history[--this.historyIndex]
}
redo() {
if (this.historyIndex === this.history.length - 1) return
this.connections = this.history[++this.historyIndex]
}
operation(
fn: (connections: Immutable.Map<string, Immutable.List<Immutable.Map<
'color' | 'circuit_id' | 'entity_number_1' | 'entity_number_2' | 'entity_side_1' | 'entity_side_2'
, string | number | undefined>>>) => Immutable.Map<any, any>
) {
this.connections = fn(this.connections)
if (this.historyIndex < this.history.length) {
this.history = this.history.slice(0, this.historyIndex + 1)
}
this.history.push(this.connections)
this.historyIndex++
}
removeConnectionData(entity_number: number) {
const entitiesToModify: Array<{
entity_number: number;
side: string;
color: string;
index: number;
}> = []
this.operation(connections => connections.withMutations(map => {
map.forEach((v, k) => {
const isE1 = Number(k.split('-')[0]) === entity_number
const isE2 = Number(k.split('-')[1]) === entity_number
if (isE1 || isE2) {
v.forEach(conn => {
const entNr2 = (isE1 ? conn.get('entity_number_2') : conn.get('entity_number_1')) as number
const conn2 = this.bp.entity(entNr2).connections
for (const side in conn2) {
for (const color in conn2[side]) {
for (const i in conn2[side][color]) {
if (entity_number === conn2[side][color][i].entity_id &&
Number(side) === (isE1 ? conn.get('entity_side_2') : conn.get('entity_side_1')) &&
color === conn.get('color')
) {
entitiesToModify.push({
entity_number: entNr2,
side,
color,
index: Number(i)
})
}
}
}
}
map.deleteIn([k, map.get(k).indexOf(conn)])
})
}
if (map.get(k).size === 0) map.delete(k)
})
}))
return entitiesToModify
}
}

324
src/factorio-data/entity.ts Normal file
View File

@@ -0,0 +1,324 @@
import { Blueprint } from './blueprint'
import Immutable from 'immutable'
import factorioData from './factorioData'
import util from '../util'
import { Area } from './positionGrid'
export default (BP: Blueprint) => {
Immutable.Map.prototype.entity = function() {
// tslint:disable-next-line:no-this-assignment
const rawEntity = this
return {
get entity_number() { return rawEntity.get('entity_number') },
get name() { return rawEntity.get('name') },
get type() { return factorioData.getEntity(this.name).type },
get entityData() { return factorioData.getEntity(this.name) },
get recipeData() { return factorioData.getRecipe(this.name) },
get itemData() { return factorioData.getItem(this.name) },
get size() { return util.switchSizeBasedOnDirection(this.entityData.size, this.direction) },
get position() { return rawEntity.get('position').toJS() },
get direction() { return rawEntity.get('direction') || 0 },
get directionType() { return rawEntity.get('type') },
get recipe() { return rawEntity.get('recipe') },
set recipe(recipeName: string) {
BP.operation(this.entity_number, 'Changed recipe', entities => (
entities.withMutations(map => {
map.setIn([this.entity_number, 'recipe'], recipeName)
const modules = this.modules
if (modules && recipeName && !factorioData.getItem('productivity-module').limitation.includes(recipeName)) {
for (const k in modules) {
if (k.includes('productivity-module')) delete modules[k]
}
map.setIn([this.entity_number, 'items'], Object.keys(modules).length ? Immutable.fromJS(modules) : undefined)
}
})
))
},
get acceptedRecipes() {
const acceptedRecipes: string[] = []
const recipes = factorioData.getRecipes()
const cc = this.entityData.crafting_categories
for (const k in recipes) {
if (cc.includes(recipes[k].category) || (cc.includes('crafting') && !recipes[k].category)) {
const recipe = (recipes[k].normal ? recipes[k].normal : recipes[k])
if (!((this.name === 'assembling-machine-1' && recipe.ingredients.length > 2) ||
(this.name === 'assembling-machine-2' && recipe.ingredients.length > 4))
) {
acceptedRecipes.push(k)
}
}
}
return acceptedRecipes
},
get acceptedModules() {
const ommitProductivityModules = this.name === 'beacon' ||
(this.recipe && !factorioData.getItem('productivity-module').limitation.includes(this.recipe))
const items = factorioData.getItems()
const acceptedModules: string[] = []
for (const k in items) {
if (items[k].type === 'module' && !(k.includes('productivity-module') && ommitProductivityModules)) acceptedModules.push(k)
}
return acceptedModules
},
set direction(direction: number) {
BP.operation(this.entity_number, 'Set entity direction to ' + direction,
entities => entities.setIn([this.entity_number, 'direction'], direction)
)
},
get modules() {
const i = rawEntity.get('items')
return i ? i.toJS() : undefined
},
get modulesList() {
const i = rawEntity.get('items')
if (!i) return
const modules = i.toJS()
const moduleList = []
for (const n in modules) {
for (let i = 0; i < modules[n]; i++) {
moduleList.push(n)
}
}
return moduleList
},
set modulesList(list: any) {
const modules = {}
for (const m of list) {
if (Object.keys(modules).includes(m)) {
modules[m]++
} else {
modules[m] = 1
}
}
BP.operation(this.entity_number, 'Changed modules',
entities => entities.setIn([this.entity_number, 'items'], Immutable.fromJS(modules))
)
},
get splitterInputPriority() {
return rawEntity.get('input_priority')
},
get splitterOutputPriority() {
return rawEntity.get('output_priority')
},
get splitterFilter() {
return rawEntity.get('filter')
},
get inserterFilters() {
const f = rawEntity.get('filters')
return f ? f.toJS() : undefined
},
get constantCombinatorFilters() {
const f = rawEntity.getIn(['control_behavior', 'filters'])
return f ? f.toJS() : undefined
},
get logisticChestFilters() {
const f = rawEntity.get('request_filters')
return f ? f.toJS() : undefined
},
get deciderCombinatorConditions() {
const c = rawEntity.getIn(['control_behavior', 'decider_conditions'])
return c ? c.toJS() : undefined
},
get arithmeticCombinatorConditions() {
const c = rawEntity.getIn(['control_behavior', 'arithmetic_conditions'])
return c ? c.toJS() : undefined
},
get hasConnections() {
return rawEntity.get('connections') !== undefined
},
get connections() {
const c = rawEntity.get('connections')
return c ? c.toJS() : undefined
},
get connectedEntities() {
const c = rawEntity.get('connections')
if (!c) return
const connections = c.toJS()
const entities = []
for (const side in connections) {
for (const color in connections[side]) {
for (const c of connections[side][color]) {
entities.push(c.entity_id)
}
}
}
return entities
},
get chemicalPlantDontConnectOutput() {
const r = this.recipe
if (!r) return false
const rData = factorioData.getRecipe(r)
const recipe = (rData.normal ? rData.normal : rData)
if (recipe.result || recipe.results[0].type === 'item') return true
return false
},
get trainStopColor() {
const c = rawEntity.get('color')
return c ? c.toJS() : undefined
},
get operator() {
if (this.name === 'decider-combinator') {
const cb = rawEntity.get('control_behavior')
if (cb) return cb.getIn(['decider_conditions', 'comparator'])
}
if (this.name === 'arithmetic-combinator') {
const cb = rawEntity.get('control_behavior')
if (cb) return cb.getIn(['arithmetic_conditions', 'operation'])
}
return undefined
},
getArea(pos?: IPoint) {
return new Area({
x: pos ? pos.x : this.position.x,
y: pos ? pos.y : this.position.y,
width: this.size.x,
height: this.size.y
}, true)
},
change(name: string, direction: number) {
BP.operation(this.entity_number, 'Changed Entity', entities => (
entities.withMutations(map => {
map.setIn([this.entity_number, 'name'], name)
map.setIn([this.entity_number, 'direction'], direction)
})
))
},
move(pos: IPoint) {
const entity = BP.entity(this.entity_number)
if (!BP.entityPositionGrid.checkNoOverlap(entity.name, entity.direction, pos)) return false
BP.operation(this.entity_number, 'Moved entity',
entities => entities.setIn([this.entity_number, 'position'], Immutable.fromJS(pos)),
'mov'
)
BP.entityPositionGrid.setTileData(this.entity_number)
return true
},
rotate(notMoving: boolean, offset?: IPoint, pushToHistory = true, otherEntity?: number) {
if (!this.assemblerCraftsWithFluid &&
(this.name === 'assembling-machine-2' || this.name === 'assembling-machine-3')) return false
if (notMoving && BP.entityPositionGrid.sharesCell(this.getArea())) return false
const pr = this.entityData.possible_rotations
if (!pr) return false
const newDir = pr[(pr.indexOf(this.direction) +
(notMoving && (this.size.x !== this.size.y || this.type === 'underground-belt') ? 2 : 1)
) % pr.length
]
if (newDir === this.direction) return false
BP.operation(otherEntity ? [this.entity_number, otherEntity] : this.entity_number, 'Rotated entity',
entities => entities.withMutations(map => {
map.setIn([this.entity_number, 'direction'], newDir)
if (notMoving && this.type === 'underground-belt') {
map.updateIn([this.entity_number, 'type'], directionType =>
directionType === 'input' ? 'output' : 'input'
)
}
if (!notMoving && this.size.x !== this.size.y) {
// tslint:disable-next-line:no-parameter-reassignment
map.updateIn([this.entity_number, 'position', 'x'], x => x += offset.x)
// tslint:disable-next-line:no-parameter-reassignment
map.updateIn([this.entity_number, 'position', 'y'], y => y += offset.y)
}
}),
'upd',
notMoving && pushToHistory
)
return true
},
topLeft() {
return { x: this.position.x - (this.size.x / 2), y: this.position.y - (this.size.y / 2) }
},
topRight() {
return { x: this.position.x + (this.size.x / 2), y: this.position.y - (this.size.y / 2) }
},
bottomLeft() {
return { x: this.position.x - (this.size.x / 2), y: this.position.y + (this.size.y / 2) }
},
bottomRight() {
return { x: this.position.x + (this.size.x / 2), y: this.position.y + (this.size.y / 2) }
},
get assemblerCraftsWithFluid() {
return this.recipe &&
factorioData.getRecipe(this.recipe).category === 'crafting-with-fluid' &&
this.entityData.crafting_categories &&
this.entityData.crafting_categories.includes('crafting-with-fluid')
},
get assemblerPipeDirection() {
if (!this.recipe) return
const recipeData = factorioData.getRecipe(this.recipe)
const rD = recipeData.normal ? recipeData.normal : recipeData
for (const io of rD.ingredients) {
if (io.type === 'fluid') {
return 'input'
}
}
if (rD.results) {
for (const io of rD.results) {
if (io.type === 'fluid') {
return 'output'
}
}
}
},
getWireConnectionPoint(color: string, side: number) {
const e = this.entityData
// poles
if (e.connection_points) return e.connection_points[this.direction / 2].wire[color]
// combinators
if (e.input_connection_points) {
if (side === 1) return e.input_connection_points[this.direction / 2].wire[color]
return e.output_connection_points[this.direction / 2].wire[color]
}
if (e.circuit_wire_connection_point) return e.circuit_wire_connection_point.wire[color]
if (this.type === 'transport-belt') {
return e.circuit_wire_connection_points[
factorioData.getBeltConnections2(BP, this.position, this.direction) * 4
].wire[color]
}
if (e.circuit_wire_connection_points.length === 8) {
return e.circuit_wire_connection_points[7 - this.direction].wire[color]
}
if (this.name === 'constant-combinator') {
return e.circuit_wire_connection_points[this.direction / 2].wire[color]
}
return e.circuit_wire_connection_points[3 - this.direction / 2].wire[color]
},
toJS() {
return rawEntity.toJS()
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,361 @@
import { Blueprint } from './blueprint'
import util from '../util'
import factorioData from './factorioData'
import Immutable from 'immutable'
import { isNumber } from 'util'
export class Area {
y: number
x: number
height: number
width: number
constructor(data: any, posIsCenter?: boolean) {
this.width = data.width || 1
this.height = data.height || 1
if (posIsCenter) {
this.x = Math.round(data.x - this.width / 2)
this.y = Math.round(data.y - this.height / 2)
} else {
this.x = Math.floor(data.x)
this.y = Math.floor(data.y)
}
}
}
export class PositionGrid {
static tileDataAction(
grid: Immutable.Map<string, number | Immutable.List<number>>,
area: Area,
fn: (key: string, cell: number | Immutable.List<number>) => boolean | void,
returnEmptyCells = false
) {
let stop = false
for (let x = area.x, maxX = area.x + area.width; x < maxX; x++) {
for (let y = area.y, maxY = area.y + area.height; y < maxY; y++) {
const key = `${x},${y}`
const cell = grid.get(key)
if (cell || returnEmptyCells) stop = !!fn(key, cell)
if (stop) break
}
if (stop) break
}
}
bp: Blueprint
grid: Immutable.Map<string, number | Immutable.List<number>>
historyIndex: number
history: Array<Immutable.Map<string, number | Immutable.List<number>>>
constructor(bp: Blueprint, entity_numbers?: number[]) {
this.bp = bp
this.grid = Immutable.Map()
// Set Bulk
if (entity_numbers) {
this.grid = this.grid.withMutations(map => {
for (const entity_number of entity_numbers) {
const entity = this.bp.entity(entity_number)
if (!entity.entityData.flags.includes('placeable-off-grid')) {
PositionGrid.tileDataAction(map, entity.getArea(), (key, cell) => {
if (cell) {
if (isNumber(cell)) {
map.set(key, Immutable.List([
cell,
entity_number
]))
} else {
map.setIn([key, cell.size], entity_number)
}
} else {
map.set(key, entity_number)
}
}, true)
}
}
})
}
this.history = [this.grid]
this.historyIndex = 0
}
undo() {
if (this.historyIndex === 0) return
this.grid = this.history[--this.historyIndex]
}
redo() {
if (this.historyIndex === this.history.length - 1) return
this.grid = this.history[++this.historyIndex]
}
operation(fn: (grid: Immutable.Map<string, number | Immutable.List<number>>) => Immutable.Map<any, any>, pushToHistory = true) {
this.grid = fn(this.grid)
if (pushToHistory) {
if (this.historyIndex < this.history.length) {
this.history = this.history.slice(0, this.historyIndex + 1)
}
this.history.push(this.grid)
this.historyIndex++
}
}
getAllPositions() {
return [...this.grid.keys()].map(p => {
const pS = p.split(',')
return {x: Number(pS[0]), y: Number(pS[1])}
})
}
getCellAtPosition(position: any): number {
const POS = position instanceof Array ? {x: position[0], y: position[1]} : position
const cell = this.grid.get(`${Math.floor(POS.x)},${Math.floor(POS.y)}`)
if (cell) {
if (isNumber(cell)) return cell
else return cell.first()
}
}
setTileData(entity_number: number) {
const entity = this.bp.entity(entity_number)
if (entity.entityData.flags.includes('placeable-off-grid')) return
this.operation(grid => grid.withMutations(map => {
PositionGrid.tileDataAction(map, entity.getArea(), (key, cell) => {
if (cell) {
if (isNumber(cell)) {
map.set(key, Immutable.List([
cell,
entity_number
]))
} else {
map.setIn([key, cell.size], entity_number)
}
} else {
map.set(key, entity_number)
}
}, true)
}))
}
removeTileData(entity_number: number, pushToHistory?: boolean) {
this.operation(grid => grid.withMutations(map => {
PositionGrid.tileDataAction(map, this.bp.entity(entity_number).getArea(), (key, cell) => {
if (isNumber(cell)) {
if (cell === entity_number) map.delete(key)
} else {
const res = cell.findIndex(v => {
if (v === entity_number) return true
})
if (res !== -1) {
if (map.get(key).count() === 1) {
map.delete(key)
} else {
map.deleteIn([key, res])
if (map.get(key).count() === 1) map.set(key, map.get(key).first())
}
}
}
})
}), pushToHistory)
}
checkNoOverlap(name: string, direction: number, pos: IPoint) {
const fd = factorioData.getEntity(name)
const size = util.switchSizeBasedOnDirection(fd.size, direction)
const area = new Area({
x: pos.x,
y: pos.y,
width: size.x,
height: size.y
}, true)
const allStrRailEnt: number[] = []
let gateEnt: number
let strRailEnt: number
let curRailEnt: number
let otherEntities = false
if (!this.foreachOverlap(area, cell => {
switch (this.bp.entity(cell).name) {
case 'gate': gateEnt = cell; break
case 'curved-rail': curRailEnt = cell; break
case 'straight-rail': allStrRailEnt.push(cell); strRailEnt = cell; break
default: otherEntities = true
}
})) return true
let sameDirStrRails = false
for (const k of allStrRailEnt) {
if (this.bp.entity(k).direction === direction) {
sameDirStrRails = true
break
}
}
if (
(name === 'gate' && strRailEnt && allStrRailEnt.length === 1 && this.bp.entity(strRailEnt).direction !== direction && !gateEnt) ||
(name === 'straight-rail' && gateEnt && !strRailEnt && this.bp.entity(gateEnt).direction !== direction && !otherEntities) ||
(name === 'straight-rail' && strRailEnt && !sameDirStrRails && !gateEnt) ||
(name === 'curved-rail' && strRailEnt && !gateEnt) ||
(name === 'straight-rail' && curRailEnt) ||
(name === 'curved-rail' && curRailEnt && this.bp.entity(curRailEnt).direction !== direction)
) return true
return false
}
checkFastReplaceableGroup(name: string, direction: number, pos: IPoint) {
const fd = factorioData.getEntity(name)
const size = util.switchSizeBasedOnDirection(fd.size, direction)
const area = new Area({
x: pos.x,
y: pos.y,
width: size.x,
height: size.y
}, true)
if (this.sharesCell(area)) return false
const ent = this.getFirstFromArea(area, cell => {
const ent = this.bp.entity(cell)
if (ent.name !== name &&
ent.entityData.fast_replaceable_group &&
fd.fast_replaceable_group &&
ent.entityData.fast_replaceable_group ===
fd.fast_replaceable_group
) return cell
})
if (!ent || pos.x !== this.bp.entity(ent).position.x ||
pos.y !== this.bp.entity(ent).position.y) return false
return ent
}
checkSameEntityAndDifferentDirection(name: string, direction: number, pos: IPoint) {
if (name === 'straight-rail') return false
const fd = factorioData.getEntity(name)
const size = util.switchSizeBasedOnDirection(fd.size, direction)
const area = new Area({
x: pos.x,
y: pos.y,
width: size.x,
height: size.y
}, true)
if (this.sharesCell(area)) return false
const ent = this.getFirstFromArea(area, cell => {
if (this.bp.entity(cell).name === name) return cell
})
if (!ent) return false
const e = this.bp.entity(ent)
if (pos.x !== e.position.x || pos.y !== e.position.y || e.direction === direction) return false
return ent
}
findEntityWithSameNameAndDirection(name: string, direction: number, pos: IPoint, searchDirection: number, maxDistance: number) {
const position = {
x: Math.floor(pos.x),
y: Math.floor(pos.y)
}
const horizontal = searchDirection % 4 !== 0
const sign = searchDirection === 0 || searchDirection === 6 ? -1 : 1
for (let i = 1; i <= maxDistance; i++) {
const cell = this.grid.get(
`${position.x + (horizontal ? i * sign : 0)},${position.y + (!horizontal ? i * sign : 0)}`
)
if (isNumber(cell)) {
const entity = this.bp.entity(cell)
if (entity.name === name) {
if (entity.direction === direction) return cell
if ((entity.direction + 4) % 8 === direction) return false
}
}
}
return false
}
sharesCell(area: Area) {
let output = false
PositionGrid.tileDataAction(this.grid, area, (_, cell) => {
if (Immutable.List.isList(cell)) {
output = true
return true
}
})
return output
}
getFirstFromArea(area: Area, fn: (cell: number) => number): false | number {
let output: boolean | number = false
PositionGrid.tileDataAction(this.grid, area, (_, cell) => {
if (isNumber(cell)) {
output = fn(cell)
if (output) return true
} else {
for (const v of cell.values()) {
output = fn(v)
if (output) return true
}
}
})
return output
}
foreachOverlap(area: Area, fn: (cell: number) => any, returnEmptyCells?: boolean) {
const output: boolean[] = []
PositionGrid.tileDataAction(this.grid, area, (_, cell) => {
let out = false
if (Immutable.List.isList(cell)) {
for (const v of cell.values()) {
const o = fn(v)
if (o !== undefined) out = o
}
} else {
const o = fn(cell as number)
if (o !== undefined) out = o
}
output.push(out)
}, returnEmptyCells)
return output.length === 0 ? false : output
}
getSurroundingEntities(
area: Area,
fn: (cell: number, relDir: number, x: number, y: number) => any,
direction?: number
) {
const coordinates = []
for (let i = 0; i < area.width; i++) {
coordinates.push([0, area.x + i, area.y - 1])
coordinates.push([4, area.x + i, area.y + area.height])
}
for (let i = 0; i < area.height; i++) {
coordinates.push([2, area.x + area.width, area.y + i])
coordinates.push([6, area.x - 1, area.y + i])
}
let output: any[] = [false, false, false, false]
for (const coordinate of coordinates) {
const cell = this.grid.get(`${coordinate[1]},${coordinate[2]}`)
const relDir = coordinate[0] / 2
if (cell) {
if (isNumber(cell)) {
const o = fn(cell as number, coordinate[0], coordinate[1], coordinate[2])
if (o !== undefined) output[relDir] = o
} else {
for (const v of cell.values()) {
const o = fn(v, coordinate[0], coordinate[1], coordinate[2])
if (o !== undefined) output[relDir] = o
}
}
}
}
if (direction) output = [...output, ...output].splice(direction / 2, 4)
return output
}
}

30
src/factorio-data/tile.ts Normal file
View File

@@ -0,0 +1,30 @@
import { Blueprint } from './blueprint'
export class Tile {
id: number
bp: any
name: any
position: any
constructor(data: any, bp: Blueprint) {
this.id = -1
this.bp = bp
this.name = data.name
if (!data.position || data.position.x === undefined || data.position.y === undefined) {
throw new Error(`Invalid position provided: ${data.position}`)
}
this.position = data.position
}
remove() {
return this.bp.removeTile(this)
}
getData() {
return {
name: this.name,
position: this.position
}
}
}

84
src/globals.ts Normal file
View File

@@ -0,0 +1,84 @@
import { Blueprint } from './factorio-data/blueprint'
import { ToolbarContainer } from './containers/toolbar'
import { BlueprintContainer } from './containers/blueprint'
import { EditEntityContainer } from './containers/editEntity'
import { InventoryContainer } from './containers/inventory'
// tslint:disable:prefer-const
let app: PIXI.Application
let toolbarContainer: ToolbarContainer
let editEntityContainer: EditEntityContainer
let inventoryContainer: InventoryContainer
let BPC: BlueprintContainer
let gridCoordsOfCursor: IPoint = { x: 0, y: 0 }
let gridCoords16: IPoint = { x: 0, y: 0 }
let railMoveOffset: IPoint = { x: 0, y: 0 }
let openedGUIWindow: InventoryContainer | EditEntityContainer | undefined
const cellSize = 32
const positionBPContainer = {
x: 0,
y: 32
}
const bpArea = {
width: 400,
height: 400
}
const sizeBPContainer = {
width: bpArea.width * 32,
height: bpArea.height * 32
}
let bp: Blueprint
const mouseStates = {
NONE: 0,
MOVING: 1,
PAINTING: 2,
PANNING: 3
}
const keyboard = {
w: false,
a: false,
s: false,
d: false,
shift: false
}
let currentMouseState = mouseStates.NONE
const copyData = {
recipe: ''
}
let renderOnly = false
export default {
renderOnly,
copyData,
openedGUIWindow,
inventoryContainer,
editEntityContainer,
BPC,
app,
keyboard,
toolbarContainer,
cellSize,
bpArea,
positionBPContainer,
sizeBPContainer,
gridCoordsOfCursor,
railMoveOffset,
gridCoords16,
bp,
mouseStates,
currentMouseState
}

25
src/index.d.ts vendored Normal file
View File

@@ -0,0 +1,25 @@
declare module '*.json' {
const content: any
export default content
}
interface IPoint {
x: number
y: number
}
interface IEntityData {
hr: boolean
dir: number
bp: Blueprint
position: IPoint
hasConnections: boolean
assemblerPipeDirection: string
dirType: string
operator: string
assemblerCraftsWithFluid: boolean
trainStopColor: { r: number; g: number; b: number; a: number}
chemicalPlantDontConnectOutput: boolean
}

13
src/index.html Normal file

File diff suppressed because one or more lines are too long

BIN
src/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

2
src/sample-blueprint.ts Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 MiB

View File

@@ -0,0 +1,65 @@
{
"frames": {
"extra-icon:cursor-boxes-0": {
"frame": { "x": 0, "y": 182, "w": 63, "h": 63 },
"sourceSize": { "w": 64, "h": 64 }
},
"extra-icon:cursor-boxes-1": {
"frame": { "x": 64, "y": 182, "w": 63, "h": 63 },
"sourceSize": { "w": 64, "h": 64 }
},
"extra-icon:cursor-boxes-2": {
"frame": { "x": 128, "y": 182, "w": 63, "h": 63 },
"sourceSize": { "w": 64, "h": 64 }
},
"extra-icon:cursor-boxes-3": {
"frame": { "x": 192, "y": 182, "w": 63, "h": 63 },
"sourceSize": { "w": 64, "h": 64 }
},
"extra-icon:cursor-boxes-4": {
"frame": { "x": 256, "y": 182, "w": 63, "h": 63 },
"sourceSize": { "w": 64, "h": 64 }
},
"extra-icon:cursor-boxes": {
"frame": { "x": 0, "y": 182, "w": 320, "h": 320 },
"sourceSize": { "w": 320, "h": 320 }
},
"extra-icon:cursor-boxes-32x32-0": {
"frame": { "x": 0, "y": 0, "w": 63, "h": 63 },
"sourceSize": { "w": 64, "h": 64 }
},
"extra-icon:cursor-boxes-32x32": {
"frame": { "x": 0, "y": 0, "w": 320, "h": 64 },
"sourceSize": { "w": 320, "h": 64 }
},
"extra-icon:electricity-icon-unplugged": {
"frame": { "x": 66, "y": 116, "w": 64, "h": 64 },
"sourceSize": { "w": 64, "h": 64 }
},
"extra-icon:entity-info-dark-background": {
"frame": { "x": 262, "y": 116, "w": 53, "h": 53 },
"sourceSize": { "w": 53, "h": 53 }
},
"extra-icon:fluid-indication-arrow": {
"frame": { "x": 0, "y": 66, "w": 48, "h": 48 },
"sourceSize": { "w": 48, "h": 48 }
},
"extra-icon:fluid-indication-arrow-both-ways": {
"frame": { "x": 50, "y": 66, "w": 48, "h": 48 },
"sourceSize": { "w": 48, "h": 48 }
},
"extra-icon:indication-arrow": {
"frame": { "x": 0, "y": 116, "w": 64, "h": 64 },
"sourceSize": { "w": 64, "h": 64 }
},
"extra-icon:underground-lines-0": {
"frame": { "x": 132, "y": 116, "w": 64, "h": 64 },
"sourceSize": { "w": 64, "h": 64 }
},
"extra-icon:underground-lines-1": {
"frame": { "x": 196, "y": 116, "w": 64, "h": 64 },
"sourceSize": { "w": 64, "h": 64 }
}
},
"meta": { "image": "extra_iconSprites.png" }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

86
src/updateGroups.ts Normal file
View File

@@ -0,0 +1,86 @@
import factorioData from './factorio-data/factorioData'
const updateGroups = [
{
is: [
'transport-belt',
'fast-transport-belt',
'express-transport-belt',
'splitter',
'fast-splitter',
'express-splitter',
'underground-belt',
'fast-underground-belt',
'express-underground-belt'
],
updates: [
'transport-belt',
'fast-transport-belt',
'express-transport-belt'
]
},
{
is: [
'heat-pipe',
'nuclear-reactor',
'heat-exchanger'
],
updates: [
'heat-pipe',
'nuclear-reactor',
'heat-exchanger'
]
},
{
has: [
'fluid_box',
'output_fluid_box',
'fluid_boxes'
],
updates: [
'fluid_box',
'output_fluid_box',
'fluid_boxes'
]
},
{
is: [
'stone-wall',
'gate',
'straight-rail'
],
updates: [
'stone-wall',
'gate',
'straight-rail'
]
}
]
for (const updateGroup of updateGroups) {
if (updateGroup.has) {
const is = []
const updates = []
for (let j = 0; j < updateGroup.has.length; j++) {
const ed = factorioData.getEntities()
for (const k in ed) {
if (ed[k][updateGroup.has[j]]) {
is.push(k)
}
}
for (const k in ed) {
if (ed[k][updateGroup.updates[j]]) {
updates.push(k)
}
}
}
delete updateGroup.has
updateGroup.is = is
updateGroup.updates = updates
}
}
export {
updateGroups
}

157
src/util.ts Normal file
View File

@@ -0,0 +1,157 @@
function duplicate(obj: any) {
return JSON.parse(JSON.stringify(obj))
}
function set_shift(shift: any, tab: any) {
tab.shift = shift
if (tab.hr_version) {
tab.hr_version.shift = shift
}
return tab
}
function add_to_shift(shift: any, tab: any) {
const SHIFT = shift.constructor === Object ? [shift.x, shift.y] : shift
tab.shift = tab.shift ? [SHIFT[0] + tab.shift[0], SHIFT[1] + tab.shift[1]] : SHIFT
if (tab.hr_version) {
tab.hr_version.shift = tab.hr_version.shift ?
[SHIFT[0] + tab.hr_version.shift[0], SHIFT[1] + tab.hr_version.shift[1]] :
SHIFT
}
return tab
}
function set_property(img: any, key: string, val: any) {
img[key] = val
if (img.hr_version) {
img.hr_version[key] = val
}
return img
}
function set_property_using(img: any, key: any, key2: any, mult = 1) {
if (key2) {
img[key] = img[key2] * mult
if (img.hr_version) {
img.hr_version[key] = img.hr_version[key2] * mult
}
}
return img
}
function duplicateAndSetPropertyUsing(img: any, key: any, key2: any, mult: number) {
return set_property_using(this.duplicate(img), key, key2, mult)
}
function getRandomInt(min: number, max: number) {
const MIN = Math.ceil(min)
const MAX = Math.floor(max)
return Math.floor(Math.random() * (MAX - MIN)) + MIN
}
function rotatePointBasedOnDir(p: any, dir: number) {
const point: IPoint = {x: 0, y: 0}
const nP = p instanceof Array ? { x: p[0], y: p[1] } : { ...p }
switch (dir) {
case 0:
// x y
point.x = nP.x
point.y = nP.y
break
case 2:
// -y x
point.x = nP.y * -1
point.y = nP.x
break
case 4:
// -x -y
point.x = nP.x * -1
point.y = nP.y * -1
break
case 6:
// y -x
point.x = nP.y
point.y = nP.x * -1
}
// if (retArray) return [point.x, point.y]
return point
}
function transformConnectionPosition(position: IPoint, direction: number) {
const dir = Math.abs(position.x) > Math.abs(position.y) ?
(Math.sign(position.x) === 1 ? 2 : 6) :
(Math.sign(position.y) === 1 ? 4 : 0)
switch (dir) {
case 0: position.y += 1; break
case 2: position.x -= 1; break
case 4: position.y -= 1; break
case 6: position.x += 1
}
return rotatePointBasedOnDir(position, direction)
}
function switchSizeBasedOnDirection(defaultSize: { width: any; height: any }, direction: number) {
if (defaultSize.width !== defaultSize.height && (direction === 2 || direction === 6)) {
return { x: defaultSize.height, y: defaultSize.width }
}
return { x: defaultSize.width, y: defaultSize.height }
}
function findBPString(data: string) {
const DATA = data.replace(/\s/g, '')
if (DATA[0] === '0') return new Promise(resolve => resolve(DATA))
// function isUrl(url: string) {
// try { return Boolean(new URL(url)) }
// catch (e) { return false }
// }
return new Promise(resolve => resolve(new URL(DATA))).then((url: URL) => {
console.log(`Loading data from: ${url}`)
const pathParts = url.pathname.slice(1).split('/')
switch (url.hostname.split('.')[0]) {
case 'pastebin':
case 'hastebin':
return fetchData(`${url.hostname}/raw/${pathParts[0]}`).then(r => r.text())
case 'gist':
return fetchData(`api.github.com/gists/${pathParts[1]}`).then(r =>
r.json().then(data => data.files[Object.keys(data.files)[0]].content)
)
case 'gitlab':
return fetchData(`${url}/raw`).then(r => r.text())
case 'factorioprints':
return fetchData(`facorio-blueprints.firebaseio.com/blueprints/${pathParts[1]}.json`).then(r =>
r.json().then(data => data.blueprintString)
)
case 'docs':
return fetchData(`${url.toString().replace('/edit', '')}/export?format=txt`).then(r => r.text())
default:
return fetchData(url.toString()).then(r => r.text())
}
// TODO: maybe add dropbox support https://www.dropbox.com/s/ID?raw=1
})
function fetchData(url: string) {
return fetch('https://allorigins.me/get?method=raw&url=' + url).then(response => {
if (response.ok) return response
throw new Error('Network response was not ok.')
})
}
}
export default {
findBPString,
duplicate,
set_shift,
set_property,
set_property_using,
add_to_shift,
getRandomInt,
duplicateAndSetPropertyUsing,
rotatePointBasedOnDir,
transformConnectionPosition,
switchSizeBasedOnDirection
}

189
src/zoomPan.ts Normal file
View File

@@ -0,0 +1,189 @@
import * as PIXI from 'pixi.js'
export class ZoomPan {
private container: PIXI.Container
private size: any
private viewPortPosition: any
private viewPortSize: any
private maxZoom: number
private dirty: boolean
private positionX: number
private positionY: number
private scaleX: number
private scaleY: number
private scaleCenterX: number
private scaleCenterY: number
private origTransform: PIXI.Matrix
private transform: PIXI.Matrix
constructor(container: PIXI.Container, size: any, viewPortPosition: any, viewPortSize: any, maxZoom: number) {
this.container = container
this.size = size
this.viewPortPosition = viewPortPosition
this.viewPortSize = viewPortSize
this.maxZoom = maxZoom
this.dirty = true
this.positionX = 0
this.positionY = 0
this.scaleX = 1
this.scaleY = 1
this.scaleCenterX = 0
this.scaleCenterY = 0
this.origTransform = new PIXI.Matrix()
this.transform = new PIXI.Matrix()
}
updateTransform() {
const t = this.getTransform()
this.container.setTransform(t.tx, t.ty, t.a, t.d)
}
_updateMatrix() {
// Accumulate zoom transformations.
// origTransform is an intermediate accumulative matrix used for tracking the current zoom target.
this.origTransform.append(new PIXI.Matrix(1, 0, 0, 1, this.scaleCenterX, this.scaleCenterY))
this.origTransform.append(new PIXI.Matrix(this.scaleX, 0, 0, this.scaleY, 0, 0))
this.origTransform.append(new PIXI.Matrix(1, 0, 0, 1, -this.scaleCenterX, -this.scaleCenterY))
// We reset Scale because origTransform is accumulative and has "captured" the information.
this.scaleX = 1
this.scaleY = 1
// Tack on translation. Note: we don't append it, but concat it into a separate matrix.
// We want to leave origTransform solely responsible for zooming.
// "transform" is the final matrix.
this.transform = this.origTransform.clone()
// UpperLeft Corner constraints
const minX = this.viewPortPosition.x - this.transform.tx
const minY = this.viewPortPosition.y - this.transform.ty
// LowerRight Corner constraints
const maxX = -(this.size.width * this.transform.a - this.viewPortSize.width) - this.transform.tx
const maxY = -(this.size.height * this.transform.a - this.viewPortSize.height) - this.transform.ty
// Check if viewport area is bigger than the container
if (maxX - minX > 0 || maxY - minY > 0) {
this.origTransform = new PIXI.Matrix()
this.scaleCenterX = this.size.width / 2
this.scaleCenterY = this.size.height / 2
const maxZoom = Math.max(
this.viewPortSize.width / (this.size.width * this.transform.a),
this.viewPortSize.height / (this.size.height * this.transform.a)
) * this.transform.a
this.scaleX = maxZoom
this.scaleY = maxZoom
this._updateMatrix()
return
}
if (this.positionX > minX) this.positionX = minX
if (this.positionY > minY) this.positionY = minY
if (this.positionX < maxX) this.positionX = maxX
if (this.positionY < maxY) this.positionY = maxY
this.transform.translate(this.positionX, this.positionY)
}
centerViewPort(focusObjectSize: IPoint, offset: IPoint) {
this.origTransform = new PIXI.Matrix()
this.positionX = this.viewPortPosition.x - (this.size.width / 2) +
(this.viewPortSize.width - this.viewPortPosition.x) / 2 + offset.x
this.positionY = this.viewPortPosition.y - (this.size.height / 2) +
(this.viewPortSize.height - this.viewPortPosition.y) / 2 + offset.y
this.scaleCenterX = this.size.width / 2 + -offset.x
this.scaleCenterY = this.size.height / 2 + -offset.y
const zoom = Math.min(
(this.viewPortSize.width - this.viewPortPosition.x) / focusObjectSize.x,
(this.viewPortSize.height - this.viewPortPosition.y) / focusObjectSize.y
)
this.scaleX = zoom
this.scaleY = zoom
this.dirty = true
this.updateTransform()
}
getTransform() {
if (this.dirty) {
this._updateMatrix()
this.dirty = false
}
return this.transform
}
setViewPortSize(width: number, height: number) {
this.viewPortSize.width = width
this.viewPortSize.height = height
this.dirty = true
}
setPosition(posX: number, posY: number) {
this.positionX = posX
this.positionY = posY
this.dirty = true
}
getPositionX() {
return this.positionX
}
getPositionY() {
return this.positionY
}
zoomBy(deltaX: number, deltaY: number) {
if (Math.sign(deltaX) === 1 && this.origTransform.a > this.maxZoom) return
this.scaleX += deltaX
this.scaleY += deltaY
this.dirty = true
}
translateBy(deltaX: number, deltaY: number) {
this.positionX += deltaX
this.positionY += deltaY
this.dirty = true
}
setCurrentScale(newScale: number) {
if (this.dirty) {
this._updateMatrix()
}
// We use dimensional analysis to set the scale. Remember we can't
// just set the scale absolutely because origTransform is an accumulating matrix.
// We have to take its current value and compute a new value based
// on the passed in value.
const scaleFactor = newScale / this.origTransform.a
this.scaleX = scaleFactor
this.scaleY = scaleFactor
this.dirty = true
}
getCurrentScale() {
return this.origTransform.a
}
setScaleCenter(posX: number, posY: number) {
this.scaleCenterX = posX
this.scaleCenterY = posY
this.dirty = true
}
}

24
tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compileOnSave": false,
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"lib": [
"dom",
"es2015",
"es2016",
"es6"
],
"target": "es6",
"module": "es2015",
"moduleResolution": "node",
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"removeComments": false,
"preserveConstEnums": true,
"sourceMap": true,
"skipLibCheck": true,
"declaration": true,
"alwaysStrict": true
}
}

122
tslint.json Normal file
View File

@@ -0,0 +1,122 @@
{
"extends": [
"tslint:all"
],
"rules": {
"no-any": false,
"typedef": [
false,
"call-signature",
"arrow-call-signature",
"parameter",
"arrow-parameter",
"property-declaration",
"member-variable-declaration"
],
"no-shadowed-variable": [
false,
{
"class": true,
"enum": true,
"function": true,
"interface": true,
"namespace": true,
"typeAlias": false,
"typeParameter": false
}
],
"align": [
true,
"parameters",
"statements",
"elements",
"members"
],
"no-default-export": false,
"cyclomatic-complexity": false,
"binary-expression-operand-order": false,
"one-variable-per-declaration": [true, "ignore-for-loop"],
"newline-per-chained-call": false,
"prefer-template": [true, "allow-single-concat"],
"newline-before-return": false,
"no-magic-numbers": false,
"member-ordering": [true, { "order": "statics-first" }],
"arrow-parens": [true, "ban-single-arg-parens"],
"class-name": true,
"comment-format": [true, "check-space"],
"indent": [true, "spaces", 4],
"interface-name": [true, "always-prefix"],
"max-classes-per-file": false,
"max-line-length": [true, 150],
"member-access": false,
"no-duplicate-variable": true,
"no-empty": true,
"no-eval": true,
"no-internal-module": true,
"no-trailing-whitespace": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-else",
"check-open-brace",
"check-whitespace"
],
"ordered-imports": false,
"radix": false,
"quotemark": [
true,
"single",
"avoid-escape"
],
"semicolon": [true, "never"],
"no-implicit-dependencies": [true, "dev"],
"triple-equals": true,
"prefer-for-of": true,
"prefer-const": true,
"curly": [true, "ignore-same-line"],
"only-arrow-functions": [true, "allow-named-functions"],
"no-console": false,
"no-string-literal": false,
"forin": false,
"switch-default": false,
"trailing-comma": [
true,
{
"multiline": "never",
"singleline": "never",
"esSpecCompliant": true
}
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"variable-name": [
true,
"ban-keywords",
"check-format",
"allow-snake-case"
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-module",
"check-separator",
"check-rest-spread",
"check-type",
"check-typecast",
"check-type-operator",
"check-preblock"
]
}
}

83
webpack.common.js Normal file
View File

@@ -0,0 +1,83 @@
'use strict'
// https://medium.com/webpack/webpack-4-mode-and-optimization-5423a6bc597a
const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const FaviconsWebpackPlugin = require('webapp-webpack-plugin') // favicons-webpack-plugin
const CopyWebpackPlugin = require('copy-webpack-plugin')
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
const extractPlugin = new ExtractTextPlugin({
filename: '[name].css',
allChunks: true
})
const babelLoader = {
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: [['@babel/preset-env', { useBuiltIns: 'entry' }]]
}
}
module.exports = {
target: 'web',
entry: {
main: './src/app'
},
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].js'
},
resolve: {
extensions: ['.js', '.json', '.ts']
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
babelLoader
]
},
{
test: /\.ts?$/,
exclude: /node_modules/,
use: [
babelLoader,
{
loader: 'ts-loader',
options: { transpileOnly: true }
}
]
},
{
test: /normalize.css/,
use: extractPlugin.extract({
use: ['css-loader']
})
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html',
hash: true
}),
extractPlugin,
new FaviconsWebpackPlugin({
logo: './src/logo.png'
}),
// https://github.com/ProvidenceGeeks/website-frontend/pull/142
new CopyWebpackPlugin([
{ from: 'src/spritesheets', to: 'spritesheets'/*'factorio-data/bundles/[name].[hash].[ext]'*/ }
]),
new CleanWebpackPlugin(['dist']),
new ForkTsCheckerWebpackPlugin({
tslint: true,
watch: ['./src']
})
]
}

9
webpack.dev.js Normal file
View File

@@ -0,0 +1,9 @@
'use strict'
const merge = require('webpack-merge')
const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'development'
// devtool: 'inline-source-map'
})

38
webpack.prod.js Normal file
View File

@@ -0,0 +1,38 @@
'use strict'
const merge = require('webpack-merge')
const common = require('./webpack.common.js')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const Visualizer = require('webpack-visualizer-plugin')
//const ClosureCompilerPlugin = require('webpack-closure-compiler')
// https://webpack.github.io/analyse/
module.exports = merge(common, {
mode: 'production',
profile: true,
plugins: [
new OptimizeCssAssetsPlugin({
cssProcessorOptions: { discardComments: { removeAll: true } }
}),
new BundleAnalyzerPlugin({
reportFilename: './stats/report.html',
analyzerMode: 'static',
openAnalyzer: false,
generateStatsFile: true,
statsFilename: './stats/stats.json'
}),
new Visualizer({
filename: './stats/statistics.html'
})
// new ClosureCompilerPlugin({
// compiler: {
// language_in: 'ECMASCRIPT6',
// language_out: 'ECMASCRIPT5',
// compilation_level: 'ADVANCED'
// },
// jsCompiler: true,
// concurrency: 3,
// })
]
})