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/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,

View File

@ -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 */

View File

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

View File

@ -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,
// -----------------------------------------------------------------------------------

View File

@ -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<Component>} */
export let gComponentRegistry = new Factory("component");
/** @type {FactoryTemplate<GameMode>} */
export let gGameModeRegistry = new Factory("gameMode");
/** @type {FactoryTemplate<BaseGameSpeed>} */
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);
}
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);
}
}
}

View File

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

View File

@ -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<number>}
@ -41,4 +54,8 @@ export class BeltReaderComponent extends Component {
*/
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 { 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;
}
}

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",
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;

View File

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

View File

@ -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,

View File

@ -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 */
}
}

View File

@ -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<UpgradeRequirement>
* improvement?: number,
* excludePrevious?: boolean
* }} TierRequirement */
/** @enum {string} */
export const enumGameModeTypes = {
default: "defaultModeType",
puzzle: "puzzleModeType",
};
/** @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
*/
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<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
* @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<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 { 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");
}

View File

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

View File

@ -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
*/

View File

@ -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<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 {Array<typeof MetaBuilding>}

View File

@ -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,

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");
/** @type {Array<Waypoint>}
*/
this.waypoints = [
{
label: null,
center: { x: 0, y: 0 },
zoomLevel: 3,
layer: gMetaBuildingRegistry.findByClass(MetaHubBuilding).getLayer(),
},
];
/** @type {Array<Waypoint>} */
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, {

View File

@ -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:

View File

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

View File

@ -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(

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 { 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<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 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<string, import("../game_mode").UpgradeTiers>} */
* @returns {Object<string, UpgradeTiers>} */
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<string, UpgradeTiers>}
*/
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<LevelDefinition>}
*/
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();
}
}

View File

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

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

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

View File

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

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",
"textColorCapped": "#ef5072",
"background": "rgba(40, 50, 60, 0.8)"
},
"zone": {
"background": "#3e3f47",
"border": "#667964"
}
},

View File

@ -48,6 +48,11 @@
"textColor": "#fff",
"textColorCapped": "#ef5072",
"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 { 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;

View File

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

View File

@ -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.<number, typeof BaseSavegameInterface>} */
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");

View File

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

View File

@ -12,6 +12,7 @@
* time: any,
* entityMgr: any,
* map: any,
* gameMode: object,
* hubGoals: any,
* pinnedShapes: 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} */
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();

View File

@ -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 {
}
<div class="buttons"></div>
</div>
<div class="bottomContainer">
<div class="buttons"></div>
</div>
</div>
<div class="footer ${G_CHINA_VERSION ? "china" : ""}">
@ -204,6 +208,11 @@ export class MainMenuState extends GameState {
const qs = this.htmlElement.querySelector.bind(this.htmlElement);
if (G_IS_DEV && globalConfig.debug.fastGameEnter) {
if (globalConfig.debug.testPuzzleMode) {
this.onPuzzleEditButtonClicked();
return;
}
const games = this.app.savegameMgr.getSavegamesMetaData();
if (games.length > 0 && globalConfig.debug.resumeGameOnFastEnter) {
this.resumeGame(games[0]);
@ -304,6 +313,68 @@ export class MainMenuState extends GameState {
this.trackClicks(playBtn, this.onPlayButtonClicked);
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() {

View File

@ -116,6 +116,12 @@ mainMenu:
savegameLevel: Level <x>
savegameLevelUnknown: Unknown Level
savegameUnnamed: Unnamed
puzzleMode: Puzzle Mode
back: Back
puzzleMenu:
play: Play
edit: Edit
dialogs:
buttons:
@ -477,6 +483,24 @@ ingame:
title: Support me
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
shopUpgrades:
belt:
@ -701,6 +725,16 @@ buildings:
name: Item Producer
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:
# Those are the rewards gained from completing the store
reward_cutter_and_trash:
@ -1128,6 +1162,8 @@ keybindings:
analyzer: *analyzer
comparator: *comparator
item_producer: Item Producer (Sandbox)
constant_producer: *constant_producer
goal_acceptor: *goal_acceptor
# ---
pipette: Pipette