1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-06-13 13:04:03 +00:00

Puzzle mode (#1135)

* Add mode button to main menu

* [WIP] Add mode menu. Add factory-based gameMode creation

* Add savefile migration, serialize, deserialize

* Add hidden HUD elements, zone, and zoom, boundary constraints

* Clean up lint issues

* Add building, HUD exclusion, building exclusion, and refactor

- [WIP] Add ConstantProducer building that combines ConstantSignal
and ItemProducer functionality. Currently using temp assets.
- Add pre-placement check to the zone
- Use Rectangles for zone and boundary
- Simplify zone drawing
- Account for exclusion in savegame data
- [WIP] Add puzzle play and edit buttons in puzzle mode menu

* [WIP] Add building, component, and systems for producing and
accepting user-specified items and checking goal criteria

* Add ingame puzzle mode UI elements

- Add minimal menus in puzzle mode for back, next navigation
- Add lower menu for changing zone dimenensions

Co-authored-by: Greg Considine <gconsidine@users.noreply.github.com>
This commit is contained in:
tobspr 2021-04-12 14:03:47 +02:00 committed by GitHub
parent 5f0a95ba11
commit fa25deb761
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 1679 additions and 159 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}
}

View File

@ -55,6 +55,10 @@
@import "ingame_hud/sandbox_controller"; @import "ingame_hud/sandbox_controller";
@import "ingame_hud/standalone_advantages"; @import "ingame_hud/standalone_advantages";
@import "ingame_hud/cat_memes"; @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 // prettier-ignore
$elements: $elements:
@ -71,6 +75,10 @@ ingame_HUD_PlacerVariants,
ingame_HUD_PinnedShapes, ingame_HUD_PinnedShapes,
ingame_HUD_GameMenu, ingame_HUD_GameMenu,
ingame_HUD_KeybindingOverlay, ingame_HUD_KeybindingOverlay,
ingame_HUD_ModeMenuBack,
ingame_HUD_ModeMenuNext,
ingame_HUD_ModeMenu,
ingame_HUD_ModeSettings,
ingame_HUD_Notifications, ingame_HUD_Notifications,
ingame_HUD_DebugInfo, ingame_HUD_DebugInfo,
ingame_HUD_EntityDebugger, ingame_HUD_EntityDebugger,
@ -113,6 +121,8 @@ body.uiHidden {
#ingame_HUD_PlacementHints, #ingame_HUD_PlacementHints,
#ingame_HUD_GameMenu, #ingame_HUD_GameMenu,
#ingame_HUD_PinnedShapes, #ingame_HUD_PinnedShapes,
#ingame_HUD_ModeMenuBack,
#ingame_HUD_ModeMenuNext,
#ingame_HUD_Notifications, #ingame_HUD_Notifications,
#ingame_HUD_TutorialHints, #ingame_HUD_TutorialHints,
#ingame_HUD_Waypoints, #ingame_HUD_Waypoints,

View File

@ -1,6 +1,6 @@
$buildings: belt, cutter, miner, mixer, painter, rotater, balancer, stacker, trash, underground_belt, wire, $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, 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 { @each $building in $buildings {
[data-icon="building_icons/#{$building}.png"] { [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, 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, 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, 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 { @each $building in $buildingsAndVariants {
[data-icon="building_tutorials/#{$building}.png"] { [data-icon="building_tutorials/#{$building}.png"] {
/* @load-async */ /* @load-async */

View File

@ -242,6 +242,16 @@
align-items: center; 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 { .browserWarning {
@include S(margin-bottom, 10px); @include S(margin-bottom, 10px);
background-color: $colorRedBright; background-color: $colorRedBright;
@ -285,6 +295,18 @@
@include S(margin-left, 15px); @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 { .savegames {
@include S(max-height, 105px); @include S(max-height, 105px);
overflow-y: auto; 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 { .footer {
display: grid; display: grid;
flex-grow: 1; flex-grow: 1;

View File

@ -62,6 +62,9 @@ export default {
// Allows unlocked achievements to be logged to console in the local build // Allows unlocked achievements to be logged to console in the local build
// testAchievements: true, // 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 // Disables the automatic switch to an overview when zooming out
// disableMapOverview: true, // disableMapOverview: true,
// ----------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------

View File

@ -5,6 +5,7 @@ import { Factory } from "./factory";
* @typedef {import("../game/time/base_game_speed").BaseGameSpeed} BaseGameSpeed * @typedef {import("../game/time/base_game_speed").BaseGameSpeed} BaseGameSpeed
* @typedef {import("../game/component").Component} Component * @typedef {import("../game/component").Component} Component
* @typedef {import("../game/base_item").BaseItem} BaseItem * @typedef {import("../game/base_item").BaseItem} BaseItem
* @typedef {import("../game/game_mode").GameMode} GameMode
* @typedef {import("../game/meta_building").MetaBuilding} MetaBuilding * @typedef {import("../game/meta_building").MetaBuilding} MetaBuilding
@ -19,6 +20,9 @@ export let gBuildingsByCategory = null;
/** @type {FactoryTemplate<Component>} */ /** @type {FactoryTemplate<Component>} */
export let gComponentRegistry = new Factory("component"); export let gComponentRegistry = new Factory("component");
/** @type {FactoryTemplate<GameMode>} */
export let gGameModeRegistry = new Factory("gameMode");
/** @type {FactoryTemplate<BaseGameSpeed>} */ /** @type {FactoryTemplate<BaseGameSpeed>} */
export let gGameSpeedRegistry = new Factory("gamespeed"); export let gGameSpeedRegistry = new Factory("gamespeed");

View File

@ -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,
})
);
}
}

View File

@ -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({}));
}
}

View File

@ -39,6 +39,6 @@ export class MetaItemProducerBuilding extends MetaBuilding {
], ],
}) })
); );
entity.addComponent(new ItemProducerComponent()); entity.addComponent(new ItemProducerComponent({}));
} }
} }

View File

@ -110,6 +110,6 @@ export class MetaReaderBuilding extends MetaBuilding {
}) })
); );
entity.addComponent(new BeltReaderComponent()); entity.addComponent(new BeltReaderComponent({}));
} }
} }

View File

@ -392,13 +392,20 @@ export class Camera extends BasicSerializableObject {
return rect.containsPoint(point.x, point.y); 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 if we can further zoom in
* @returns {boolean} * @returns {boolean}
*/ */
canZoomIn() { canZoomIn() {
const maxLevel = this.root.app.platformWrapper.getMaximumZoom(); return this.zoomLevel <= this.getMaximumZoom() - 0.01;
return this.zoomLevel <= maxLevel - 0.01;
} }
/** /**
@ -406,8 +413,7 @@ export class Camera extends BasicSerializableObject {
* @returns {boolean} * @returns {boolean}
*/ */
canZoomOut() { canZoomOut() {
const minLevel = this.root.app.platformWrapper.getMinimumZoom(); return this.zoomLevel >= this.getMinimumZoom() + 0.01;
return this.zoomLevel >= minLevel + 0.01;
} }
// EVENTS // EVENTS
@ -743,17 +749,30 @@ export class Camera extends BasicSerializableObject {
if (G_IS_DEV && globalConfig.debug.disableZoomLimits) { if (G_IS_DEV && globalConfig.debug.disableZoomLimits) {
return; return;
} }
const wrapper = this.root.app.platformWrapper;
assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *before* clamp: " + this.zoomLevel); 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); assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *after* clamp: " + this.zoomLevel);
if (this.desiredZoom) { 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 * Updates the camera
* @param {number} dt Delta time in milliseconds * @param {number} dt Delta time in milliseconds
@ -857,6 +876,7 @@ export class Camera extends BasicSerializableObject {
// Panning // Panning
this.currentPan = mixVector(this.currentPan, this.desiredPan, 0.06); this.currentPan = mixVector(this.currentPan, this.desiredPan, 0.06);
this.center = this.center.add(this.currentPan.multiplyScalar((0.5 * dt) / this.zoomLevel)); 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() ((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.x += moveAmount * forceX * movementSpeed;
this.center.y += moveAmount * forceY * movementSpeed; this.center.y += moveAmount * forceY * movementSpeed;
this.clampToBounds(this.center);
} }
} }
} }

