diff --git a/res/ui/building_icons/constant_producer.png b/res/ui/building_icons/constant_producer.png new file mode 100644 index 00000000..ca267ea6 Binary files /dev/null and b/res/ui/building_icons/constant_producer.png differ diff --git a/res/ui/building_icons/goal_acceptor.png b/res/ui/building_icons/goal_acceptor.png new file mode 100644 index 00000000..ca267ea6 Binary files /dev/null and b/res/ui/building_icons/goal_acceptor.png differ diff --git a/res/ui/building_tutorials/constant_producer.png b/res/ui/building_tutorials/constant_producer.png new file mode 100644 index 00000000..b0d15387 Binary files /dev/null and b/res/ui/building_tutorials/constant_producer.png differ diff --git a/res/ui/building_tutorials/goal_acceptor.png b/res/ui/building_tutorials/goal_acceptor.png new file mode 100644 index 00000000..b0d15387 Binary files /dev/null and b/res/ui/building_tutorials/goal_acceptor.png differ diff --git a/res_raw/sprites/blueprints/constant_producer.png b/res_raw/sprites/blueprints/constant_producer.png new file mode 100644 index 00000000..2288c07d Binary files /dev/null and b/res_raw/sprites/blueprints/constant_producer.png differ diff --git a/res_raw/sprites/blueprints/goal_acceptor.png b/res_raw/sprites/blueprints/goal_acceptor.png new file mode 100644 index 00000000..2288c07d Binary files /dev/null and b/res_raw/sprites/blueprints/goal_acceptor.png differ diff --git a/res_raw/sprites/buildings/constant_producer.png b/res_raw/sprites/buildings/constant_producer.png new file mode 100644 index 00000000..573ef372 Binary files /dev/null and b/res_raw/sprites/buildings/constant_producer.png differ diff --git a/res_raw/sprites/buildings/goal_acceptor.png b/res_raw/sprites/buildings/goal_acceptor.png new file mode 100644 index 00000000..573ef372 Binary files /dev/null and b/res_raw/sprites/buildings/goal_acceptor.png differ diff --git a/src/css/ingame_hud/mode_menu.scss b/src/css/ingame_hud/mode_menu.scss new file mode 100644 index 00000000..69c0adb5 --- /dev/null +++ b/src/css/ingame_hud/mode_menu.scss @@ -0,0 +1,54 @@ +#ingame_HUD_ModeMenu { + position: absolute; + @include S(bottom, 10px); + @include S(left, 10px); + display: flex; + + backdrop-filter: blur(D(1px)); + flex-direction: column; + align-items: flex-start; + backdrop-filter: blur(D(1px)); + padding: D(3px); + + > button, + > .button { + @include PlainText; + @include IncreasedClickArea(0px); + background: green; + @include S(width, 30px); + @include S(height, 30px); + + pointer-events: all; + cursor: pointer; + position: relative; + transition: all 0.12s ease-in-out; + transition-property: opacity, transform; + + display: inline-flex; + background: center center / 70% no-repeat; + grid-row: 1; + + &.pressed { + transform: scale(0.9) !important; + } + + opacity: 0.7; + &:hover { + opacity: 0.9 !important; + } + + @include DarkThemeInvert; + + &.settings { + & { + /* @load-async */ + background-image: uiResource("icons/settings_menu_settings.png"); + } + } + + &:hover { + opacity: 0.9; + transform: translateY(0); + } + } +} diff --git a/src/css/ingame_hud/mode_menu_back.scss b/src/css/ingame_hud/mode_menu_back.scss new file mode 100644 index 00000000..27d07592 --- /dev/null +++ b/src/css/ingame_hud/mode_menu_back.scss @@ -0,0 +1,35 @@ +#ingame_HUD_ModeMenuBack { + position: absolute; + @include S(top, 10px); + @include S(left, 10px); + + display: flex; + flex-direction: column; + align-items: flex-start; + backdrop-filter: blur(D(1px)); + padding: D(3px); + + > .button { + @include PlainText; + @include IncreasedClickArea(0px); + pointer-events: all; + cursor: pointer; + position: relative; + color: #333438; + transition: all 0.12s ease-in-out; + transition-property: opacity, transform; + + opacity: 0.8; + &:hover { + opacity: 1 !important; + } + + &.pressed { + transform: scale(0.9) !important; + } + + @include DarkThemeOverride { + color: #fff; + } + } +} diff --git a/src/css/ingame_hud/mode_menu_next.scss b/src/css/ingame_hud/mode_menu_next.scss new file mode 100644 index 00000000..45239d85 --- /dev/null +++ b/src/css/ingame_hud/mode_menu_next.scss @@ -0,0 +1,42 @@ +#ingame_HUD_ModeMenuNext { + position: absolute; + @include S(top, 10px); + @include S(right, 10px); + + display: flex; + flex-direction: column; + align-items: flex-end; + backdrop-filter: blur(D(1px)); + padding: D(3px); + + > .button { + @include ButtonText; + @include IncreasedClickArea(0px); + pointer-events: all; + cursor: pointer; + position: relative; + color: #333438; + transition: all 0.12s ease-in-out; + transition-property: opacity, transform; + + opacity: 0.8; + &:hover { + opacity: 1 !important; + } + + &.pressed { + transform: scale(0.9) !important; + } + + @include DarkThemeOverride { + color: #fff; + } + } + + > .content { + @include SuperSmallText; + @include S(font-size, 7px); + @include S(width, 150px); + text-align: right; + } +} diff --git a/src/css/ingame_hud/mode_settings.scss b/src/css/ingame_hud/mode_settings.scss new file mode 100644 index 00000000..8df3f468 --- /dev/null +++ b/src/css/ingame_hud/mode_settings.scss @@ -0,0 +1,47 @@ +#ingame_HUD_ModeSettings { + position: absolute; + background: $ingameHudBg; + @include S(padding, 10px); + @include S(bottom, 50px); + @include S(left, 15px); + + @include SuperSmallText; + color: #eee; + display: flex; + flex-direction: column; + + > .section { + > label { + text-transform: uppercase; + } + + .plusMinus { + @include S(margin-top, 5px); + display: grid; + grid-template-columns: 1fr auto auto auto; + align-items: center; + @include S(grid-gap, 5px); + + label { + @include S(margin-right, 10px); + } + + button { + @include PlainText; + @include S(padding, 0); + display: flex; + align-items: center; + justify-content: center; + @include S(width, 15px); + @include S(height, 15px); + @include IncreasedClickArea(0px); + } + + + .value { + text-align: center; + @include S(min-width, 15px); + } + } + } +} diff --git a/src/css/main.scss b/src/css/main.scss index 35d54e23..f857050e 100644 --- a/src/css/main.scss +++ b/src/css/main.scss @@ -55,6 +55,10 @@ @import "ingame_hud/sandbox_controller"; @import "ingame_hud/standalone_advantages"; @import "ingame_hud/cat_memes"; +@import "ingame_hud/mode_menu_back"; +@import "ingame_hud/mode_menu_next"; +@import "ingame_hud/mode_menu"; +@import "ingame_hud/mode_settings"; // prettier-ignore $elements: @@ -71,6 +75,10 @@ ingame_HUD_PlacerVariants, ingame_HUD_PinnedShapes, ingame_HUD_GameMenu, ingame_HUD_KeybindingOverlay, +ingame_HUD_ModeMenuBack, +ingame_HUD_ModeMenuNext, +ingame_HUD_ModeMenu, +ingame_HUD_ModeSettings, ingame_HUD_Notifications, ingame_HUD_DebugInfo, ingame_HUD_EntityDebugger, @@ -113,6 +121,8 @@ body.uiHidden { #ingame_HUD_PlacementHints, #ingame_HUD_GameMenu, #ingame_HUD_PinnedShapes, + #ingame_HUD_ModeMenuBack, + #ingame_HUD_ModeMenuNext, #ingame_HUD_Notifications, #ingame_HUD_TutorialHints, #ingame_HUD_Waypoints, diff --git a/src/css/resources.scss b/src/css/resources.scss index 5bb3ea99..769829f6 100644 --- a/src/css/resources.scss +++ b/src/css/resources.scss @@ -1,6 +1,6 @@ $buildings: belt, cutter, miner, mixer, painter, rotater, balancer, stacker, trash, underground_belt, wire, constant_signal, logic_gate, lever, filter, wire_tunnel, display, virtual_processor, reader, storage, - transistor, analyzer, comparator, item_producer; + transistor, analyzer, comparator, item_producer, constant_producer, goal_acceptor; @each $building in $buildings { [data-icon="building_icons/#{$building}.png"] { @@ -13,7 +13,8 @@ $buildingsAndVariants: belt, balancer, underground_belt, underground_belt-tier2, cutter, cutter-quad, rotater, rotater-ccw, stacker, mixer, painter-double, painter-quad, trash, storage, reader, rotater-rotate180, display, constant_signal, wire, wire_tunnel, logic_gate-or, logic_gate-not, logic_gate-xor, analyzer, virtual_processor-rotater, virtual_processor-unstacker, item_producer, - virtual_processor-stacker, virtual_processor-painter, wire-second, painter, painter-mirrored, comparator; + constant_producer, virtual_processor-stacker, virtual_processor-painter, wire-second, painter, + painter-mirrored, comparator, goal_acceptor; @each $building in $buildingsAndVariants { [data-icon="building_tutorials/#{$building}.png"] { /* @load-async */ diff --git a/src/css/states/main_menu.scss b/src/css/states/main_menu.scss index b21d465f..5dbcf0e9 100644 --- a/src/css/states/main_menu.scss +++ b/src/css/states/main_menu.scss @@ -242,6 +242,16 @@ align-items: center; } + .modeButtons { + display: grid; + grid-template-columns: repeat(2, 1fr); + @include S(grid-column-gap, 10px); + align-items: start; + height: 100%; + width: 100%; + box-sizing: border-box; + } + .browserWarning { @include S(margin-bottom, 10px); background-color: $colorRedBright; @@ -285,6 +295,18 @@ @include S(margin-left, 15px); } + .playModeButton { + @include IncreasedClickArea(0px); + @include S(margin-top, 15px); + @include S(margin-left, 15px); + } + + .editModeButton { + @include IncreasedClickArea(0px); + @include S(margin-top, 15px); + @include S(margin-left, 15px); + } + .savegames { @include S(max-height, 105px); overflow-y: auto; @@ -439,6 +461,27 @@ } } + .bottomContainer { + display: flex; + align-items: center; + justify-content: center; + flex-direction: row; + @include S(padding-top, 10px); + height: 100%; + width: 100%; + box-sizing: border-box; + + .buttons { + display: grid; + grid-template-columns: repeat(2, 1fr); + @include S(grid-column-gap, 10px); + align-items: start; + height: 100%; + width: 100%; + box-sizing: border-box; + } + } + .footer { display: grid; flex-grow: 1; diff --git a/src/js/core/config.local.template.js b/src/js/core/config.local.template.js index 5e3cdad6..fc71c01e 100644 --- a/src/js/core/config.local.template.js +++ b/src/js/core/config.local.template.js @@ -62,6 +62,9 @@ export default { // Allows unlocked achievements to be logged to console in the local build // testAchievements: true, // ----------------------------------------------------------------------------------- + // Enables use of (some) existing flags within the puzzle mode context + // testPuzzleMode: true, + // ----------------------------------------------------------------------------------- // Disables the automatic switch to an overview when zooming out // disableMapOverview: true, // ----------------------------------------------------------------------------------- diff --git a/src/js/core/global_registries.js b/src/js/core/global_registries.js index ad45850c..723bf567 100644 --- a/src/js/core/global_registries.js +++ b/src/js/core/global_registries.js @@ -5,6 +5,7 @@ import { Factory } from "./factory"; * @typedef {import("../game/time/base_game_speed").BaseGameSpeed} BaseGameSpeed * @typedef {import("../game/component").Component} Component * @typedef {import("../game/base_item").BaseItem} BaseItem + * @typedef {import("../game/game_mode").GameMode} GameMode * @typedef {import("../game/meta_building").MetaBuilding} MetaBuilding @@ -19,6 +20,9 @@ export let gBuildingsByCategory = null; /** @type {FactoryTemplate} */ export let gComponentRegistry = new Factory("component"); +/** @type {FactoryTemplate} */ +export let gGameModeRegistry = new Factory("gameMode"); + /** @type {FactoryTemplate} */ export let gGameSpeedRegistry = new Factory("gamespeed"); diff --git a/src/js/game/buildings/constant_producer.js b/src/js/game/buildings/constant_producer.js new file mode 100644 index 00000000..2725402a --- /dev/null +++ b/src/js/game/buildings/constant_producer.js @@ -0,0 +1,41 @@ +/* typehints:start */ +import { Entity } from "../entity"; +/* typehints:end */ + +import { enumDirection, Vector } from "../../core/vector"; +import { enumConstantSignalType, ConstantSignalComponent } from "../components/constant_signal"; +import { ItemEjectorComponent } from "../components/item_ejector"; +import { enumItemProducerType, ItemProducerComponent } from "../components/item_producer"; +import { MetaBuilding } from "../meta_building"; + +export class MetaConstantProducerBuilding extends MetaBuilding { + constructor() { + super("constant_producer"); + } + + getSilhouetteColor() { + return "#bfd630"; + } + + /** + * Creates the entity at the given location + * @param {Entity} entity + */ + setupEntityComponents(entity) { + entity.addComponent( + new ItemEjectorComponent({ + slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }], + }) + ); + entity.addComponent( + new ItemProducerComponent({ + type: enumItemProducerType.wireless, + }) + ); + entity.addComponent( + new ConstantSignalComponent({ + type: enumConstantSignalType.wireless, + }) + ); + } +} diff --git a/src/js/game/buildings/goal_acceptor.js b/src/js/game/buildings/goal_acceptor.js new file mode 100644 index 00000000..bb50cd47 --- /dev/null +++ b/src/js/game/buildings/goal_acceptor.js @@ -0,0 +1,52 @@ +/* typehints:start */ +import { Entity } from "../entity"; +/* typehints:end */ + +import { enumDirection, Vector } from "../../core/vector"; +import { enumBeltReaderType, BeltReaderComponent } from "../components/belt_reader"; +import { GoalAcceptorComponent } from "../components/goal_acceptor"; +import { ItemEjectorComponent } from "../components/item_ejector"; +import { ItemAcceptorComponent } from "../components/item_acceptor"; +import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; +import { MetaBuilding } from "../meta_building"; + +export class MetaGoalAcceptorBuilding extends MetaBuilding { + constructor() { + super("goal_acceptor"); + } + + getSilhouetteColor() { + return "#ce418a"; + } + + /** + * Creates the entity at the given location + * @param {Entity} entity + */ + setupEntityComponents(entity) { + entity.addComponent( + new ItemAcceptorComponent({ + slots: [ + { + pos: new Vector(0, 0), + directions: [enumDirection.top], + }, + ], + }) + ); + + entity.addComponent( + new ItemProcessorComponent({ + processorType: enumItemProcessorTypes.goal, + }) + ); + + entity.addComponent( + new BeltReaderComponent({ + type: enumBeltReaderType.wireless, + }) + ); + + entity.addComponent(new GoalAcceptorComponent({})); + } +} diff --git a/src/js/game/buildings/item_producer.js b/src/js/game/buildings/item_producer.js index 477ed603..1140c8f1 100644 --- a/src/js/game/buildings/item_producer.js +++ b/src/js/game/buildings/item_producer.js @@ -39,6 +39,6 @@ export class MetaItemProducerBuilding extends MetaBuilding { ], }) ); - entity.addComponent(new ItemProducerComponent()); + entity.addComponent(new ItemProducerComponent({})); } } diff --git a/src/js/game/buildings/reader.js b/src/js/game/buildings/reader.js index 006d6582..62207564 100644 --- a/src/js/game/buildings/reader.js +++ b/src/js/game/buildings/reader.js @@ -110,6 +110,6 @@ export class MetaReaderBuilding extends MetaBuilding { }) ); - entity.addComponent(new BeltReaderComponent()); + entity.addComponent(new BeltReaderComponent({})); } } diff --git a/src/js/game/camera.js b/src/js/game/camera.js index 107d1fb4..a62745e8 100644 --- a/src/js/game/camera.js +++ b/src/js/game/camera.js @@ -392,13 +392,20 @@ export class Camera extends BasicSerializableObject { return rect.containsPoint(point.x, point.y); } + getMaximumZoom() { + return this.root.gameMode.getMaximumZoom() * this.root.app.platformWrapper.getScreenScale(); + } + + getMinimumZoom() { + return this.root.gameMode.getMinimumZoom() * this.root.app.platformWrapper.getScreenScale(); + } + /** * Returns if we can further zoom in * @returns {boolean} */ canZoomIn() { - const maxLevel = this.root.app.platformWrapper.getMaximumZoom(); - return this.zoomLevel <= maxLevel - 0.01; + return this.zoomLevel <= this.getMaximumZoom() - 0.01; } /** @@ -406,8 +413,7 @@ export class Camera extends BasicSerializableObject { * @returns {boolean} */ canZoomOut() { - const minLevel = this.root.app.platformWrapper.getMinimumZoom(); - return this.zoomLevel >= minLevel + 0.01; + return this.zoomLevel >= this.getMinimumZoom() + 0.01; } // EVENTS @@ -743,17 +749,30 @@ export class Camera extends BasicSerializableObject { if (G_IS_DEV && globalConfig.debug.disableZoomLimits) { return; } - const wrapper = this.root.app.platformWrapper; - assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *before* clamp: " + this.zoomLevel); - this.zoomLevel = clamp(this.zoomLevel, wrapper.getMinimumZoom(), wrapper.getMaximumZoom()); + this.zoomLevel = clamp(this.zoomLevel, this.getMinimumZoom(), this.getMaximumZoom()); assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *after* clamp: " + this.zoomLevel); if (this.desiredZoom) { - this.desiredZoom = clamp(this.desiredZoom, wrapper.getMinimumZoom(), wrapper.getMaximumZoom()); + this.desiredZoom = clamp(this.desiredZoom, this.getMinimumZoom(), this.getMaximumZoom()); } } + /** + * Clamps x, y position within set boundaries + * @param {Vector} vector + */ + clampToBounds(vector) { + if (!this.root.gameMode.hasBounds()) { + return; + } + + const bounds = this.root.gameMode.getBounds().allScaled(globalConfig.tileSize); + + vector.x = clamp(vector.x, bounds.x, bounds.x + bounds.w); + vector.y = clamp(vector.y, bounds.y, bounds.y + bounds.h); + } + /** * Updates the camera * @param {number} dt Delta time in milliseconds @@ -857,6 +876,7 @@ export class Camera extends BasicSerializableObject { // Panning this.currentPan = mixVector(this.currentPan, this.desiredPan, 0.06); this.center = this.center.add(this.currentPan.multiplyScalar((0.5 * dt) / this.zoomLevel)); + this.clampToBounds(this.center); } } @@ -921,6 +941,8 @@ export class Camera extends BasicSerializableObject { ((0.5 * dt) / this.zoomLevel) * this.root.app.settings.getMovementSpeed() ) ); + + this.clampToBounds(this.center); } /** @@ -1006,6 +1028,8 @@ export class Camera extends BasicSerializableObject { this.center.x += moveAmount * forceX * movementSpeed; this.center.y += moveAmount * forceY * movementSpeed; + + this.clampToBounds(this.center); } } } diff --git a/src/js/game/component_registry.js b/src/js/game/component_registry.js index f094e60d..9c9247e6 100644 --- a/src/js/game/component_registry.js +++ b/src/js/game/component_registry.js @@ -19,6 +19,7 @@ import { DisplayComponent } from "./components/display"; import { BeltReaderComponent } from "./components/belt_reader"; import { FilterComponent } from "./components/filter"; import { ItemProducerComponent } from "./components/item_producer"; +import { GoalAcceptorComponent } from "./components/goal_acceptor"; export function initComponentRegistry() { gComponentRegistry.register(StaticMapEntityComponent); @@ -41,6 +42,7 @@ export function initComponentRegistry() { gComponentRegistry.register(BeltReaderComponent); gComponentRegistry.register(FilterComponent); gComponentRegistry.register(ItemProducerComponent); + gComponentRegistry.register(GoalAcceptorComponent); // IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS diff --git a/src/js/game/components/belt_reader.js b/src/js/game/components/belt_reader.js index d451bab5..c7f05511 100644 --- a/src/js/game/components/belt_reader.js +++ b/src/js/game/components/belt_reader.js @@ -3,6 +3,12 @@ import { BaseItem } from "../base_item"; import { typeItemSingleton } from "../item_resolver"; import { types } from "../../savegame/serialization"; +/** @enum {string} */ +export const enumBeltReaderType = { + wired: "wired", + wireless: "wireless", +}; + export class BeltReaderComponent extends Component { static getId() { return "BeltReader"; @@ -10,13 +16,20 @@ export class BeltReaderComponent extends Component { static getSchema() { return { + type: types.string, lastItem: types.nullable(typeItemSingleton), }; } - constructor() { + /** + * @param {object} param0 + * @param {string=} param0.type + */ + constructor({ type = enumBeltReaderType.wired }) { super(); + this.type = type; + /** * Which items went through the reader, we only store the time * @type {Array} @@ -41,4 +54,8 @@ export class BeltReaderComponent extends Component { */ this.lastThroughputComputation = 0; } + + isWireless() { + return this.type === enumBeltReaderType.wireless; + } } diff --git a/src/js/game/components/constant_signal.js b/src/js/game/components/constant_signal.js index 286108be..d2186bda 100644 --- a/src/js/game/components/constant_signal.js +++ b/src/js/game/components/constant_signal.js @@ -4,6 +4,12 @@ import { Component } from "../component"; import { BaseItem } from "../base_item"; import { typeItemSingleton } from "../item_resolver"; +/** @enum {string} */ +export const enumConstantSignalType = { + wired: "wired", + wireless: "wireless", +}; + export class ConstantSignalComponent extends Component { static getId() { return "ConstantSignal"; @@ -11,6 +17,7 @@ export class ConstantSignalComponent extends Component { static getSchema() { return { + type: types.string, signal: types.nullable(typeItemSingleton), }; } @@ -21,15 +28,22 @@ export class ConstantSignalComponent extends Component { */ copyAdditionalStateTo(otherComponent) { otherComponent.signal = this.signal; + otherComponent.type = this.type; } /** * * @param {object} param0 + * @param {string=} param0.type * @param {BaseItem=} param0.signal The signal to store */ - constructor({ signal = null }) { + constructor({ signal = null, type = enumConstantSignalType.wired }) { super(); this.signal = signal; + this.type = type; + } + + isWireless() { + return this.type === enumConstantSignalType.wireless; } } diff --git a/src/js/game/components/goal_acceptor.js b/src/js/game/components/goal_acceptor.js new file mode 100644 index 00000000..72c157d7 --- /dev/null +++ b/src/js/game/components/goal_acceptor.js @@ -0,0 +1,22 @@ +import { BaseItem } from "../base_item"; +import { Component } from "../component"; + +export class GoalAcceptorComponent extends Component { + static getId() { + return "GoalAcceptor"; + } + + /** + * @param {object} param0 + * @param {BaseItem=} param0.item + * @param {number=} param0.rate + */ + constructor({ item = null, rate = null }) { + super(); + this.item = item; + this.rate = rate; + + this.achieved = false; + this.achievedOnce = false; + } +} diff --git a/src/js/game/components/item_processor.js b/src/js/game/components/item_processor.js index fd466662..166dd49c 100644 --- a/src/js/game/components/item_processor.js +++ b/src/js/game/components/item_processor.js @@ -19,6 +19,7 @@ export const enumItemProcessorTypes = { hub: "hub", filter: "filter", reader: "reader", + goal: "goal", }; /** @enum {string} */ @@ -104,7 +105,11 @@ export class ItemProcessorComponent extends Component { * @param {number} sourceSlot */ tryTakeItem(item, sourceSlot) { - if (this.type === enumItemProcessorTypes.hub || this.type === enumItemProcessorTypes.trash) { + if ( + this.type === enumItemProcessorTypes.hub || + this.type === enumItemProcessorTypes.trash || + this.type === enumItemProcessorTypes.goal + ) { // Hub has special logic .. not really nice but efficient. this.inputSlots.push({ item, sourceSlot }); return true; diff --git a/src/js/game/components/item_producer.js b/src/js/game/components/item_producer.js index ef3571e2..4cb36132 100644 --- a/src/js/game/components/item_producer.js +++ b/src/js/game/components/item_producer.js @@ -1,7 +1,33 @@ +import { types } from "../../savegame/serialization"; import { Component } from "../component"; +/** @enum {string} */ +export const enumItemProducerType = { + wired: "wired", + wireless: "wireless", +}; + export class ItemProducerComponent extends Component { static getId() { return "ItemProducer"; } + + static getSchema() { + return { + type: types.string, + }; + } + + /** + * @param {object} param0 + * @param {string=} param0.type + */ + constructor({ type = enumItemProducerType.wired }) { + super(); + this.type = type; + } + + isWireless() { + return this.type === enumItemProducerType.wireless; + } } diff --git a/src/js/game/core.js b/src/js/game/core.js index f4b3e9ee..d9d73cae 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -31,7 +31,7 @@ import { KeyActionMapper } from "./key_action_mapper"; import { GameLogic } from "./logic"; import { MapView } from "./map_view"; import { defaultBuildingVariant } from "./meta_building"; -import { RegularGameMode } from "./modes/regular"; +import { GameMode } from "./game_mode"; import { ProductionAnalytics } from "./production_analytics"; import { GameRoot } from "./root"; import { ShapeDefinitionManager } from "./shape_definition_manager"; @@ -82,7 +82,7 @@ export class GameCore { * @param {import("../states/ingame").InGameState} parentState * @param {Savegame} savegame */ - initializeRoot(parentState, savegame) { + initializeRoot(parentState, savegame, gameModeId) { // Construct the root element, this is the data representation of the game this.root = new GameRoot(this.app); this.root.gameState = parentState; @@ -104,7 +104,7 @@ export class GameCore { root.dynamicTickrate = new DynamicTickrate(root); // Init game mode - root.gameMode = new RegularGameMode(root); + root.gameMode = GameMode.create(root, gameModeId); // Init classes root.camera = new Camera(root); @@ -168,6 +168,10 @@ export class GameCore { this.root.gameIsFresh = true; this.root.map.seed = randomInt(0, 100000); + if (!this.root.gameMode.hasHub()) { + return; + } + // Place the hub const hub = gMetaBuildingRegistry.findByClass(MetaHubBuilding).createEntity({ root: this.root, diff --git a/src/js/game/entity_components.js b/src/js/game/entity_components.js index 7dee590a..163be9f9 100644 --- a/src/js/game/entity_components.js +++ b/src/js/game/entity_components.js @@ -19,6 +19,7 @@ import { DisplayComponent } from "./components/display"; import { BeltReaderComponent } from "./components/belt_reader"; import { FilterComponent } from "./components/filter"; import { ItemProducerComponent } from "./components/item_producer"; +import { GoalAcceptorComponent } from "./components/goal_acceptor"; /* typehints:end */ /** @@ -89,6 +90,9 @@ export class EntityComponentStorage { /** @type {ItemProducerComponent} */ this.ItemProducer; + /** @type {GoalAcceptorComponent} */ + this.GoalAcceptor; + /* typehints:end */ } } diff --git a/src/js/game/game_mode.js b/src/js/game/game_mode.js index 15403eb5..f2b46c55 100644 --- a/src/js/game/game_mode.js +++ b/src/js/game/game_mode.js @@ -1,71 +1,184 @@ /* typehints:start */ -import { enumHubGoalRewards } from "./tutorial_goals"; +import { GameRoot } from "./root"; +import { Rectangle } from "../core/rectangle"; /* typehints:end */ -import { GameRoot } from "./root"; +import { gGameModeRegistry } from "../core/global_registries"; +import { types, BasicSerializableObject } from "../savegame/serialization"; -/** @typedef {{ - * shape: string, - * amount: number - * }} UpgradeRequirement */ +/** @enum {string} */ +export const enumGameModeIds = { + puzzleEdit: "puzzleEditMode", + puzzlePlay: "puzzlePlayMode", + regular: "regularMode", +}; -/** @typedef {{ - * required: Array - * improvement?: number, - * excludePrevious?: boolean - * }} TierRequirement */ +/** @enum {string} */ +export const enumGameModeTypes = { + default: "defaultModeType", + puzzle: "puzzleModeType", +}; -/** @typedef {Array} UpgradeTiers */ +export class GameMode extends BasicSerializableObject { + /** @returns {string} */ + static getId() { + abstract; + return "unknownMode"; + } + + /** @returns {string} */ + static getType() { + abstract; + return "unknownType"; + } + /** + * @param {GameRoot} root + * @param {string} [id=Regular] + */ + static create(root, id = enumGameModeIds.regular) { + return new (gGameModeRegistry.findById(id))(root); + } -/** @typedef {{ - * shape: string, - * required: number, - * reward: enumHubGoalRewards, - * throughputOnly?: boolean - * }} LevelDefinition */ - -export class GameMode { /** - * * @param {GameRoot} root */ constructor(root) { + super(); this.root = root; + this.hudParts = {}; + this.buildings = {}; + } + + /** @returns {object} */ + serialize() { + return { + $: this.getId(), + data: super.serialize(), + }; + } + + /** @param {object} savedata */ + deserialize({ data }) { + super.deserialize(data, this.root); + } + + /** @returns {string} */ + getId() { + // @ts-ignore + return this.constructor.getId(); + } + + /** @returns {string} */ + getType() { + // @ts-ignore + return this.constructor.getType(); + } + + setBuildings(buildings) { + Object.assign(this.buildings, buildings); + } + + setHudParts(parts) { + Object.assign(this.hudParts, parts); } /** - * Should return all available upgrades - * @returns {Object} - */ - getUpgrades() { - abstract; - return null; - } - - /** - * Returns the blueprint shape key - * @returns {string} - */ - getBlueprintShapeKey() { - abstract; - return null; - } - - /** - * Returns the goals for all levels including their reward - * @returns {Array} - */ - getLevelDefinitions() { - abstract; - return null; - } - - /** - * Should return whether free play is available or if the game stops - * after the predefined levels + * @param {string} name - Class name of HUD Part * @returns {boolean} */ - getIsFreeplayAvailable() { + isHudPartExcluded(name) { + return this.hudParts[name] === false; + } + + /** + * @param {string} name - Class name of building + * @returns {boolean} + */ + isBuildingExcluded(name) { + return this.buildings[name] === false; + } + + /** @returns {boolean} */ + hasZone() { + return false; + } + + /** @returns {boolean} */ + hasHub() { return true; } + + /** @returns {boolean} */ + hasResources() { + return true; + } + + /** @returns {boolean} */ + hasBounds() { + return false; + } + + /** @returns {boolean} */ + isZoneRestricted() { + return false; + } + + /** @returns {boolean} */ + isBoundaryRestricted() { + return false; + } + + /** @returns {number} */ + getMinimumZoom() { + return 0.1; + } + + /** @returns {number} */ + getMaximumZoom() { + return 3.5; + } + + /** @returns {Object} */ + getUpgrades() { + return { + belt: [], + miner: [], + processors: [], + painting: [], + }; + } + + /** @returns {?Rectangle} */ + getZone() { + return null; + } + + /** + * @param {number} w + * @param {number} h + */ + expandZone(w = 0, h = 0) { + abstract; + return; + } + + /** @returns {?Rectangle} */ + getBounds() { + return null; + } + + /** @returns {array} */ + getLevelDefinitions() { + return []; + } + + /** @returns {boolean} */ + getIsFreeplayAvailable() { + return false; + } + + /** @returns {string} */ + getBlueprintShapeKey() { + return "CbCbCbRb:CwCwCwCw"; + } } diff --git a/src/js/game/game_mode_registry.js b/src/js/game/game_mode_registry.js new file mode 100644 index 00000000..03daceb0 --- /dev/null +++ b/src/js/game/game_mode_registry.js @@ -0,0 +1,10 @@ +import { gGameModeRegistry } from "../core/global_registries"; +import { PuzzleEditGameMode } from "./modes/puzzle_edit"; +import { PuzzlePlayGameMode } from "./modes/puzzle_play"; +import { RegularGameMode } from "./modes/regular"; + +export function initGameModeRegistry() { + gGameModeRegistry.register(PuzzleEditGameMode); + gGameModeRegistry.register(PuzzlePlayGameMode); + gGameModeRegistry.register(RegularGameMode); +} diff --git a/src/js/game/game_system_manager.js b/src/js/game/game_system_manager.js index 74ba798f..9220acaa 100644 --- a/src/js/game/game_system_manager.js +++ b/src/js/game/game_system_manager.js @@ -24,6 +24,9 @@ import { ItemProcessorOverlaysSystem } from "./systems/item_processor_overlays"; import { BeltReaderSystem } from "./systems/belt_reader"; import { FilterSystem } from "./systems/filter"; import { ItemProducerSystem } from "./systems/item_producer"; +import { ConstantProducerSystem } from "./systems/constant_producer"; +import { GoalAcceptorSystem } from "./systems/goal_acceptor"; +import { ZoneSystem } from "./systems/zone"; const logger = createLogger("game_system_manager"); @@ -100,6 +103,15 @@ export class GameSystemManager { /** @type {ItemProducerSystem} */ itemProducer: null, + /** @type {ConstantProducerSystem} */ + ConstantProducer: null, + + /** @type {GoalAcceptorSystem} */ + GoalAcceptor: null, + + /** @type {ZoneSystem} */ + zone: null, + /* typehints:end */ }; this.systemUpdateOrder = []; @@ -138,7 +150,9 @@ export class GameSystemManager { add("itemEjector", ItemEjectorSystem); - add("mapResources", MapResourcesSystem); + if (this.root.gameMode.hasResources()) { + add("mapResources", MapResourcesSystem); + } add("hub", HubSystem); @@ -165,6 +179,14 @@ export class GameSystemManager { add("itemProcessorOverlays", ItemProcessorOverlaysSystem); + add("constantProducer", ConstantProducerSystem); + + add("goalAcceptor", GoalAcceptorSystem); + + if (this.root.gameMode.hasZone()) { + add("zone", ZoneSystem); + } + logger.log("📦 There are", this.systemUpdateOrder.length, "game systems"); } diff --git a/src/js/game/hub_goals.js b/src/js/game/hub_goals.js index 327b6da7..b3536a3c 100644 --- a/src/js/game/hub_goals.js +++ b/src/js/game/hub_goals.js @@ -500,6 +500,7 @@ export class HubGoals extends BasicSerializableObject { switch (processorType) { case enumItemProcessorTypes.trash: case enumItemProcessorTypes.hub: + case enumItemProcessorTypes.goal: return 1e30; case enumItemProcessorTypes.balancer: return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt * 2; diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index d64f96a8..0c76437e 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -49,6 +49,10 @@ import { HUDStandaloneAdvantages } from "./parts/standalone_advantages"; import { HUDCatMemes } from "./parts/cat_memes"; import { HUDTutorialVideoOffer } from "./parts/tutorial_video_offer"; import { HUDConstantSignalEdit } from "./parts/constant_signal_edit"; +import { HUDModeMenuBack } from "./parts/mode_menu_back"; +import { HUDModeMenuNext } from "./parts/mode_menu_next"; +import { HUDModeMenu } from "./parts/mode_menu"; +import { HUDModeSettings } from "./parts/mode_settings"; export class GameHUD { /** @@ -74,45 +78,51 @@ export class GameHUD { unlockNotificationFinished: /** @type {TypedSignal<[]>} */ (new Signal()), }; - this.parts = { - buildingsToolbar: new HUDBuildingsToolbar(this.root), - wiresToolbar: new HUDWiresToolbar(this.root), - blueprintPlacer: new HUDBlueprintPlacer(this.root), - buildingPlacer: new HUDBuildingPlacer(this.root), - unlockNotification: new HUDUnlockNotification(this.root), - gameMenu: new HUDGameMenu(this.root), - massSelector: new HUDMassSelector(this.root), - shop: new HUDShop(this.root), - statistics: new HUDStatistics(this.root), - waypoints: new HUDWaypoints(this.root), - wireInfo: new HUDWireInfo(this.root), - leverToggle: new HUDLeverToggle(this.root), - constantSignalEdit: new HUDConstantSignalEdit(this.root), + this.initParts({ + buildingsToolbar: HUDBuildingsToolbar, + wiresToolbar: HUDWiresToolbar, + blueprintPlacer: HUDBlueprintPlacer, + buildingPlacer: HUDBuildingPlacer, + unlockNotification: HUDUnlockNotification, + gameMenu: HUDGameMenu, + massSelector: HUDMassSelector, + shop: HUDShop, + statistics: HUDStatistics, + waypoints: HUDWaypoints, + wireInfo: HUDWireInfo, + leverToggle: HUDLeverToggle, + constantSignalEdit: HUDConstantSignalEdit, + modeMenuBack: HUDModeMenuBack, + modeMenuNext: HUDModeMenuNext, + modeMenu: HUDModeMenu, + modeSettings: HUDModeSettings, // Must always exist - pinnedShapes: new HUDPinnedShapes(this.root), - notifications: new HUDNotifications(this.root), - settingsMenu: new HUDSettingsMenu(this.root), - debugInfo: new HUDDebugInfo(this.root), - dialogs: new HUDModalDialogs(this.root), - screenshotExporter: new HUDScreenshotExporter(this.root), - shapeViewer: new HUDShapeViewer(this.root), + pinnedShapes: HUDPinnedShapes, + notifications: HUDNotifications, + settingsMenu: HUDSettingsMenu, + debugInfo: HUDDebugInfo, + dialogs: HUDModalDialogs, + screenshotExporter: HUDScreenshotExporter, + shapeViewer: HUDShapeViewer, - wiresOverlay: new HUDWiresOverlay(this.root), - layerPreview: new HUDLayerPreview(this.root), + wiresOverlay: HUDWiresOverlay, + layerPreview: HUDLayerPreview, - minerHighlight: new HUDMinerHighlight(this.root), - tutorialVideoOffer: new HUDTutorialVideoOffer(this.root), + minerHighlight: HUDMinerHighlight, + tutorialVideoOffer: HUDTutorialVideoOffer, // Typing hints /* typehints:start */ /** @type {HUDChangesDebugger} */ changesDebugger: null, /* typehints:end */ - }; + }); if (!IS_MOBILE) { - this.parts.keybindingOverlay = new HUDKeybindingOverlay(this.root); + if (!this.root.gameMode.isHudPartExcluded(HUDKeybindingOverlay.name)) { + this.parts.keybindingOverlay = new HUDKeybindingOverlay(this.root); + } } if (G_IS_DEV && globalConfig.debug.enableEntityInspector) { @@ -130,8 +140,13 @@ export class GameHUD { } if (this.root.app.settings.getAllSettings().offerHints) { - this.parts.tutorialHints = new HUDPartTutorialHints(this.root); - this.parts.interactiveTutorial = new HUDInteractiveTutorial(this.root); + if (!this.root.gameMode.isHudPartExcluded(HUDPartTutorialHints.name)) { + this.parts.tutorialHints = new HUDPartTutorialHints(this.root); + } + + if (!this.root.gameMode.isHudPartExcluded(HUDInteractiveTutorial.name)) { + this.parts.interactiveTutorial = new HUDInteractiveTutorial(this.root); + } } if (this.root.app.settings.getAllSettings().vignette) { @@ -170,6 +185,21 @@ export class GameHUD { /* dev:end*/ } + /** @param {object} parts */ + initParts(parts) { + this.parts = {}; + + for (let key in parts) { + const Part = parts[key]; + + if (!Part || this.root.gameMode.isHudPartExcluded(Part.name)) { + continue; + } + + this.parts[key] = new Part(this.root); + } + } + /** * Attempts to close all overlays */ diff --git a/src/js/game/hud/parts/base_toolbar.js b/src/js/game/hud/parts/base_toolbar.js index b3f5abfc..01e9fafa 100644 --- a/src/js/game/hud/parts/base_toolbar.js +++ b/src/js/game/hud/parts/base_toolbar.js @@ -23,8 +23,8 @@ export class HUDBaseToolbar extends BaseHUDPart { ) { super(root); - this.primaryBuildings = primaryBuildings; - this.secondaryBuildings = secondaryBuildings; + this.primaryBuildings = this.filterBuildings(primaryBuildings); + this.secondaryBuildings = this.filterBuildings(secondaryBuildings); this.visibilityCondition = visibilityCondition; this.htmlElementId = htmlElementId; this.layer = layer; @@ -47,6 +47,24 @@ export class HUDBaseToolbar extends BaseHUDPart { this.element = makeDiv(parent, this.htmlElementId, ["ingame_buildingsToolbar"], ""); } + /** + * @param {Array} buildings + * @returns {Array} + */ + filterBuildings(buildings) { + const filtered = []; + + for (let i = 0; i < buildings.length; i++) { + if (this.root.gameMode.isBuildingExcluded(buildings[i].name)) { + continue; + } + + filtered.push(buildings[i]); + } + + return filtered; + } + /** * Returns all buildings * @returns {Array} diff --git a/src/js/game/hud/parts/buildings_toolbar.js b/src/js/game/hud/parts/buildings_toolbar.js index 05ffc795..2881eece 100644 --- a/src/js/game/hud/parts/buildings_toolbar.js +++ b/src/js/game/hud/parts/buildings_toolbar.js @@ -15,12 +15,15 @@ import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt"; import { HUDBaseToolbar } from "./base_toolbar"; import { MetaStorageBuilding } from "../../buildings/storage"; import { MetaItemProducerBuilding } from "../../buildings/item_producer"; -import { queryParamOptions } from "../../../core/query_parameters"; +import { MetaConstantProducerBuilding } from "../../buildings/constant_producer"; +import { MetaGoalAcceptorBuilding } from "../../buildings/goal_acceptor"; export class HUDBuildingsToolbar extends HUDBaseToolbar { constructor(root) { super(root, { primaryBuildings: [ + MetaConstantProducerBuilding, + MetaGoalAcceptorBuilding, MetaBeltBuilding, MetaBalancerBuilding, MetaUndergroundBeltBuilding, @@ -31,7 +34,7 @@ export class HUDBuildingsToolbar extends HUDBaseToolbar { MetaMixerBuilding, MetaPainterBuilding, MetaTrashBuilding, - ...(queryParamOptions.sandboxMode || G_IS_DEV ? [MetaItemProducerBuilding] : []), + MetaItemProducerBuilding, ], secondaryBuildings: [ MetaStorageBuilding, diff --git a/src/js/game/hud/parts/mode_menu.js b/src/js/game/hud/parts/mode_menu.js new file mode 100644 index 00000000..cb05d6a2 --- /dev/null +++ b/src/js/game/hud/parts/mode_menu.js @@ -0,0 +1,17 @@ +import { BaseHUDPart } from "../base_hud_part"; +import { makeDiv } from "../../../core/utils"; + +export class HUDModeMenu extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv(parent, "ingame_HUD_ModeMenu"); + + this.settingsButton = makeDiv(this.element, null, ["button", "settings"]); + this.trackClicks(this.settingsButton, this.openSettings); + } + + openSettings() { + this.root.hud.parts.modeSettings.toggle(); + } + + initialize() {} +} diff --git a/src/js/game/hud/parts/mode_menu_back.js b/src/js/game/hud/parts/mode_menu_back.js new file mode 100644 index 00000000..ff729706 --- /dev/null +++ b/src/js/game/hud/parts/mode_menu_back.js @@ -0,0 +1,23 @@ +import { BaseHUDPart } from "../base_hud_part"; +import { makeDiv } from "../../../core/utils"; +import { T } from "../../../translations"; + +export class HUDModeMenuBack extends BaseHUDPart { + createElements(parent) { + const key = this.root.gameMode.getId(); + + this.element = makeDiv(parent, "ingame_HUD_ModeMenuBack"); + this.button = document.createElement("button"); + this.button.classList.add("button"); + this.button.textContent = "⬅ " + T.ingame.modeMenu[key].back.title; + this.element.appendChild(this.button); + + this.trackClicks(this.button, this.back); + } + + initialize() {} + + back() { + this.root.gameState.goBackToMenu(); + } +} diff --git a/src/js/game/hud/parts/mode_menu_next.js b/src/js/game/hud/parts/mode_menu_next.js new file mode 100644 index 00000000..a3698269 --- /dev/null +++ b/src/js/game/hud/parts/mode_menu_next.js @@ -0,0 +1,23 @@ +import { BaseHUDPart } from "../base_hud_part"; +import { makeDiv } from "../../../core/utils"; +import { T } from "../../../translations"; + +export class HUDModeMenuNext extends BaseHUDPart { + createElements(parent) { + const key = this.root.gameMode.getId(); + + this.element = makeDiv(parent, "ingame_HUD_ModeMenuNext"); + this.button = document.createElement("button"); + this.button.classList.add("button"); + this.button.textContent = T.ingame.modeMenu[key].next.title + " ➡ "; + this.element.appendChild(this.button); + + this.content = makeDiv(this.element, null, ["content"], T.ingame.modeMenu[key].next.desc); + + this.trackClicks(this.button, this.next); + } + + initialize() {} + + next() {} +} diff --git a/src/js/game/hud/parts/mode_settings.js b/src/js/game/hud/parts/mode_settings.js new file mode 100644 index 00000000..a1dd220a --- /dev/null +++ b/src/js/game/hud/parts/mode_settings.js @@ -0,0 +1,67 @@ +import { makeDiv } from "../../../core/utils"; +import { BaseHUDPart } from "../base_hud_part"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; + +export class HUDModeSettings extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv(parent, "ingame_HUD_ModeSettings"); + + const bind = (selector, handler) => this.trackClicks(this.element.querySelector(selector), handler); + + if (this.root.gameMode.hasZone()) { + this.zone = makeDiv( + this.element, + null, + ["section", "zone"], + ` + + +
+
+ + + + +
+ +
+ + + + +
+
` + ); + + bind(".zoneWidth .minus", () => this.modifyZone(-1, 0)); + bind(".zoneWidth .plus", () => this.modifyZone(1, 0)); + bind(".zoneHeight .minus", () => this.modifyZone(0, -1)); + bind(".zoneHeight .plus", () => this.modifyZone(0, 1)); + } + } + + initialize() { + this.visible = false; + this.domAttach = new DynamicDomAttach(this.root, this.element); + this.updateZoneValues(); + } + + modifyZone(width, height) { + this.root.gameMode.expandZone(width, height); + this.updateZoneValues(); + } + + updateZoneValues() { + const zone = this.root.gameMode.getZone(); + this.element.querySelector(".zoneWidth > .value").textContent = String(zone.w); + this.element.querySelector(".zoneHeight > .value").textContent = String(zone.h); + } + + toggle() { + this.visible = !this.visible; + } + + update() { + this.domAttach.update(this.visible); + } +} diff --git a/src/js/game/hud/parts/waypoints.js b/src/js/game/hud/parts/waypoints.js index 1a0e3739..2e0bc159 100644 --- a/src/js/game/hud/parts/waypoints.js +++ b/src/js/game/hud/parts/waypoints.js @@ -100,16 +100,14 @@ export class HUDWaypoints extends BaseHUDPart { this.directionIndicatorSprite = Loader.getSprite("sprites/misc/hub_direction_indicator.png"); - /** @type {Array} - */ - this.waypoints = [ - { - label: null, - center: { x: 0, y: 0 }, - zoomLevel: 3, - layer: gMetaBuildingRegistry.findByClass(MetaHubBuilding).getLayer(), - }, - ]; + /** @type {Array} */ + this.waypoints = []; + this.waypoints.push({ + label: null, + center: { x: 0, y: 0 }, + zoomLevel: 3, + layer: gMetaBuildingRegistry.findByClass(MetaHubBuilding).getLayer(), + }); // Create a buffer we can use to measure text this.dummyBuffer = makeOffscreenBuffer(1, 1, { diff --git a/src/js/game/key_action_mapper.js b/src/js/game/key_action_mapper.js index 13f33d66..ad9f6e8e 100644 --- a/src/js/game/key_action_mapper.js +++ b/src/js/game/key_action_mapper.js @@ -49,6 +49,10 @@ export const KEYMAPPINGS = { }, buildings: { + // Puzzle buildings + constant_producer: { keyCode: key("H") }, + goal_acceptor: { keyCode: key("N") }, + // Primary Toolbar belt: { keyCode: key("1") }, balancer: { keyCode: key("2") }, @@ -262,6 +266,8 @@ export function getStringForKeyCode(code) { return "."; case 191: return "/"; + case 192: + return "`"; case 219: return "["; case 220: diff --git a/src/js/game/map_chunk_view.js b/src/js/game/map_chunk_view.js index 848afbab..06ff7337 100644 --- a/src/js/game/map_chunk_view.js +++ b/src/js/game/map_chunk_view.js @@ -41,7 +41,14 @@ export class MapChunkView extends MapChunk { */ drawBackgroundLayer(parameters) { const systems = this.root.systemMgr.systems; - systems.mapResources.drawChunk(parameters, this); + if (this.root.gameMode.hasZone()) { + systems.zone.drawChunk(parameters, this); + } + + if (this.root.gameMode.hasResources()) { + systems.mapResources.drawChunk(parameters, this); + } + systems.beltUnderlays.drawChunk(parameters, this); systems.belt.drawChunk(parameters, this); } @@ -69,6 +76,7 @@ export class MapChunkView extends MapChunk { systems.lever.drawChunk(parameters, this); systems.display.drawChunk(parameters, this); systems.storage.drawChunk(parameters, this); + systems.constantProducer.drawChunk(parameters, this); systems.itemProcessorOverlays.drawChunk(parameters, this); } diff --git a/src/js/game/meta_building_registry.js b/src/js/game/meta_building_registry.js index 0613103e..90a253f0 100644 --- a/src/js/game/meta_building_registry.js +++ b/src/js/game/meta_building_registry.js @@ -5,10 +5,12 @@ import { MetaAnalyzerBuilding } from "./buildings/analyzer"; import { enumBalancerVariants, MetaBalancerBuilding } from "./buildings/balancer"; import { MetaBeltBuilding } from "./buildings/belt"; import { MetaComparatorBuilding } from "./buildings/comparator"; +import { MetaConstantProducerBuilding } from "./buildings/constant_producer"; import { MetaConstantSignalBuilding } from "./buildings/constant_signal"; import { enumCutterVariants, MetaCutterBuilding } from "./buildings/cutter"; import { MetaDisplayBuilding } from "./buildings/display"; import { MetaFilterBuilding } from "./buildings/filter"; +import { MetaGoalAcceptorBuilding } from "./buildings/goal_acceptor"; import { MetaHubBuilding } from "./buildings/hub"; import { MetaItemProducerBuilding } from "./buildings/item_producer"; import { MetaLeverBuilding } from "./buildings/lever"; @@ -45,6 +47,7 @@ export function initMetaBuildingRegistry() { gMetaBuildingRegistry.register(MetaStorageBuilding); gMetaBuildingRegistry.register(MetaBeltBuilding); gMetaBuildingRegistry.register(MetaUndergroundBeltBuilding); + gMetaBuildingRegistry.register(MetaGoalAcceptorBuilding); gMetaBuildingRegistry.register(MetaHubBuilding); gMetaBuildingRegistry.register(MetaWireBuilding); gMetaBuildingRegistry.register(MetaConstantSignalBuilding); @@ -59,6 +62,7 @@ export function initMetaBuildingRegistry() { gMetaBuildingRegistry.register(MetaAnalyzerBuilding); gMetaBuildingRegistry.register(MetaComparatorBuilding); gMetaBuildingRegistry.register(MetaItemProducerBuilding); + gMetaBuildingRegistry.register(MetaConstantProducerBuilding); // Belt registerBuildingVariant(1, MetaBeltBuilding, defaultBuildingVariant, 0); @@ -165,6 +169,12 @@ export function initMetaBuildingRegistry() { // Item producer registerBuildingVariant(61, MetaItemProducerBuilding); + // Constant producer + registerBuildingVariant(62, MetaConstantProducerBuilding); + + // Goal acceptor + registerBuildingVariant(63, MetaGoalAcceptorBuilding); + // Propagate instances for (const key in gBuildingVariants) { gBuildingVariants[key].metaInstance = gMetaBuildingRegistry.findByClass( diff --git a/src/js/game/modes/puzzle.js b/src/js/game/modes/puzzle.js new file mode 100644 index 00000000..2f7f50ca --- /dev/null +++ b/src/js/game/modes/puzzle.js @@ -0,0 +1,129 @@ +/* typehints:start */ +import { GameRoot } from "../root"; +/* typehints:end */ + +import { Rectangle } from "../../core/rectangle"; +import { types } from "../../savegame/serialization"; +import { enumGameModeTypes, GameMode } from "../game_mode"; +import { HUDGameMenu } from "../hud/parts/game_menu"; +import { HUDInteractiveTutorial } from "../hud/parts/interactive_tutorial"; +import { HUDKeybindingOverlay } from "../hud/parts/keybinding_overlay"; +import { HUDPartTutorialHints } from "../hud/parts/tutorial_hints"; +import { HUDPinnedShapes } from "../hud/parts/pinned_shapes"; +import { HUDWaypoints } from "../hud/parts/waypoints"; + +export class PuzzleGameMode extends GameMode { + static getType() { + return enumGameModeTypes.puzzle; + } + + /** @returns {object} */ + static getSchema() { + return { + zoneHeight: types.uint, + zoneWidth: types.uint, + }; + } + + /** @param {GameRoot} root */ + constructor(root) { + super(root); + + const data = this.getSaveData(); + + this.setHudParts({ + [HUDGameMenu.name]: false, + [HUDInteractiveTutorial.name]: false, + [HUDKeybindingOverlay.name]: false, + [HUDPartTutorialHints.name]: false, + [HUDPinnedShapes.name]: false, + [HUDWaypoints.name]: false, + }); + + this.setDimensions(data.zoneWidth, data.zoneHeight); + } + + setDimensions(w = 16, h = 9) { + this.zoneWidth = w < 2 ? 2 : w; + this.zoneHeight = h < 2 ? 2 : h; + this.boundsHeight = this.zoneHeight < 8 ? 8 : this.zoneHeight; + this.boundsWidth = this.zoneWidth < 8 ? 8 : this.zoneWidth; + } + + getSaveData() { + const save = this.root.savegame.getCurrentDump(); + + if (!save) { + return {}; + } + + return save.gameMode.data; + } + + createCenteredRectangle(width, height) { + return new Rectangle(-Math.ceil(width / 2), -Math.ceil(height / 2), width, height); + } + + getBounds() { + if (this.bounds) { + return this.bounds; + } + + this.bounds = this.createCenteredRectangle(this.boundsWidth, this.boundsHeight); + + return this.bounds; + } + + getZone() { + if (this.zone) { + return this.zone; + } + + this.zone = this.createCenteredRectangle(this.zoneWidth, this.zoneHeight); + + return this.zone; + } + + /** + * Overrides GameMode's implementation to treat buildings like a whitelist + * instead of a blacklist by default. + * @param {string} name - Class name of building + * @returns {boolean} + */ + isBuildingExcluded(name) { + return this.buildings[name] !== true; + } + + isInBounds(x, y) { + return this.bounds.containsPoint(x, y); + } + + isInZone(x, y) { + return this.zone.containsPoint(x, y); + } + + hasZone() { + return true; + } + + hasHub() { + return false; + } + + hasResources() { + return false; + } + + hasBounds() { + return true; + } + + getMinimumZoom() { + return 1; + } + + /** @returns {boolean} */ + getIsFreeplayAvailable() { + return true; + } +} diff --git a/src/js/game/modes/puzzle_edit.js b/src/js/game/modes/puzzle_edit.js new file mode 100644 index 00000000..f927b001 --- /dev/null +++ b/src/js/game/modes/puzzle_edit.js @@ -0,0 +1,52 @@ +/* typehints:start */ +import { GameRoot } from "../root"; +/* typehints:end */ + +// import { MetaBeltBuilding } from "../buildings/belt"; +import { MetaConstantProducerBuilding } from "../buildings/constant_producer"; +import { MetaGoalAcceptorBuilding } from "../buildings/goal_acceptor"; +// import { MetaItemProducerBuilding } from "../buildings/item_producer"; +import { enumGameModeIds } from "../game_mode"; +import { PuzzleGameMode } from "./puzzle"; + +export class PuzzleEditGameMode extends PuzzleGameMode { + static getId() { + return enumGameModeIds.puzzleEdit; + } + + static getSchema() { + return {}; + } + + /** @param {GameRoot} root */ + constructor(root) { + super(root); + + this.playtest = false; + + this.setBuildings({ + [MetaConstantProducerBuilding.name]: true, + [MetaGoalAcceptorBuilding.name]: true, + }); + } + + isZoneRestricted() { + return !this.playtest; + } + + isBoundaryRestricted() { + return this.playtest; + } + + expandZone(w = 0, h = 0) { + if (this.zoneWidth + w > 0) { + this.zoneWidth += w; + } + + if (this.zoneHeight + h > 0) { + this.zoneHeight += h; + } + + this.zone = this.createCenteredRectangle(this.zoneWidth, this.zoneHeight); + } +} diff --git a/src/js/game/modes/puzzle_play.js b/src/js/game/modes/puzzle_play.js new file mode 100644 index 00000000..a2d090fd --- /dev/null +++ b/src/js/game/modes/puzzle_play.js @@ -0,0 +1,17 @@ +/* typehints:start */ +import { GameRoot } from "../root"; +/* typehints:end */ + +import { PuzzleGameMode } from "./puzzle"; +import { enumGameModeIds } from "../game_mode"; + +export class PuzzlePlayGameMode extends PuzzleGameMode { + static getId() { + return enumGameModeIds.puzzlePlay; + } + + /** @param {GameRoot} root */ + constructor(root) { + super(root); + } +} diff --git a/src/js/game/modes/regular.js b/src/js/game/modes/regular.js index e99f4a7c..1f3626ef 100644 --- a/src/js/game/modes/regular.js +++ b/src/js/game/modes/regular.js @@ -1,19 +1,50 @@ +/* typehints:start */ +import { GameRoot } from "../root"; +/* typehints:end */ + +import { queryParamOptions } from "../../core/query_parameters"; import { findNiceIntegerValue } from "../../core/utils"; -import { GameMode } from "../game_mode"; +import { MetaConstantProducerBuilding } from "../buildings/constant_producer"; +import { MetaGoalAcceptorBuilding } from "../buildings/goal_acceptor"; +import { MetaItemProducerBuilding } from "../buildings/item_producer"; +import { HUDModeMenuBack } from "../hud/parts/mode_menu_back"; +import { HUDModeMenuNext } from "../hud/parts/mode_menu_next"; +import { HUDModeMenu } from "../hud/parts/mode_menu"; +import { HUDModeSettings } from "../hud/parts/mode_settings"; +import { enumGameModeIds, enumGameModeTypes, GameMode } from "../game_mode"; import { ShapeDefinition } from "../shape_definition"; import { enumHubGoalRewards } from "../tutorial_goals"; +/** @typedef {{ + * shape: string, + * amount: number + * }} UpgradeRequirement */ + +/** @typedef {{ + * required: Array + * improvement?: number, + * excludePrevious?: boolean + * }} TierRequirement */ + +/** @typedef {Array} UpgradeTiers */ + +/** @typedef {{ + * shape: string, + * required: number, + * reward: enumHubGoalRewards, + * throughputOnly?: boolean + * }} LevelDefinition */ + const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw"; const finalGameShape = "RuCw--Cw:----Ru--"; const preparementShape = "CpRpCp--:SwSwSwSw"; -const blueprintShape = "CbCbCbRb:CwCwCwCw"; // Tiers need % of the previous tier as requirement too const tierGrowth = 2.5; /** * Generates all upgrades - * @returns {Object} */ + * @returns {Object} */ function generateUpgrades(limitedVersion = false) { const fixedImprovements = [0.5, 0.5, 1, 1, 2, 1, 1]; const numEndgameUpgrades = limitedVersion ? 0 : 1000 - fixedImprovements.length - 1; @@ -454,27 +485,58 @@ const fullVersionLevels = generateLevelDefinitions(false); const demoVersionLevels = generateLevelDefinitions(true); export class RegularGameMode extends GameMode { - constructor(root) { - super(root); + static getId() { + return enumGameModeIds.regular; } + static getType() { + return enumGameModeTypes.default; + } + + /** @param {GameRoot} root */ + constructor(root) { + super(root); + + this.setHudParts({ + [HUDModeMenuBack.name]: false, + [HUDModeMenuNext.name]: false, + [HUDModeMenu.name]: false, + [HUDModeSettings.name]: false, + }); + + this.setBuildings({ + [MetaConstantProducerBuilding.name]: false, + [MetaGoalAcceptorBuilding.name]: false, + [MetaItemProducerBuilding.name]: queryParamOptions.sandboxMode || G_IS_DEV, + }); + } + + /** + * Should return all available upgrades + * @returns {Object} + */ getUpgrades() { return this.root.app.restrictionMgr.getHasExtendedUpgrades() ? fullVersionUpgrades : demoVersionUpgrades; } - getIsFreeplayAvailable() { - return this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay(); - } - - getBlueprintShapeKey() { - return blueprintShape; - } - + /** + * Returns the goals for all levels including their reward + * @returns {Array} + */ getLevelDefinitions() { return this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay() ? fullVersionLevels : demoVersionLevels; } + + /** + * Should return whether free play is available or if the game stops + * after the predefined levels + * @returns {boolean} + */ + getIsFreeplayAvailable() { + return this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay(); + } } diff --git a/src/js/game/systems/belt_reader.js b/src/js/game/systems/belt_reader.js index fbd00b6c..f6080aa9 100644 --- a/src/js/game/systems/belt_reader.js +++ b/src/js/game/systems/belt_reader.js @@ -14,7 +14,6 @@ export class BeltReaderSystem extends GameSystemWithFilter { const minimumTimeForThroughput = now - 1; for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; - const readerComp = entity.components.BeltReader; const pinsComp = entity.components.WiredPins; @@ -23,12 +22,14 @@ export class BeltReaderSystem extends GameSystemWithFilter { readerComp.lastItemTimes.shift(); } - pinsComp.slots[1].value = readerComp.lastItem; - pinsComp.slots[0].value = - (readerComp.lastItemTimes[readerComp.lastItemTimes.length - 1] || 0) > - minimumTimeForThroughput - ? BOOL_TRUE_SINGLETON - : BOOL_FALSE_SINGLETON; + if (!entity.components.BeltReader.isWireless()) { + pinsComp.slots[1].value = readerComp.lastItem; + pinsComp.slots[0].value = + (readerComp.lastItemTimes[readerComp.lastItemTimes.length - 1] || 0) > + minimumTimeForThroughput + ? BOOL_TRUE_SINGLETON + : BOOL_FALSE_SINGLETON; + } if (now - readerComp.lastThroughputComputation > 0.5) { // Compute throughput diff --git a/src/js/game/systems/constant_producer.js b/src/js/game/systems/constant_producer.js new file mode 100644 index 00000000..9b1ec96f --- /dev/null +++ b/src/js/game/systems/constant_producer.js @@ -0,0 +1,54 @@ +/* typehints:start */ +import { GameRoot } from "../root"; +/* typehints:end */ + +import { globalConfig } from "../../core/config"; +import { ConstantSignalComponent } from "../components/constant_signal"; +import { ItemProducerComponent } from "../components/item_producer"; +import { GameSystemWithFilter } from "../game_system_with_filter"; + +export class ConstantProducerSystem extends GameSystemWithFilter { + /** @param {GameRoot} root */ + constructor(root) { + super(root, [ConstantSignalComponent, ItemProducerComponent]); + } + + update() { + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + const producerComp = entity.components.ItemProducer; + const signalComp = entity.components.ConstantSignal; + + if (!producerComp.isWireless() || !signalComp.isWireless()) { + continue; + } + + const ejectorComp = entity.components.ItemEjector; + + ejectorComp.tryEject(0, signalComp.signal); + } + } + + drawChunk(parameters, chunk) { + const contents = chunk.containedEntitiesByLayer.regular; + for (let i = 0; i < contents.length; ++i) { + const producerComp = contents[i].components.ItemProducer; + const signalComp = contents[i].components.ConstantSignal; + + if (!producerComp || !producerComp.isWireless() || !signalComp || !signalComp.isWireless()) { + return; + } + + const staticComp = contents[i].components.StaticMapEntity; + const item = signalComp.signal; + + if (!item) { + return; + } + + // TODO: Better looking overlay + const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); + item.drawItemCenteredClipped(center.x, center.y, parameters, globalConfig.tileSize); + } + } +} diff --git a/src/js/game/systems/constant_signal.js b/src/js/game/systems/constant_signal.js index d698c1d5..6a9c2a02 100644 --- a/src/js/game/systems/constant_signal.js +++ b/src/js/game/systems/constant_signal.js @@ -6,9 +6,10 @@ import { fillInLinkIntoTranslation } from "../../core/utils"; import { T } from "../../translations"; import { BaseItem } from "../base_item"; import { enumColors } from "../colors"; -import { ConstantSignalComponent } from "../components/constant_signal"; +import { enumConstantSignalType, ConstantSignalComponent } from "../components/constant_signal"; import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; +import { HUDPinnedShapes } from "../hud/parts/pinned_shapes"; import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item"; import { COLOR_ITEM_SINGLETONS } from "../items/color_item"; import { ShapeDefinition } from "../shape_definition"; @@ -26,8 +27,13 @@ export class ConstantSignalSystem extends GameSystemWithFilter { // Set signals for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; - const pinsComp = entity.components.WiredPins; const signalComp = entity.components.ConstantSignal; + + if (signalComp.isWireless()) { + continue; + } + + const pinsComp = entity.components.WiredPins; pinsComp.slots[0].value = signalComp.signal; } } @@ -54,23 +60,33 @@ export class ConstantSignalSystem extends GameSystemWithFilter { validator: val => this.parseSignalCode(val), }); + const items = [ + BOOL_FALSE_SINGLETON, + BOOL_TRUE_SINGLETON, + ...Object.values(COLOR_ITEM_SINGLETONS), + this.root.shapeDefinitionMgr.getShapeItemFromShortKey(this.root.gameMode.getBlueprintShapeKey()), + ]; + + if (this.root.gameMode.hasHub()) { + items.push( + this.root.shapeDefinitionMgr.getShapeItemFromDefinition( + this.root.hubGoals.currentGoal.definition + ) + ); + } + + if (!this.root.gameMode.isHudPartExcluded(HUDPinnedShapes.name)) { + items.push( + ...this.root.hud.parts.pinnedShapes.pinnedShapes.map(key => + this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key) + ) + ); + } + const itemInput = new FormElementItemChooser({ id: "signalItem", label: null, - items: [ - BOOL_FALSE_SINGLETON, - BOOL_TRUE_SINGLETON, - ...Object.values(COLOR_ITEM_SINGLETONS), - this.root.shapeDefinitionMgr.getShapeItemFromDefinition( - this.root.hubGoals.currentGoal.definition - ), - this.root.shapeDefinitionMgr.getShapeItemFromShortKey( - this.root.gameMode.getBlueprintShapeKey() - ), - ...this.root.hud.parts.pinnedShapes.pinnedShapes.map(key => - this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key) - ), - ], + items, }); const dialog = new DialogWithForm({ @@ -103,7 +119,6 @@ export class ConstantSignalSystem extends GameSystemWithFilter { } if (itemInput.chosenItem) { - console.log(itemInput.chosenItem); constantComp.signal = itemInput.chosenItem; } else { constantComp.signal = this.parseSignalCode(signalValueInput.getValue()); diff --git a/src/js/game/systems/goal_acceptor.js b/src/js/game/systems/goal_acceptor.js new file mode 100644 index 00000000..e24eb80b --- /dev/null +++ b/src/js/game/systems/goal_acceptor.js @@ -0,0 +1,120 @@ +/* typehints:start */ +import { GameRoot } from "../root"; +/* typehints:end */ + +import { THIRDPARTY_URLS, globalConfig } from "../../core/config"; +import { DialogWithForm } from "../../core/modal_dialog_elements"; +import { FormElementInput, FormElementItemChooser } from "../../core/modal_dialog_forms"; +import { fillInLinkIntoTranslation } from "../../core/utils"; +import { T } from "../../translations"; +import { GoalAcceptorComponent } from "../components/goal_acceptor"; +import { GameSystemWithFilter } from "../game_system_with_filter"; +// import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item"; +// import { COLOR_ITEM_SINGLETONS } from "../items/color_item"; + +export class GoalAcceptorSystem extends GameSystemWithFilter { + /** @param {GameRoot} root */ + constructor(root) { + super(root, [GoalAcceptorComponent]); + + this.root.signals.entityManuallyPlaced.add(this.editGoal, this); + } + + update() { + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + const goalComp = entity.components.GoalAcceptor; + const readerComp = entity.components.BeltReader; + + // Check against goals (set on placement) + } + + // Check if goal criteria has been met for all goals + } + + drawChunk(parameters, chunk) { + /* + *const contents = chunk.containedEntitiesByLayer.regular; + *for (let i = 0; i < contents.length; ++i) {} + */ + } + + editGoal(entity) { + if (!entity.components.GoalAcceptor) { + return; + } + + const uid = entity.uid; + const goalComp = entity.components.GoalAcceptor; + + const itemInput = new FormElementInput({ + id: "goalItemInput", + label: fillInLinkIntoTranslation(T.dialogs.editSignal.descShortKey, THIRDPARTY_URLS.shapeViewer), + placeholder: "CuCuCuCu", + defaultValue: "CuCuCuCu", + validator: val => this.parseItem(val), + }); + + const rateInput = new FormElementInput({ + id: "goalRateInput", + label: "Rate:", + placeholder: "0", + defaultValue: "0", + validator: val => !isNaN(Number(val)), + }); + + const dialog = new DialogWithForm({ + app: this.root.app, + title: "Set Goal", + desc: "", + formElements: [itemInput, rateInput], + buttons: ["cancel:bad:escape", "ok:good:enter"], + closeButton: false, + }); + this.root.hud.parts.dialogs.internalShowDialog(dialog); + + const closeHandler = () => { + if (this.isEntityStale(uid)) { + return; + } + + goalComp.item = this.parseItem(itemInput.getValue()); + goalComp.rate = this.parseRate(rateInput.getValue()); + }; + + dialog.buttonSignals.ok.add(closeHandler); + dialog.buttonSignals.cancel.add(() => { + if (this.isEntityStale(uid)) { + return; + } + + this.root.logic.tryDeleteBuilding(entity); + }); + } + + parseRate(value) { + return Number(value); + } + + parseItem(value) { + return this.root.systemMgr.systems.constantSignal.parseSignalCode(value); + } + + isEntityStale(uid) { + if (!this.root || !this.root.entityMgr) { + return true; + } + + const entity = this.root.entityMgr.findByUid(uid, false); + if (!entity) { + return true; + } + + const goalComp = entity.components.GoalAcceptor; + if (!goalComp) { + return true; + } + + return false; + } +} diff --git a/src/js/game/systems/item_processor.js b/src/js/game/systems/item_processor.js index 9775afde..17d64e4d 100644 --- a/src/js/game/systems/item_processor.js +++ b/src/js/game/systems/item_processor.js @@ -59,6 +59,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter { [enumItemProcessorTypes.painterQuad]: this.process_PAINTER_QUAD, [enumItemProcessorTypes.hub]: this.process_HUB, [enumItemProcessorTypes.reader]: this.process_READER, + [enumItemProcessorTypes.goal]: this.process_GOAL, }; // Bind all handlers @@ -562,4 +563,13 @@ export class ItemProcessorSystem extends GameSystemWithFilter { this.root.hubGoals.handleDefinitionDelivered(item.definition); } } + + /** + * @param {ProcessorImplementationPayload} payload + */ + process_GOAL(payload) { + const readerComp = payload.entity.components.BeltReader; + readerComp.lastItemTimes.push(this.root.time.now()); + readerComp.lastItem = payload.items[payload.items.length - 1].item; + } } diff --git a/src/js/game/systems/item_producer.js b/src/js/game/systems/item_producer.js index 52edf5d1..be78e4e8 100644 --- a/src/js/game/systems/item_producer.js +++ b/src/js/game/systems/item_producer.js @@ -1,14 +1,27 @@ +/* typehints:start */ +import { GameRoot } from "../root"; +/* typehints:end */ + import { ItemProducerComponent } from "../components/item_producer"; import { GameSystemWithFilter } from "../game_system_with_filter"; export class ItemProducerSystem extends GameSystemWithFilter { + /** @param {GameRoot} root */ constructor(root) { super(root, [ItemProducerComponent]); + this.item = null; } update() { for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; + const producerComp = entity.components.ItemProducer; + const ejectorComp = entity.components.ItemEjector; + + if (producerComp.isWireless()) { + continue; + } + const pinsComp = entity.components.WiredPins; const pin = pinsComp.slots[0]; const network = pin.linkedNetwork; @@ -17,8 +30,8 @@ export class ItemProducerSystem extends GameSystemWithFilter { continue; } - const ejectorComp = entity.components.ItemEjector; - ejectorComp.tryEject(0, network.currentValue); + this.item = network.currentValue; + ejectorComp.tryEject(0, this.item); } } } diff --git a/src/js/game/systems/zone.js b/src/js/game/systems/zone.js new file mode 100644 index 00000000..3dd68804 --- /dev/null +++ b/src/js/game/systems/zone.js @@ -0,0 +1,63 @@ +/* typehints:start */ +import { DrawParameters } from "../../core/draw_parameters"; +import { MapChunkView } from "../map_chunk_view"; +import { GameRoot } from "../root"; +/* typehints:end */ + +import { globalConfig } from "../../core/config"; +import { STOP_PROPAGATION } from "../../core/signal"; +import { GameSystem } from "../game_system"; +import { THEME } from "../theme"; + +export class ZoneSystem extends GameSystem { + /** @param {GameRoot} root */ + constructor(root) { + super(root); + + this.root.signals.prePlacementCheck.add(this.prePlacementCheck, this); + } + + prePlacementCheck(entity, tile = null) { + const staticComp = entity.components.StaticMapEntity; + + if (!staticComp) { + return; + } + + const mode = this.root.gameMode; + const zone = mode.getZone().expandedInAllDirections(-1); + const transformed = staticComp.getTileSpaceBounds(); + + if (zone.containsRect(transformed)) { + if (mode.isZoneRestricted()) { + return STOP_PROPAGATION; + } + } else { + if (mode.isBoundaryRestricted()) { + return STOP_PROPAGATION; + } + } + } + + /** + * Draws the zone + * @param {DrawParameters} parameters + * @param {MapChunkView} chunk + */ + drawChunk(parameters, chunk) { + const mode = this.root.gameMode; + const zone = mode.getZone().allScaled(globalConfig.tileSize); + const context = parameters.context; + + context.globalAlpha = 0.1; + context.fillStyle = THEME.map.zone.background; + context.fillRect(zone.x, zone.y, zone.w, zone.h); + + context.globalAlpha = 1; + context.strokeStyle = THEME.map.zone.border; + context.lineWidth = 2; + context.strokeRect(zone.x, zone.y, zone.w, zone.h); + + context.globalAlpha = 1; + } +} diff --git a/src/js/game/themes/dark.json b/src/js/game/themes/dark.json index 733b7682..02ff6ae3 100644 --- a/src/js/game/themes/dark.json +++ b/src/js/game/themes/dark.json @@ -47,6 +47,11 @@ "textColor": "#fff", "textColorCapped": "#ef5072", "background": "rgba(40, 50, 60, 0.8)" + }, + + "zone": { + "background": "#3e3f47", + "border": "#667964" } }, diff --git a/src/js/game/themes/light.json b/src/js/game/themes/light.json index 0c793c26..4aa367fd 100644 --- a/src/js/game/themes/light.json +++ b/src/js/game/themes/light.json @@ -48,6 +48,11 @@ "textColor": "#fff", "textColorCapped": "#ef5072", "background": "rgba(40, 50, 60, 0.8)" + }, + + "zone": { + "background": "#fff", + "border": "#cbffc4" } }, diff --git a/src/js/main.js b/src/js/main.js index 5b9df699..94f3d37a 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -9,6 +9,7 @@ import { initComponentRegistry } from "./game/component_registry"; import { initDrawUtils } from "./core/draw_utils"; import { initItemRegistry } from "./game/item_registry"; import { initMetaBuildingRegistry } from "./game/meta_building_registry"; +import { initGameModeRegistry } from "./game/game_mode_registry"; import { initGameSpeedRegistry } from "./game/game_speed_registry"; const logger = createLogger("main"); @@ -81,6 +82,7 @@ initDrawUtils(); initComponentRegistry(); initItemRegistry(); initMetaBuildingRegistry(); +initGameModeRegistry(); initGameSpeedRegistry(); let app = null; diff --git a/src/js/savegame/savegame.js b/src/js/savegame/savegame.js index e56ae1dc..999b90ec 100644 --- a/src/js/savegame/savegame.js +++ b/src/js/savegame/savegame.js @@ -13,6 +13,7 @@ import { SavegameInterface_V1005 } from "./schemas/1005"; import { SavegameInterface_V1006 } from "./schemas/1006"; import { SavegameInterface_V1007 } from "./schemas/1007"; import { SavegameInterface_V1008 } from "./schemas/1008"; +import { SavegameInterface_V1009 } from "./schemas/1009"; const logger = createLogger("savegame"); @@ -53,7 +54,7 @@ export class Savegame extends ReadWriteProxy { * @returns {number} */ static getCurrentVersion() { - return 1008; + return 1009; } /** @@ -136,6 +137,11 @@ export class Savegame extends ReadWriteProxy { data.version = 1008; } + if (data.version === 1008) { + SavegameInterface_V1009.migrate1008to1009(data); + data.version = 1009; + } + return ExplainedResult.good(); } diff --git a/src/js/savegame/savegame_interface_registry.js b/src/js/savegame/savegame_interface_registry.js index 395040b3..b4dc4233 100644 --- a/src/js/savegame/savegame_interface_registry.js +++ b/src/js/savegame/savegame_interface_registry.js @@ -9,6 +9,7 @@ import { SavegameInterface_V1005 } from "./schemas/1005"; import { SavegameInterface_V1006 } from "./schemas/1006"; import { SavegameInterface_V1007 } from "./schemas/1007"; import { SavegameInterface_V1008 } from "./schemas/1008"; +import { SavegameInterface_V1009 } from "./schemas/1009"; /** @type {Object.} */ export const savegameInterfaces = { @@ -21,6 +22,7 @@ export const savegameInterfaces = { 1006: SavegameInterface_V1006, 1007: SavegameInterface_V1007, 1008: SavegameInterface_V1008, + 1009: SavegameInterface_V1009, }; const logger = createLogger("savegame_interface_registry"); diff --git a/src/js/savegame/savegame_serializer.js b/src/js/savegame/savegame_serializer.js index c1247225..fb36393e 100644 --- a/src/js/savegame/savegame_serializer.js +++ b/src/js/savegame/savegame_serializer.js @@ -2,6 +2,8 @@ import { ExplainedResult } from "../core/explained_result"; import { createLogger } from "../core/logging"; import { gComponentRegistry } from "../core/global_registries"; import { SerializerInternal } from "./serializer_internal"; +import { HUDPinnedShapes } from "../game/hud/parts/pinned_shapes"; +import { HUDWaypoints } from "../game/hud/parts/waypoints"; /** * @typedef {import("../game/component").Component} Component @@ -33,12 +35,17 @@ export class SavegameSerializer { camera: root.camera.serialize(), time: root.time.serialize(), map: root.map.serialize(), + gameMode: root.gameMode.serialize(), entityMgr: root.entityMgr.serialize(), hubGoals: root.hubGoals.serialize(), - pinnedShapes: root.hud.parts.pinnedShapes.serialize(), - waypoints: root.hud.parts.waypoints.serialize(), entities: this.internal.serializeEntityArray(root.entityMgr.entities), beltPaths: root.systemMgr.systems.belt.serializePaths(), + pinnedShapes: root.gameMode.isHudPartExcluded(HUDPinnedShapes.name) + ? null + : root.hud.parts.pinnedShapes.serialize(), + waypoints: root.gameMode.isHudPartExcluded(HUDWaypoints.name) + ? null + : root.hud.parts.waypoints.serialize(), }; if (G_IS_DEV) { @@ -130,12 +137,19 @@ export class SavegameSerializer { errorReason = errorReason || root.time.deserialize(savegame.time); errorReason = errorReason || root.camera.deserialize(savegame.camera); errorReason = errorReason || root.map.deserialize(savegame.map); + errorReason = errorReason || root.gameMode.deserialize(savegame.gameMode); errorReason = errorReason || root.hubGoals.deserialize(savegame.hubGoals, root); - errorReason = errorReason || root.hud.parts.pinnedShapes.deserialize(savegame.pinnedShapes); - errorReason = errorReason || root.hud.parts.waypoints.deserialize(savegame.waypoints); errorReason = errorReason || this.internal.deserializeEntityArray(root, savegame.entities); errorReason = errorReason || root.systemMgr.systems.belt.deserializePaths(savegame.beltPaths); + if (!root.gameMode.isHudPartExcluded(HUDPinnedShapes.name)) { + errorReason = errorReason || root.hud.parts.pinnedShapes.deserialize(savegame.pinnedShapes); + } + + if (!root.gameMode.isHudPartExcluded(HUDWaypoints.name)) { + errorReason = errorReason || root.hud.parts.waypoints.deserialize(savegame.waypoints); + } + // Check for errors if (errorReason) { return ExplainedResult.bad(errorReason); diff --git a/src/js/savegame/savegame_typedefs.js b/src/js/savegame/savegame_typedefs.js index fb872113..49929f99 100644 --- a/src/js/savegame/savegame_typedefs.js +++ b/src/js/savegame/savegame_typedefs.js @@ -12,6 +12,7 @@ * time: any, * entityMgr: any, * map: any, + * gameMode: object, * hubGoals: any, * pinnedShapes: any, * waypoints: any, diff --git a/src/js/savegame/schemas/1009.js b/src/js/savegame/schemas/1009.js new file mode 100644 index 00000000..e6e1abc6 --- /dev/null +++ b/src/js/savegame/schemas/1009.js @@ -0,0 +1,34 @@ +import { createLogger } from "../../core/logging.js"; +import { RegularGameMode } from "../../game/modes/regular.js"; +import { SavegameInterface_V1008 } from "./1008.js"; + +const schema = require("./1009.json"); +const logger = createLogger("savegame_interface/1009"); + +export class SavegameInterface_V1009 extends SavegameInterface_V1008 { + getVersion() { + return 1009; + } + + getSchemaUncached() { + return schema; + } + + /** + * @param {import("../savegame_typedefs.js").SavegameData} data + */ + static migrate1008to1009(data) { + logger.log("Migrating 1008 to 1009"); + const dump = data.dump; + if (!dump) { + return true; + } + + dump.gameMode = { + mode: { + id: RegularGameMode.getId(), + data: {}, + }, + }; + } +} diff --git a/src/js/savegame/schemas/1009.json b/src/js/savegame/schemas/1009.json new file mode 100644 index 00000000..6682f615 --- /dev/null +++ b/src/js/savegame/schemas/1009.json @@ -0,0 +1,5 @@ +{ + "type": "object", + "required": [], + "additionalProperties": true +} diff --git a/src/js/states/ingame.js b/src/js/states/ingame.js index 316c536c..34f360e3 100644 --- a/src/js/states/ingame.js +++ b/src/js/states/ingame.js @@ -39,6 +39,9 @@ export class GameCreationPayload { /** @type {boolean|undefined} */ this.fastEnter; + /** @type {string} */ + this.gameModeId; + /** @type {Savegame} */ this.savegame; } @@ -220,7 +223,7 @@ export class InGameState extends GameState { logger.log("Creating new game core"); this.core = new GameCore(this.app); - this.core.initializeRoot(this, this.savegame); + this.core.initializeRoot(this, this.savegame, this.gameModeId); if (this.savegame.hasGameDump()) { this.stage4bResumeGame(); @@ -354,6 +357,7 @@ export class InGameState extends GameState { this.creationPayload = payload; this.savegame = payload.savegame; + this.gameModeId = payload.gameModeId; this.loadingOverlay = new GameLoadingOverlay(this.app, this.getDivElement()); this.loadingOverlay.showBasic(); diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index 3afad9bf..b82252ff 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -15,6 +15,7 @@ import { startFileChoose, waitNextFrame, } from "../core/utils"; +import { enumGameModeIds } from "../game/game_mode"; import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; import { getApplicationSettingById } from "../profile/application_settings"; import { T } from "../translations"; @@ -82,6 +83,9 @@ export class MainMenuState extends GameState { }
+
+
+