View File

@ -19,6 +19,7 @@ import { DisplayComponent } from "./components/display";
import { BeltReaderComponent } from "./components/belt_reader"; import { BeltReaderComponent } from "./components/belt_reader";
import { FilterComponent } from "./components/filter"; import { FilterComponent } from "./components/filter";
import { ItemProducerComponent } from "./components/item_producer"; import { ItemProducerComponent } from "./components/item_producer";
import { GoalAcceptorComponent } from "./components/goal_acceptor";
export function initComponentRegistry() { export function initComponentRegistry() {
gComponentRegistry.register(StaticMapEntityComponent); gComponentRegistry.register(StaticMapEntityComponent);
@ -41,6 +42,7 @@ export function initComponentRegistry() {
gComponentRegistry.register(BeltReaderComponent); gComponentRegistry.register(BeltReaderComponent);
gComponentRegistry.register(FilterComponent); gComponentRegistry.register(FilterComponent);
gComponentRegistry.register(ItemProducerComponent); gComponentRegistry.register(ItemProducerComponent);
gComponentRegistry.register(GoalAcceptorComponent);
// IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS // IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS

View File

@ -3,6 +3,12 @@ import { BaseItem } from "../base_item";
import { typeItemSingleton } from "../item_resolver"; import { typeItemSingleton } from "../item_resolver";
import { types } from "../../savegame/serialization"; import { types } from "../../savegame/serialization";
/** @enum {string} */
export const enumBeltReaderType = {
wired: "wired",
wireless: "wireless",
};
export class BeltReaderComponent extends Component { export class BeltReaderComponent extends Component {
static getId() { static getId() {
return "BeltReader"; return "BeltReader";
@ -10,13 +16,20 @@ export class BeltReaderComponent extends Component {
static getSchema() { static getSchema() {
return { return {
type: types.string,
lastItem: types.nullable(typeItemSingleton), lastItem: types.nullable(typeItemSingleton),
}; };
} }
constructor() { /**
* @param {object} param0
* @param {string=} param0.type
*/
constructor({ type = enumBeltReaderType.wired }) {
super(); super();
this.type = type;
/** /**
* Which items went through the reader, we only store the time * Which items went through the reader, we only store the time
* @type {Array<number>} * @type {Array<number>}
@ -41,4 +54,8 @@ export class BeltReaderComponent extends Component {
*/ */
this.lastThroughputComputation = 0; this.lastThroughputComputation = 0;
} }
isWireless() {
return this.type === enumBeltReaderType.wireless;
}
} }

View File

@ -4,6 +4,12 @@ import { Component } from "../component";
import { BaseItem } from "../base_item"; import { BaseItem } from "../base_item";
import { typeItemSingleton } from "../item_resolver"; import { typeItemSingleton } from "../item_resolver";
/** @enum {string} */
export const enumConstantSignalType = {
wired: "wired",
wireless: "wireless",
};
export class ConstantSignalComponent extends Component { export class ConstantSignalComponent extends Component {
static getId() { static getId() {
return "ConstantSignal"; return "ConstantSignal";
@ -11,6 +17,7 @@ export class ConstantSignalComponent extends Component {
static getSchema() { static getSchema() {
return { return {
type: types.string,
signal: types.nullable(typeItemSingleton), signal: types.nullable(typeItemSingleton),
}; };
} }
@ -21,15 +28,22 @@ export class ConstantSignalComponent extends Component {
*/ */
copyAdditionalStateTo(otherComponent) { copyAdditionalStateTo(otherComponent) {
otherComponent.signal = this.signal; otherComponent.signal = this.signal;
otherComponent.type = this.type;
} }
/** /**
* *
* @param {object} param0 * @param {object} param0
* @param {string=} param0.type
* @param {BaseItem=} param0.signal The signal to store * @param {BaseItem=} param0.signal The signal to store
*/ */
constructor({ signal = null }) { constructor({ signal = null, type = enumConstantSignalType.wired }) {
super(); super();
this.signal = signal; this.signal = signal;
this.type = type;
}
isWireless() {
return this.type === enumConstantSignalType.wireless;
} }
} }

View File

@ -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;
}
}

View File

@ -19,6 +19,7 @@ export const enumItemProcessorTypes = {
hub: "hub", hub: "hub",
filter: "filter", filter: "filter",
reader: "reader", reader: "reader",
goal: "goal",
}; };
/** @enum {string} */ /** @enum {string} */
@ -104,7 +105,11 @@ export class ItemProcessorComponent extends Component {
* @param {number} sourceSlot * @param {number} sourceSlot
*/ */
tryTakeItem(item, 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. // Hub has special logic .. not really nice but efficient.
this.inputSlots.push({ item, sourceSlot }); this.inputSlots.push({ item, sourceSlot });
return true; return true;

View File

@ -1,7 +1,33 @@
import { types } from "../../savegame/serialization";
import { Component } from "../component"; import { Component } from "../component";
/** @enum {string} */
export const enumItemProducerType = {
wired: "wired",
wireless: "wireless",
};
export class ItemProducerComponent extends Component { export class ItemProducerComponent extends Component {
static getId() { static getId() {
return "ItemProducer"; 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;
}
} }

View File

@ -31,7 +31,7 @@ import { KeyActionMapper } from "./key_action_mapper";
import { GameLogic } from "./logic"; import { GameLogic } from "./logic";
import { MapView } from "./map_view"; import { MapView } from "./map_view";
import { defaultBuildingVariant } from "./meta_building"; import { defaultBuildingVariant } from "./meta_building";
import { RegularGameMode } from "./modes/regular"; import { GameMode } from "./game_mode";
import { ProductionAnalytics } from "./production_analytics"; import { ProductionAnalytics } from "./production_analytics";
import { GameRoot } from "./root"; import { GameRoot } from "./root";
import { ShapeDefinitionManager } from "./shape_definition_manager"; import { ShapeDefinitionManager } from "./shape_definition_manager";
@ -82,7 +82,7 @@ export class GameCore {
* @param {import("../states/ingame").InGameState} parentState * @param {import("../states/ingame").InGameState} parentState
* @param {Savegame} savegame * @param {Savegame} savegame
*/ */
initializeRoot(parentState, savegame) { initializeRoot(parentState, savegame, gameModeId) {
// Construct the root element, this is the data representation of the game // Construct the root element, this is the data representation of the game
this.root = new GameRoot(this.app); this.root = new GameRoot(this.app);
this.root.gameState = parentState; this.root.gameState = parentState;
@ -104,7 +104,7 @@ export class GameCore {
root.dynamicTickrate = new DynamicTickrate(root); root.dynamicTickrate = new DynamicTickrate(root);
// Init game mode // Init game mode
root.gameMode = new RegularGameMode(root); root.gameMode = GameMode.create(root, gameModeId);
// Init classes // Init classes
root.camera = new Camera(root); root.camera = new Camera(root);
@ -168,6 +168,10 @@ export class GameCore {
this.root.gameIsFresh = true; this.root.gameIsFresh = true;
this.root.map.seed = randomInt(0, 100000); this.root.map.seed = randomInt(0, 100000);
if (!this.root.gameMode.hasHub()) {
return;
}
// Place the hub // Place the hub
const hub = gMetaBuildingRegistry.findByClass(MetaHubBuilding).createEntity({ const hub = gMetaBuildingRegistry.findByClass(MetaHubBuilding).createEntity({
root: this.root, root: this.root,

View File

@ -19,6 +19,7 @@ import { DisplayComponent } from "./components/display";
import { BeltReaderComponent } from "./components/belt_reader"; import { BeltReaderComponent } from "./components/belt_reader";
import { FilterComponent } from "./components/filter"; import { FilterComponent } from "./components/filter";
import { ItemProducerComponent } from "./components/item_producer"; import { ItemProducerComponent } from "./components/item_producer";
import { GoalAcceptorComponent } from "./components/goal_acceptor";
/* typehints:end */ /* typehints:end */
/** /**
@ -89,6 +90,9 @@ export class EntityComponentStorage {
/** @type {ItemProducerComponent} */ /** @type {ItemProducerComponent} */
this.ItemProducer; this.ItemProducer;
/** @type {GoalAcceptorComponent} */
this.GoalAcceptor;
/* typehints:end */ /* typehints:end */
} }
} }

View File

@ -1,71 +1,184 @@
/* typehints:start */ /* typehints:start */
import { enumHubGoalRewards } from "./tutorial_goals"; import { GameRoot } from "./root";
import { Rectangle } from "../core/rectangle";
/* typehints:end */ /* typehints:end */
import { GameRoot } from "./root"; import { gGameModeRegistry } from "../core/global_registries";
import { types, BasicSerializableObject } from "../savegame/serialization";
/** @typedef {{ /** @enum {string} */
* shape: string, export const enumGameModeIds = {
* amount: number puzzleEdit: "puzzleEditMode",
* }} UpgradeRequirement */ puzzlePlay: "puzzlePlayMode",
regular: "regularMode",
};
/** @typedef {{ /** @enum {string} */
* required: Array<UpgradeRequirement> export const enumGameModeTypes = {
* improvement?: number, default: "defaultModeType",
* excludePrevious?: boolean puzzle: "puzzleModeType",
* }} TierRequirement */ };
/** @typedef {Array<TierRequirement>} 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 * @param {GameRoot} root
*/ */
constructor(root) { constructor(root) {
super();
this.root = root; 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 * @param {string} name - Class name of HUD Part
* @returns {Object<string, UpgradeTiers>}
*/
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<LevelDefinition>}
*/
getLevelDefinitions() {
abstract;
return null;
}
/**
* Should return whether free play is available or if the game stops
* after the predefined levels
* @returns {boolean} * @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; 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<string, Array>} */
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";
}
} }

View File

@ -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);
}

View File

@ -24,6 +24,9 @@ import { ItemProcessorOverlaysSystem } from "./systems/item_processor_overlays";
import { BeltReaderSystem } from "./systems/belt_reader"; import { BeltReaderSystem } from "./systems/belt_reader";
import { FilterSystem } from "./systems/filter"; import { FilterSystem } from "./systems/filter";
import { ItemProducerSystem } from "./systems/item_producer"; 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"); const logger = createLogger("game_system_manager");
@ -100,6 +103,15 @@ export class GameSystemManager {
/** @type {ItemProducerSystem} */ /** @type {ItemProducerSystem} */
itemProducer: null, itemProducer: null,
/** @type {ConstantProducerSystem} */
ConstantProducer: null,
/** @type {GoalAcceptorSystem} */
GoalAcceptor: null,
/** @type {ZoneSystem} */
zone: null,
/* typehints:end */ /* typehints:end */
}; };
this.systemUpdateOrder = []; this.systemUpdateOrder = [];
@ -138,7 +150,9 @@ export class GameSystemManager {
add("itemEjector", ItemEjectorSystem); add("itemEjector", ItemEjectorSystem);
if (this.root.gameMode.hasResources()) {
add("mapResources", MapResourcesSystem); add("mapResources", MapResourcesSystem);
}
add("hub", HubSystem); add("hub", HubSystem);
@ -165,6 +179,14 @@ export class GameSystemManager {
add("itemProcessorOverlays", ItemProcessorOverlaysSystem); 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"); logger.log("📦 There are", this.systemUpdateOrder.length, "game systems");
} }

View File

@ -500,6 +500,7 @@ export class HubGoals extends BasicSerializableObject {
switch (processorType) { switch (processorType) {
case enumItemProcessorTypes.trash: case enumItemProcessorTypes.trash:
case enumItemProcessorTypes.hub: case enumItemProcessorTypes.hub:
case enumItemProcessorTypes.goal:
return 1e30; return 1e30;
case enumItemProcessorTypes.balancer: case enumItemProcessorTypes.balancer:
return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt * 2; return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt * 2;

View File

@ -49,6 +49,10 @@ import { HUDStandaloneAdvantages } from "./parts/standalone_advantages";
import { HUDCatMemes } from "./parts/cat_memes"; import { HUDCatMemes } from "./parts/cat_memes";
import { HUDTutorialVideoOffer } from "./parts/tutorial_video_offer"; import { HUDTutorialVideoOffer } from "./parts/tutorial_video_offer";
import { HUDConstantSignalEdit } from "./parts/constant_signal_edit"; 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 { export class GameHUD {
/** /**
@ -74,46 +78,52 @@ export class GameHUD {
unlockNotificationFinished: /** @type {TypedSignal<[]>} */ (new Signal()), unlockNotificationFinished: /** @type {TypedSignal<[]>} */ (new Signal()),
}; };
this.parts = { this.initParts({
buildingsToolbar: new HUDBuildingsToolbar(this.root), buildingsToolbar: HUDBuildingsToolbar,
wiresToolbar: new HUDWiresToolbar(this.root), wiresToolbar: HUDWiresToolbar,
blueprintPlacer: new HUDBlueprintPlacer(this.root), blueprintPlacer: HUDBlueprintPlacer,
buildingPlacer: new HUDBuildingPlacer(this.root), buildingPlacer: HUDBuildingPlacer,
unlockNotification: new HUDUnlockNotification(this.root), unlockNotification: HUDUnlockNotification,
gameMenu: new HUDGameMenu(this.root), gameMenu: HUDGameMenu,
massSelector: new HUDMassSelector(this.root), massSelector: HUDMassSelector,
shop: new HUDShop(this.root), shop: HUDShop,
statistics: new HUDStatistics(this.root), statistics: HUDStatistics,
waypoints: new HUDWaypoints(this.root), waypoints: HUDWaypoints,
wireInfo: new HUDWireInfo(this.root), wireInfo: HUDWireInfo,
leverToggle: new HUDLeverToggle(this.root), leverToggle: HUDLeverToggle,
constantSignalEdit: new HUDConstantSignalEdit(this.root), constantSignalEdit: HUDConstantSignalEdit,
modeMenuBack: HUDModeMenuBack,
modeMenuNext: HUDModeMenuNext,
modeMenu: HUDModeMenu,
modeSettings: HUDModeSettings,
// Must always exist // Must always exist
pinnedShapes: new HUDPinnedShapes(this.root), pinnedShapes: HUDPinnedShapes,
notifications: new HUDNotifications(this.root), notifications: HUDNotifications,
settingsMenu: new HUDSettingsMenu(this.root), settingsMenu: HUDSettingsMenu,
debugInfo: new HUDDebugInfo(this.root), debugInfo: HUDDebugInfo,
dialogs: new HUDModalDialogs(this.root), dialogs: HUDModalDialogs,
screenshotExporter: new HUDScreenshotExporter(this.root), screenshotExporter: HUDScreenshotExporter,
shapeViewer: new HUDShapeViewer(this.root), shapeViewer: HUDShapeViewer,
wiresOverlay: new HUDWiresOverlay(this.root), wiresOverlay: HUDWiresOverlay,
layerPreview: new HUDLayerPreview(this.root), layerPreview: HUDLayerPreview,
minerHighlight: new HUDMinerHighlight(this.root), minerHighlight: HUDMinerHighlight,
tutorialVideoOffer: new HUDTutorialVideoOffer(this.root), tutorialVideoOffer: HUDTutorialVideoOffer,
// Typing hints // Typing hints
/* typehints:start */ /* typehints:start */
/** @type {HUDChangesDebugger} */ /** @type {HUDChangesDebugger} */
changesDebugger: null, changesDebugger: null,
/* typehints:end */ /* typehints:end */
}; });
if (!IS_MOBILE) { if (!IS_MOBILE) {
if (!this.root.gameMode.isHudPartExcluded(HUDKeybindingOverlay.name)) {
this.parts.keybindingOverlay = new HUDKeybindingOverlay(this.root); this.parts.keybindingOverlay = new HUDKeybindingOverlay(this.root);
} }
}
if (G_IS_DEV && globalConfig.debug.enableEntityInspector) { if (G_IS_DEV && globalConfig.debug.enableEntityInspector) {
this.parts.entityDebugger = new HUDEntityDebugger(this.root); this.parts.entityDebugger = new HUDEntityDebugger(this.root);
@ -130,9 +140,14 @@ export class GameHUD {
} }
if (this.root.app.settings.getAllSettings().offerHints) { if (this.root.app.settings.getAllSettings().offerHints) {
if (!this.root.gameMode.isHudPartExcluded(HUDPartTutorialHints.name)) {
this.parts.tutorialHints = new HUDPartTutorialHints(this.root); this.parts.tutorialHints = new HUDPartTutorialHints(this.root);
}
if (!this.root.gameMode.isHudPartExcluded(HUDInteractiveTutorial.name)) {
this.parts.interactiveTutorial = new HUDInteractiveTutorial(this.root); this.parts.interactiveTutorial = new HUDInteractiveTutorial(this.root);
} }
}
if (this.root.app.settings.getAllSettings().vignette) { if (this.root.app.settings.getAllSettings().vignette) {
this.parts.vignetteOverlay = new HUDVignetteOverlay(this.root); this.parts.vignetteOverlay = new HUDVignetteOverlay(this.root);
@ -170,6 +185,21 @@ export class GameHUD {
/* dev:end*/ /* 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 * Attempts to close all overlays
*/ */

View File

@ -23,8 +23,8 @@ export class HUDBaseToolbar extends BaseHUDPart {
) { ) {
super(root); super(root);
this.primaryBuildings = primaryBuildings; this.primaryBuildings = this.filterBuildings(primaryBuildings);
this.secondaryBuildings = secondaryBuildings; this.secondaryBuildings = this.filterBuildings(secondaryBuildings);
this.visibilityCondition = visibilityCondition; this.visibilityCondition = visibilityCondition;
this.htmlElementId = htmlElementId; this.htmlElementId = htmlElementId;
this.layer = layer; this.layer = layer;
@ -47,6 +47,24 @@ export class HUDBaseToolbar extends BaseHUDPart {
this.element = makeDiv(parent, this.htmlElementId, ["ingame_buildingsToolbar"], ""); this.element = makeDiv(parent, this.htmlElementId, ["ingame_buildingsToolbar"], "");
} }
/**
* @param {Array<typeof MetaBuilding>} buildings
* @returns {Array<typeof MetaBuilding>}
*/
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 all buildings
* @returns {Array<typeof MetaBuilding>} * @returns {Array<typeof MetaBuilding>}

View File

@ -15,12 +15,15 @@ import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt";
import { HUDBaseToolbar } from "./base_toolbar"; import { HUDBaseToolbar } from "./base_toolbar";
import { MetaStorageBuilding } from "../../buildings/storage"; import { MetaStorageBuilding } from "../../buildings/storage";
import { MetaItemProducerBuilding } from "../../buildings/item_producer"; 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 { export class HUDBuildingsToolbar extends HUDBaseToolbar {
constructor(root) { constructor(root) {
super(root, { super(root, {
primaryBuildings: [ primaryBuildings: [
MetaConstantProducerBuilding,
MetaGoalAcceptorBuilding,
MetaBeltBuilding, MetaBeltBuilding,
MetaBalancerBuilding, MetaBalancerBuilding,
MetaUndergroundBeltBuilding, MetaUndergroundBeltBuilding,
@ -31,7 +34,7 @@ export class HUDBuildingsToolbar extends HUDBaseToolbar {
MetaMixerBuilding, MetaMixerBuilding,
MetaPainterBuilding, MetaPainterBuilding,
MetaTrashBuilding, MetaTrashBuilding,
...(queryParamOptions.sandboxMode || G_IS_DEV ? [MetaItemProducerBuilding] : []), MetaItemProducerBuilding,
], ],
secondaryBuildings: [ secondaryBuildings: [
MetaStorageBuilding, MetaStorageBuilding,

View File

@ -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() {}
}

View File

@ -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();
}
}

View File

@ -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() {}
}

View File

@ -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"],
`
<label>Zone</label>
<div class="buttons">
<div class="zoneWidth plusMinus">
<label>Width</label>
<button class="styledButton minus">-</button>
<span class="value"></span>
<button class="styledButton plus">+</button>
</div>
<div class="zoneHeight plusMinus">
<label>Height</label>
<button class="styledButton minus">-</button>
<span class="value"></span>
<button class="styledButton plus">+</button>
</div>
</div>`
);
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);
}
}

View File

@ -100,16 +100,14 @@ export class HUDWaypoints extends BaseHUDPart {
this.directionIndicatorSprite = Loader.getSprite("sprites/misc/hub_direction_indicator.png"); this.directionIndicatorSprite = Loader.getSprite("sprites/misc/hub_direction_indicator.png");
/** @type {Array<Waypoint>} /** @type {Array<Waypoint>} */
*/ this.waypoints = [];
this.waypoints = [ this.waypoints.push({
{
label: null, label: null,
center: { x: 0, y: 0 }, center: { x: 0, y: 0 },
zoomLevel: 3, zoomLevel: 3,
layer: gMetaBuildingRegistry.findByClass(MetaHubBuilding).getLayer(), layer: gMetaBuildingRegistry.findByClass(MetaHubBuilding).getLayer(),
}, });
];
// Create a buffer we can use to measure text // Create a buffer we can use to measure text
this.dummyBuffer = makeOffscreenBuffer(1, 1, { this.dummyBuffer = makeOffscreenBuffer(1, 1, {

View File

@ -49,6 +49,10 @@ export const KEYMAPPINGS = {
}, },
buildings: { buildings: {
// Puzzle buildings
constant_producer: { keyCode: key("H") },
goal_acceptor: { keyCode: key("N") },
// Primary Toolbar // Primary Toolbar
belt: { keyCode: key("1") }, belt: { keyCode: key("1") },
balancer: { keyCode: key("2") }, balancer: { keyCode: key("2") },
@ -262,6 +266,8 @@ export function getStringForKeyCode(code) {
return "."; return ".";
case 191: case 191:
return "/"; return "/";
case 192:
return "`";
case 219: case 219:
return "["; return "[";
case 220: case 220:

View File

@ -41,7 +41,14 @@ export class MapChunkView extends MapChunk {
*/ */
drawBackgroundLayer(parameters) { drawBackgroundLayer(parameters) {
const systems = this.root.systemMgr.systems; const systems = this.root.systemMgr.systems;
if (this.root.gameMode.hasZone()) {
systems.zone.drawChunk(parameters, this);
}
if (this.root.gameMode.hasResources()) {
systems.mapResources.drawChunk(parameters, this); systems.mapResources.drawChunk(parameters, this);
}
systems.beltUnderlays.drawChunk(parameters, this); systems.beltUnderlays.drawChunk(parameters, this);
systems.belt.drawChunk(parameters, this); systems.belt.drawChunk(parameters, this);
} }
@ -69,6 +76,7 @@ export class MapChunkView extends MapChunk {
systems.lever.drawChunk(parameters, this); systems.lever.drawChunk(parameters, this);
systems.display.drawChunk(parameters, this); systems.display.drawChunk(parameters, this);
systems.storage.drawChunk(parameters, this); systems.storage.drawChunk(parameters, this);
systems.constantProducer.drawChunk(parameters, this);
systems.itemProcessorOverlays.drawChunk(parameters, this); systems.itemProcessorOverlays.drawChunk(parameters, this);
} }

View File

@ -5,10 +5,12 @@ import { MetaAnalyzerBuilding } from "./buildings/analyzer";
import { enumBalancerVariants, MetaBalancerBuilding } from "./buildings/balancer"; import { enumBalancerVariants, MetaBalancerBuilding } from "./buildings/balancer";
import { MetaBeltBuilding } from "./buildings/belt"; import { MetaBeltBuilding } from "./buildings/belt";
import { MetaComparatorBuilding } from "./buildings/comparator"; import { MetaComparatorBuilding } from "./buildings/comparator";
import { MetaConstantProducerBuilding } from "./buildings/constant_producer";
import { MetaConstantSignalBuilding } from "./buildings/constant_signal"; import { MetaConstantSignalBuilding } from "./buildings/constant_signal";
import { enumCutterVariants, MetaCutterBuilding } from "./buildings/cutter"; import { enumCutterVariants, MetaCutterBuilding } from "./buildings/cutter";
import { MetaDisplayBuilding } from "./buildings/display"; import { MetaDisplayBuilding } from "./buildings/display";
import { MetaFilterBuilding } from "./buildings/filter"; import { MetaFilterBuilding } from "./buildings/filter";
import { MetaGoalAcceptorBuilding } from "./buildings/goal_acceptor";
import { MetaHubBuilding } from "./buildings/hub"; import { MetaHubBuilding } from "./buildings/hub";
import { MetaItemProducerBuilding } from "./buildings/item_producer"; import { MetaItemProducerBuilding } from "./buildings/item_producer";
import { MetaLeverBuilding } from "./buildings/lever"; import { MetaLeverBuilding } from "./buildings/lever";
@ -45,6 +47,7 @@ export function initMetaBuildingRegistry() {
gMetaBuildingRegistry.register(MetaStorageBuilding); gMetaBuildingRegistry.register(MetaStorageBuilding);
gMetaBuildingRegistry.register(MetaBeltBuilding); gMetaBuildingRegistry.register(MetaBeltBuilding);
gMetaBuildingRegistry.register(MetaUndergroundBeltBuilding); gMetaBuildingRegistry.register(MetaUndergroundBeltBuilding);
gMetaBuildingRegistry.register(MetaGoalAcceptorBuilding);
gMetaBuildingRegistry.register(MetaHubBuilding); gMetaBuildingRegistry.register(MetaHubBuilding);
gMetaBuildingRegistry.register(MetaWireBuilding); gMetaBuildingRegistry.register(MetaWireBuilding);
gMetaBuildingRegistry.register(MetaConstantSignalBuilding); gMetaBuildingRegistry.register(MetaConstantSignalBuilding);
@ -59,6 +62,7 @@ export function initMetaBuildingRegistry() {
gMetaBuildingRegistry.register(MetaAnalyzerBuilding); gMetaBuildingRegistry.register(MetaAnalyzerBuilding);
gMetaBuildingRegistry.register(MetaComparatorBuilding); gMetaBuildingRegistry.register(MetaComparatorBuilding);
gMetaBuildingRegistry.register(MetaItemProducerBuilding); gMetaBuildingRegistry.register(MetaItemProducerBuilding);
gMetaBuildingRegistry.register(MetaConstantProducerBuilding);
// Belt // Belt
registerBuildingVariant(1, MetaBeltBuilding, defaultBuildingVariant, 0); registerBuildingVariant(1, MetaBeltBuilding, defaultBuildingVariant, 0);
@ -165,6 +169,12 @@ export function initMetaBuildingRegistry() {
// Item producer // Item producer
registerBuildingVariant(61, MetaItemProducerBuilding); registerBuildingVariant(61, MetaItemProducerBuilding);
// Constant producer
registerBuildingVariant(62, MetaConstantProducerBuilding);
// Goal acceptor
registerBuildingVariant(63, MetaGoalAcceptorBuilding);
// Propagate instances // Propagate instances
for (const key in gBuildingVariants) { for (const key in gBuildingVariants) {
gBuildingVariants[key].metaInstance = gMetaBuildingRegistry.findByClass( gBuildingVariants[key].metaInstance = gMetaBuildingRegistry.findByClass(

129
src/js/game/modes/puzzle.js Normal file
View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -1,19 +1,50 @@
/* typehints:start */
import { GameRoot } from "../root";
/* typehints:end */
import { queryParamOptions } from "../../core/query_parameters";
import { findNiceIntegerValue } from "../../core/utils"; 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 { ShapeDefinition } from "../shape_definition";
import { enumHubGoalRewards } from "../tutorial_goals"; import { enumHubGoalRewards } from "../tutorial_goals";
/** @typedef {{
* shape: string,
* amount: number
* }} UpgradeRequirement */
/** @typedef {{
* required: Array<UpgradeRequirement>
* improvement?: number,
* excludePrevious?: boolean
* }} TierRequirement */
/** @typedef {Array<TierRequirement>} UpgradeTiers */
/** @typedef {{
* shape: string,
* required: number,
* reward: enumHubGoalRewards,
* throughputOnly?: boolean
* }} LevelDefinition */
const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw"; const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw";
const finalGameShape = "RuCw--Cw:----Ru--"; const finalGameShape = "RuCw--Cw:----Ru--";
const preparementShape = "CpRpCp--:SwSwSwSw"; const preparementShape = "CpRpCp--:SwSwSwSw";
const blueprintShape = "CbCbCbRb:CwCwCwCw";
// Tiers need % of the previous tier as requirement too // Tiers need % of the previous tier as requirement too
const tierGrowth = 2.5; const tierGrowth = 2.5;
/** /**
* Generates all upgrades * Generates all upgrades
* @returns {Object<string, import("../game_mode").UpgradeTiers>} */ * @returns {Object<string, UpgradeTiers>} */
function generateUpgrades(limitedVersion = false) { function generateUpgrades(limitedVersion = false) {
const fixedImprovements = [0.5, 0.5, 1, 1, 2, 1, 1]; const fixedImprovements = [0.5, 0.5, 1, 1, 2, 1, 1];
const numEndgameUpgrades = limitedVersion ? 0 : 1000 - fixedImprovements.length - 1; const numEndgameUpgrades = limitedVersion ? 0 : 1000 - fixedImprovements.length - 1;
@ -454,27 +485,58 @@ const fullVersionLevels = generateLevelDefinitions(false);
const demoVersionLevels = generateLevelDefinitions(true); const demoVersionLevels = generateLevelDefinitions(true);
export class RegularGameMode extends GameMode { export class RegularGameMode extends GameMode {
constructor(root) { static getId() {
super(root); 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<string, UpgradeTiers>}
*/
getUpgrades() { getUpgrades() {
return this.root.app.restrictionMgr.getHasExtendedUpgrades() return this.root.app.restrictionMgr.getHasExtendedUpgrades()
? fullVersionUpgrades ? fullVersionUpgrades
: demoVersionUpgrades; : demoVersionUpgrades;
} }
getIsFreeplayAvailable() { /**
return this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay(); * Returns the goals for all levels including their reward
} * @returns {Array<LevelDefinition>}
*/
getBlueprintShapeKey() {
return blueprintShape;
}
getLevelDefinitions() { getLevelDefinitions() {
return this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay() return this.root.app.restrictionMgr.getHasExtendedLevelsAndFreeplay()
? fullVersionLevels ? fullVersionLevels
: demoVersionLevels; : 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();
}
} }

View File

@ -14,7 +14,6 @@ export class BeltReaderSystem extends GameSystemWithFilter {
const minimumTimeForThroughput = now - 1; const minimumTimeForThroughput = now - 1;
for (let i = 0; i < this.allEntities.length; ++i) { for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i]; const entity = this.allEntities[i];
const readerComp = entity.components.BeltReader; const readerComp = entity.components.BeltReader;
const pinsComp = entity.components.WiredPins; const pinsComp = entity.components.WiredPins;
@ -23,12 +22,14 @@ export class BeltReaderSystem extends GameSystemWithFilter {
readerComp.lastItemTimes.shift(); readerComp.lastItemTimes.shift();
} }
if (!entity.components.BeltReader.isWireless()) {
pinsComp.slots[1].value = readerComp.lastItem; pinsComp.slots[1].value = readerComp.lastItem;
pinsComp.slots[0].value = pinsComp.slots[0].value =
(readerComp.lastItemTimes[readerComp.lastItemTimes.length - 1] || 0) > (readerComp.lastItemTimes[readerComp.lastItemTimes.length - 1] || 0) >
minimumTimeForThroughput minimumTimeForThroughput
? BOOL_TRUE_SINGLETON ? BOOL_TRUE_SINGLETON
: BOOL_FALSE_SINGLETON; : BOOL_FALSE_SINGLETON;
}
if (now - readerComp.lastThroughputComputation > 0.5) { if (now - readerComp.lastThroughputComputation > 0.5) {
// Compute throughput // Compute throughput

View File

@ -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);
}
}
}

View File

@ -6,9 +6,10 @@ import { fillInLinkIntoTranslation } from "../../core/utils";
import { T } from "../../translations"; import { T } from "../../translations";
import { BaseItem } from "../base_item"; import { BaseItem } from "../base_item";
import { enumColors } from "../colors"; import { enumColors } from "../colors";
import { ConstantSignalComponent } from "../components/constant_signal"; import { enumConstantSignalType, ConstantSignalComponent } from "../components/constant_signal";
import { Entity } from "../entity"; import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter"; 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 { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item";
import { COLOR_ITEM_SINGLETONS } from "../items/color_item"; import { COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { ShapeDefinition } from "../shape_definition"; import { ShapeDefinition } from "../shape_definition";
@ -26,8 +27,13 @@ export class ConstantSignalSystem extends GameSystemWithFilter {
// Set signals // Set signals
for (let i = 0; i < this.allEntities.length; ++i) { for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i]; const entity = this.allEntities[i];
const pinsComp = entity.components.WiredPins;
const signalComp = entity.components.ConstantSignal; const signalComp = entity.components.ConstantSignal;
if (signalComp.isWireless()) {
continue;
}
const pinsComp = entity.components.WiredPins;
pinsComp.slots[0].value = signalComp.signal; pinsComp.slots[0].value = signalComp.signal;
} }
} }
@ -54,23 +60,33 @@ export class ConstantSignalSystem extends GameSystemWithFilter {
validator: val => this.parseSignalCode(val), validator: val => this.parseSignalCode(val),
}); });
const itemInput = new FormElementItemChooser({ const items = [
id: "signalItem",
label: null,
items: [
BOOL_FALSE_SINGLETON, BOOL_FALSE_SINGLETON,
BOOL_TRUE_SINGLETON, BOOL_TRUE_SINGLETON,
...Object.values(COLOR_ITEM_SINGLETONS), ...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.shapeDefinitionMgr.getShapeItemFromDefinition(
this.root.hubGoals.currentGoal.definition this.root.hubGoals.currentGoal.definition
), )
this.root.shapeDefinitionMgr.getShapeItemFromShortKey( );
this.root.gameMode.getBlueprintShapeKey() }
),
if (!this.root.gameMode.isHudPartExcluded(HUDPinnedShapes.name)) {
items.push(
...this.root.hud.parts.pinnedShapes.pinnedShapes.map(key => ...this.root.hud.parts.pinnedShapes.pinnedShapes.map(key =>
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key) this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key)
), )
], );
}
const itemInput = new FormElementItemChooser({
id: "signalItem",
label: null,
items,
}); });
const dialog = new DialogWithForm({ const dialog = new DialogWithForm({
@ -103,7 +119,6 @@ export class ConstantSignalSystem extends GameSystemWithFilter {
} }
if (itemInput.chosenItem) { if (itemInput.chosenItem) {
console.log(itemInput.chosenItem);
constantComp.signal = itemInput.chosenItem; constantComp.signal = itemInput.chosenItem;
} else { } else {
constantComp.signal = this.parseSignalCode(signalValueInput.getValue()); constantComp.signal = this.parseSignalCode(signalValueInput.getValue());

View File

@ -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;
}
}

View File

@ -59,6 +59,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
[enumItemProcessorTypes.painterQuad]: this.process_PAINTER_QUAD, [enumItemProcessorTypes.painterQuad]: this.process_PAINTER_QUAD,
[enumItemProcessorTypes.hub]: this.process_HUB, [enumItemProcessorTypes.hub]: this.process_HUB,
[enumItemProcessorTypes.reader]: this.process_READER, [enumItemProcessorTypes.reader]: this.process_READER,
[enumItemProcessorTypes.goal]: this.process_GOAL,
}; };
// Bind all handlers // Bind all handlers
@ -562,4 +563,13 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
this.root.hubGoals.handleDefinitionDelivered(item.definition); 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;
}
} }

View File

@ -1,14 +1,27 @@
/* typehints:start */
import { GameRoot } from "../root";
/* typehints:end */
import { ItemProducerComponent } from "../components/item_producer"; import { ItemProducerComponent } from "../components/item_producer";
import { GameSystemWithFilter } from "../game_system_with_filter"; import { GameSystemWithFilter } from "../game_system_with_filter";
export class ItemProducerSystem extends GameSystemWithFilter { export class ItemProducerSystem extends GameSystemWithFilter {
/** @param {GameRoot} root */
constructor(root) { constructor(root) {
super(root, [ItemProducerComponent]); super(root, [ItemProducerComponent]);
this.item = null;
} }
update() { update() {
for (let i = 0; i < this.allEntities.length; ++i) { for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[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 pinsComp = entity.components.WiredPins;
const pin = pinsComp.slots[0]; const pin = pinsComp.slots[0];
const network = pin.linkedNetwork; const network = pin.linkedNetwork;
@ -17,8 +30,8 @@ export class ItemProducerSystem extends GameSystemWithFilter {
continue; continue;
} }
const ejectorComp = entity.components.ItemEjector; this.item = network.currentValue;
ejectorComp.tryEject(0, network.currentValue); ejectorComp.tryEject(0, this.item);
} }
} }
} }

View File

@ -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;
}
}

View File

@ -47,6 +47,11 @@
"textColor": "#fff", "textColor": "#fff",
"textColorCapped": "#ef5072", "textColorCapped": "#ef5072",
"background": "rgba(40, 50, 60, 0.8)" "background": "rgba(40, 50, 60, 0.8)"
},
"zone": {
"background": "#3e3f47",
"border": "#667964"
} }
}, },

View File

@ -48,6 +48,11 @@
"textColor": "#fff", "textColor": "#fff",
"textColorCapped": "#ef5072", "textColorCapped": "#ef5072",
"background": "rgba(40, 50, 60, 0.8)" "background": "rgba(40, 50, 60, 0.8)"
},
"zone": {
"background": "#fff",
"border": "#cbffc4"
} }
}, },

View File

@ -9,6 +9,7 @@ import { initComponentRegistry } from "./game/component_registry";
import { initDrawUtils } from "./core/draw_utils"; import { initDrawUtils } from "./core/draw_utils";
import { initItemRegistry } from "./game/item_registry"; import { initItemRegistry } from "./game/item_registry";
import { initMetaBuildingRegistry } from "./game/meta_building_registry"; import { initMetaBuildingRegistry } from "./game/meta_building_registry";
import { initGameModeRegistry } from "./game/game_mode_registry";
import { initGameSpeedRegistry } from "./game/game_speed_registry"; import { initGameSpeedRegistry } from "./game/game_speed_registry";
const logger = createLogger("main"); const logger = createLogger("main");
@ -81,6 +82,7 @@ initDrawUtils();
initComponentRegistry(); initComponentRegistry();
initItemRegistry(); initItemRegistry();
initMetaBuildingRegistry(); initMetaBuildingRegistry();
initGameModeRegistry();
initGameSpeedRegistry(); initGameSpeedRegistry();
let app = null; let app = null;

View File

@ -13,6 +13,7 @@ import { SavegameInterface_V1005 } from "./schemas/1005";
import { SavegameInterface_V1006 } from "./schemas/1006"; import { SavegameInterface_V1006 } from "./schemas/1006";
import { SavegameInterface_V1007 } from "./schemas/1007"; import { SavegameInterface_V1007 } from "./schemas/1007";
import { SavegameInterface_V1008 } from "./schemas/1008"; import { SavegameInterface_V1008 } from "./schemas/1008";
import { SavegameInterface_V1009 } from "./schemas/1009";
const logger = createLogger("savegame"); const logger = createLogger("savegame");
@ -53,7 +54,7 @@ export class Savegame extends ReadWriteProxy {
* @returns {number} * @returns {number}
*/ */
static getCurrentVersion() { static getCurrentVersion() {
return 1008; return 1009;
} }
/** /**
@ -136,6 +137,11 @@ export class Savegame extends ReadWriteProxy {
data.version = 1008; data.version = 1008;
} }
if (data.version === 1008) {
SavegameInterface_V1009.migrate1008to1009(data);
data.version = 1009;
}
return ExplainedResult.good(); return ExplainedResult.good();
} }

View File

@ -9,6 +9,7 @@ import { SavegameInterface_V1005 } from "./schemas/1005";
import { SavegameInterface_V1006 } from "./schemas/1006"; import { SavegameInterface_V1006 } from "./schemas/1006";
import { SavegameInterface_V1007 } from "./schemas/1007"; import { SavegameInterface_V1007 } from "./schemas/1007";
import { SavegameInterface_V1008 } from "./schemas/1008"; import { SavegameInterface_V1008 } from "./schemas/1008";
import { SavegameInterface_V1009 } from "./schemas/1009";
/** @type {Object.<number, typeof BaseSavegameInterface>} */ /** @type {Object.<number, typeof BaseSavegameInterface>} */
export const savegameInterfaces = { export const savegameInterfaces = {
@ -21,6 +22,7 @@ export const savegameInterfaces = {
1006: SavegameInterface_V1006, 1006: SavegameInterface_V1006,
1007: SavegameInterface_V1007, 1007: SavegameInterface_V1007,
1008: SavegameInterface_V1008, 1008: SavegameInterface_V1008,
1009: SavegameInterface_V1009,
}; };
const logger = createLogger("savegame_interface_registry"); const logger = createLogger("savegame_interface_registry");

View File

@ -2,6 +2,8 @@ import { ExplainedResult } from "../core/explained_result";
import { createLogger } from "../core/logging"; import { createLogger } from "../core/logging";
import { gComponentRegistry } from "../core/global_registries"; import { gComponentRegistry } from "../core/global_registries";
import { SerializerInternal } from "./serializer_internal"; 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 * @typedef {import("../game/component").Component} Component
@ -33,12 +35,17 @@ export class SavegameSerializer {
camera: root.camera.serialize(), camera: root.camera.serialize(),
time: root.time.serialize(), time: root.time.serialize(),
map: root.map.serialize(), map: root.map.serialize(),
gameMode: root.gameMode.serialize(),
entityMgr: root.entityMgr.serialize(), entityMgr: root.entityMgr.serialize(),
hubGoals: root.hubGoals.serialize(), hubGoals: root.hubGoals.serialize(),
pinnedShapes: root.hud.parts.pinnedShapes.serialize(),
waypoints: root.hud.parts.waypoints.serialize(),
entities: this.internal.serializeEntityArray(root.entityMgr.entities), entities: this.internal.serializeEntityArray(root.entityMgr.entities),
beltPaths: root.systemMgr.systems.belt.serializePaths(), 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) { if (G_IS_DEV) {
@ -130,12 +137,19 @@ export class SavegameSerializer {
errorReason = errorReason || root.time.deserialize(savegame.time); errorReason = errorReason || root.time.deserialize(savegame.time);
errorReason = errorReason || root.camera.deserialize(savegame.camera); errorReason = errorReason || root.camera.deserialize(savegame.camera);
errorReason = errorReason || root.map.deserialize(savegame.map); 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.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 || this.internal.deserializeEntityArray(root, savegame.entities);
errorReason = errorReason || root.systemMgr.systems.belt.deserializePaths(savegame.beltPaths); 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 // Check for errors
if (errorReason) { if (errorReason) {
return ExplainedResult.bad(errorReason); return ExplainedResult.bad(errorReason);

View File

@ -12,6 +12,7 @@
* time: any, * time: any,
* entityMgr: any, * entityMgr: any,
* map: any, * map: any,
* gameMode: object,
* hubGoals: any, * hubGoals: any,
* pinnedShapes: any, * pinnedShapes: any,
* waypoints: any, * waypoints: any,

View File

@ -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: {},
},
};
}
}

View File

@ -0,0 +1,5 @@
{
"type": "object",
"required": [],
"additionalProperties": true
}

View File

@ -39,6 +39,9 @@ export class GameCreationPayload {
/** @type {boolean|undefined} */ /** @type {boolean|undefined} */
this.fastEnter; this.fastEnter;
/** @type {string} */
this.gameModeId;
/** @type {Savegame} */ /** @type {Savegame} */
this.savegame; this.savegame;
} }
@ -220,7 +223,7 @@ export class InGameState extends GameState {
logger.log("Creating new game core"); logger.log("Creating new game core");
this.core = new GameCore(this.app); this.core = new GameCore(this.app);
this.core.initializeRoot(this, this.savegame); this.core.initializeRoot(this, this.savegame, this.gameModeId);
if (this.savegame.hasGameDump()) { if (this.savegame.hasGameDump()) {
this.stage4bResumeGame(); this.stage4bResumeGame();
@ -354,6 +357,7 @@ export class InGameState extends GameState {
this.creationPayload = payload; this.creationPayload = payload;
this.savegame = payload.savegame; this.savegame = payload.savegame;
this.gameModeId = payload.gameModeId;
this.loadingOverlay = new GameLoadingOverlay(this.app, this.getDivElement()); this.loadingOverlay = new GameLoadingOverlay(this.app, this.getDivElement());
this.loadingOverlay.showBasic(); this.loadingOverlay.showBasic();

View File

@ -15,6 +15,7 @@ import {
startFileChoose, startFileChoose,
waitNextFrame, waitNextFrame,
} from "../core/utils"; } from "../core/utils";
import { enumGameModeIds } from "../game/game_mode";
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { getApplicationSettingById } from "../profile/application_settings"; import { getApplicationSettingById } from "../profile/application_settings";
import { T } from "../translations"; import { T } from "../translations";
@ -82,6 +83,9 @@ export class MainMenuState extends GameState {
} }
<div class="buttons"></div> <div class="buttons"></div>
</div> </div>
<div class="bottomContainer">
<div class="buttons"></div>
</div>
</div> </div>
<div class="footer ${G_CHINA_VERSION ? "china" : ""}"> <div class="footer ${G_CHINA_VERSION ? "china" : ""}">
@ -204,6 +208,11 @@ export class MainMenuState extends GameState {
const qs = this.htmlElement.querySelector.bind(this.htmlElement); const qs = this.htmlElement.querySelector.bind(this.htmlElement);
if (G_IS_DEV && globalConfig.debug.fastGameEnter) { if (G_IS_DEV && globalConfig.debug.fastGameEnter) {
if (globalConfig.debug.testPuzzleMode) {
this.onPuzzleEditButtonClicked();
return;
}
const games = this.app.savegameMgr.getSavegamesMetaData(); const games = this.app.savegameMgr.getSavegamesMetaData();
if (games.length > 0 && globalConfig.debug.resumeGameOnFastEnter) { if (games.length > 0 && globalConfig.debug.resumeGameOnFastEnter) {
this.resumeGame(games[0]); this.resumeGame(games[0]);
@ -304,6 +313,68 @@ export class MainMenuState extends GameState {
this.trackClicks(playBtn, this.onPlayButtonClicked); this.trackClicks(playBtn, this.onPlayButtonClicked);
buttonContainer.appendChild(importButtonElement); buttonContainer.appendChild(importButtonElement);
} }
const bottomButtonContainer = this.htmlElement.querySelector(".bottomContainer .buttons");
removeAllChildren(bottomButtonContainer);
const puzzleModeButton = makeButton(bottomButtonContainer, ["styledButton"], T.mainMenu.puzzleMode);
bottomButtonContainer.appendChild(puzzleModeButton);
this.trackClicks(puzzleModeButton, this.onPuzzleModeButtonClicked);
}
renderPuzzleModeMenu() {
const savegames = this.htmlElement.querySelector(".mainContainer .savegames");
if (savegames) {
savegames.remove();
}
const buttonContainer = this.htmlElement.querySelector(".mainContainer .buttons");
removeAllChildren(buttonContainer);
const playButtonElement = makeButtonElement(["playModeButton", "styledButton"], T.puzzleMenu.play);
const editButtonElement = makeButtonElement(["editModeButton", "styledButton"], T.puzzleMenu.edit);
buttonContainer.appendChild(playButtonElement);
this.trackClicks(playButtonElement, this.onPuzzlePlayButtonClicked);
buttonContainer.appendChild(editButtonElement);
this.trackClicks(editButtonElement, this.onPuzzleEditButtonClicked);
const bottomButtonContainer = this.htmlElement.querySelector(".bottomContainer .buttons");
removeAllChildren(bottomButtonContainer);
const backButton = makeButton(bottomButtonContainer, ["styledButton"], T.mainMenu.back);
bottomButtonContainer.appendChild(backButton);
this.trackClicks(backButton, this.onBackButtonClicked);
}
onPuzzlePlayButtonClicked() {
const savegame = this.app.savegameMgr.createNewSavegame();
this.moveToState("InGameState", {
gameModeId: enumGameModeIds.puzzlePlay,
savegame,
});
}
onPuzzleEditButtonClicked() {
const savegame = this.app.savegameMgr.createNewSavegame();
this.moveToState("InGameState", {
gameModeId: enumGameModeIds.puzzleEdit,
savegame,
});
}
onPuzzleModeButtonClicked() {
this.renderPuzzleModeMenu();
}
onBackButtonClicked() {
this.renderMainMenu();
this.renderSavegames();
} }
onSteamLinkClicked() { onSteamLinkClicked() {

View File

@ -116,6 +116,12 @@ mainMenu:
savegameLevel: Level <x> savegameLevel: Level <x>
savegameLevelUnknown: Unknown Level savegameLevelUnknown: Unknown Level
savegameUnnamed: Unnamed savegameUnnamed: Unnamed
puzzleMode: Puzzle Mode
back: Back
puzzleMenu:
play: Play
edit: Edit
dialogs: dialogs:
buttons: buttons:
@ -477,6 +483,24 @@ ingame:
title: Support me title: Support me
desc: I develop the game in my spare time! desc: I develop the game in my spare time!
modeMenu:
puzzleEditMode:
back:
title: Main Menu
next:
title: Playtest
desc: You will have to complete the puzzle before being able to publish it
puzzleEditTestMode:
back:
title: Edit
next:
title: Publish
puzzlePlayMode:
back:
title: Puzzle Menu
next:
title: Next
# All shop upgrades # All shop upgrades
shopUpgrades: shopUpgrades:
belt: belt:
@ -701,6 +725,16 @@ buildings:
name: Item Producer name: Item Producer
description: Available in sandbox mode only, outputs the given signal from the wires layer on the regular layer. description: Available in sandbox mode only, outputs the given signal from the wires layer on the regular layer.
constant_producer:
default:
name: &constant_producer Constant Producer
description: Outputs a shape, color or boolean (1 or 0) as specified.
goal_acceptor:
default:
name: &goal_acceptor Goal Acceptor
description: Accepts items and triggers a goal if the specified item and/or rate criteria are met.
storyRewards: storyRewards:
# Those are the rewards gained from completing the store # Those are the rewards gained from completing the store
reward_cutter_and_trash: reward_cutter_and_trash:
@ -1128,6 +1162,8 @@ keybindings:
analyzer: *analyzer analyzer: *analyzer
comparator: *comparator comparator: *comparator
item_producer: Item Producer (Sandbox) item_producer: Item Producer (Sandbox)
constant_producer: *constant_producer
goal_acceptor: *goal_acceptor
# --- # ---
pipette: Pipette pipette: Pipette