mirror of
https://github.com/tobspr/shapez.io.git
synced 2026-03-02 03:39:21 +00:00
Puzzle DLC (#1172)
* 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> * Performance optimizations (#1154) * 1.3.1 preparations * Minor fixes, update translations * Fix achievements not working * Lots of belt optimizations, ~15% performance boost * Puzzle mode, part 1 * Puzzle mode, part 2 * Fix missing import * Puzzle mode, part 3 * Fix typo * Puzzle mode, part 4 * Puzzle Mode fixes: Correct zone restrictions and more (#1155) * Hide Puzzle Editor Controls in regular game mode, fix typo * Disallow shrinking zone if there are buildings * Fix multi-tile buildings for shrinking * Puzzle mode, Refactor hud * Puzzle mode * Fixed typo in latest puzzle commit (#1156) * Allow completing puzzles * Puzzle mode, almost done * Bump version to 1.4.0 * Fixes * [puzzle] Prevent pipette cheats (miners, emitters) (#1158) * Puzzle mode, almost done * Allow clearing belts with 'B' * Multiple users for the puzzle dlc * Bump api key * Minor adjustments * Update * Minor fixes * Fix throughput * Fix belts * Minor puzzle adjustments * New difficulty * Minor puzzle improvements * Fix belt path * Update translations * Added a button to return to the menu after a puzzle is completed (#1170) * added another button to return to the menu * improved menu return * fixed continue button to not go back to menu * [Puzzle] Added ability to lock buildings in the puzzle editor! (#1164) * initial test * tried to get it to work * added icon * added test exclusion * reverted css * completed flow for building locking * added lock option * finalized look and changed locked building to same sprite * removed unused art * added clearing every goal acceptor on lock to prevent creating impossible puzzles * heavily improved validation and prevented autocompletion * validation only checks every 100 ticks to improve performance * validation only checks every 100 ticks to improve performance * removed clearing goal acceptors as it isn't needed because of validation * Add soundtrack, puzzle dlc fixes Co-authored-by: Greg Considine <gconsidine@users.noreply.github.com> Co-authored-by: dengr1065 <dengr1065@gmail.com> Co-authored-by: Sense101 <67970865+Sense101@users.noreply.github.com>
This commit is contained in:
211
src/js/savegame/puzzle_serializer.js
Normal file
211
src/js/savegame/puzzle_serializer.js
Normal file
@@ -0,0 +1,211 @@
|
||||
/* typehints:start */
|
||||
import { GameRoot } from "../game/root";
|
||||
import { PuzzleGameMode } from "../game/modes/puzzle";
|
||||
/* typehints:end */
|
||||
import { enumConstantSignalType } from "../game/components/constant_signal";
|
||||
import { StaticMapEntityComponent } from "../game/components/static_map_entity";
|
||||
import { ShapeItem } from "../game/items/shape_item";
|
||||
import { Vector } from "../core/vector";
|
||||
import { MetaConstantProducerBuilding } from "../game/buildings/constant_producer";
|
||||
import { defaultBuildingVariant, MetaBuilding } from "../game/meta_building";
|
||||
import { gMetaBuildingRegistry } from "../core/global_registries";
|
||||
import { MetaGoalAcceptorBuilding } from "../game/buildings/goal_acceptor";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { BaseItem } from "../game/base_item";
|
||||
import trim from "trim";
|
||||
import { enumColors } from "../game/colors";
|
||||
import { COLOR_ITEM_SINGLETONS } from "../game/items/color_item";
|
||||
import { ShapeDefinition } from "../game/shape_definition";
|
||||
import { MetaBlockBuilding } from "../game/buildings/block";
|
||||
|
||||
const logger = createLogger("puzzle-serializer");
|
||||
|
||||
export class PuzzleSerializer {
|
||||
/**
|
||||
* Serializes the game root into a dump
|
||||
* @param {GameRoot} root
|
||||
* @returns {import("./savegame_typedefs").PuzzleGameData}
|
||||
*/
|
||||
generateDumpFromGameRoot(root) {
|
||||
console.log("serializing", root);
|
||||
|
||||
/**
|
||||
* @type {import("./savegame_typedefs").PuzzleGameData["buildings"]}
|
||||
*/
|
||||
let buildings = [];
|
||||
for (const entity of root.entityMgr.getAllWithComponent(StaticMapEntityComponent)) {
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
const signalComp = entity.components.ConstantSignal;
|
||||
|
||||
if (signalComp) {
|
||||
assert(signalComp.type === enumConstantSignalType.wireless, "not a wireless signal");
|
||||
assert(["shape", "color"].includes(signalComp.signal.getItemType()), "not a shape signal");
|
||||
buildings.push({
|
||||
type: "emitter",
|
||||
item: signalComp.signal.getAsCopyableKey(),
|
||||
pos: {
|
||||
x: staticComp.origin.x,
|
||||
y: staticComp.origin.y,
|
||||
r: staticComp.rotation,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const goalComp = entity.components.GoalAcceptor;
|
||||
if (goalComp) {
|
||||
assert(goalComp.item, "goals is missing item");
|
||||
assert(goalComp.item.getItemType() === "shape", "goal is not an item");
|
||||
buildings.push({
|
||||
type: "goal",
|
||||
item: goalComp.item.getAsCopyableKey(),
|
||||
pos: {
|
||||
x: staticComp.origin.x,
|
||||
y: staticComp.origin.y,
|
||||
r: staticComp.rotation,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (staticComp.getMetaBuilding().id === gMetaBuildingRegistry.findByClass(MetaBlockBuilding).id) {
|
||||
buildings.push({
|
||||
type: "block",
|
||||
pos: {
|
||||
x: staticComp.origin.x,
|
||||
y: staticComp.origin.y,
|
||||
r: staticComp.rotation,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const mode = /** @type {PuzzleGameMode} */ (root.gameMode);
|
||||
|
||||
const handles = root.hud.parts.buildingsToolbar.buildingHandles;
|
||||
const ids = gMetaBuildingRegistry.getAllIds();
|
||||
|
||||
/** @type {Array<string>} */
|
||||
let excludedBuildings = [];
|
||||
for (let i = 0; i < ids.length; ++i) {
|
||||
const handle = handles[ids[i]];
|
||||
if (handle && handle.puzzleLocked) {
|
||||
// @ts-ignore
|
||||
excludedBuildings.push(handle.metaBuilding.getId());
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
buildings,
|
||||
bounds: {
|
||||
w: mode.zoneWidth,
|
||||
h: mode.zoneHeight,
|
||||
},
|
||||
//read from the toolbar when making a puzzle
|
||||
excludedBuildings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to parse a signal code
|
||||
* @param {GameRoot} root
|
||||
* @param {string} code
|
||||
* @returns {BaseItem}
|
||||
*/
|
||||
parseItemCode(root, code) {
|
||||
if (!root || !root.shapeDefinitionMgr) {
|
||||
// Stale reference
|
||||
return null;
|
||||
}
|
||||
|
||||
code = trim(code);
|
||||
const codeLower = code.toLowerCase();
|
||||
|
||||
if (enumColors[codeLower]) {
|
||||
return COLOR_ITEM_SINGLETONS[codeLower];
|
||||
}
|
||||
|
||||
if (ShapeDefinition.isValidShortKey(code)) {
|
||||
return root.shapeDefinitionMgr.getShapeItemFromShortKey(code);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* @param {GameRoot} root
|
||||
* @param {import("./savegame_typedefs").PuzzleGameData} puzzle
|
||||
*/
|
||||
deserializePuzzle(root, puzzle) {
|
||||
if (puzzle.version !== 1) {
|
||||
return "invalid-version";
|
||||
}
|
||||
|
||||
for (const building of puzzle.buildings) {
|
||||
switch (building.type) {
|
||||
case "emitter": {
|
||||
const item = this.parseItemCode(root, building.item);
|
||||
if (!item) {
|
||||
return "bad-item:" + building.item;
|
||||
}
|
||||
|
||||
const entity = root.logic.tryPlaceBuilding({
|
||||
origin: new Vector(building.pos.x, building.pos.y),
|
||||
building: gMetaBuildingRegistry.findByClass(MetaConstantProducerBuilding),
|
||||
originalRotation: building.pos.r,
|
||||
rotation: building.pos.r,
|
||||
rotationVariant: 0,
|
||||
variant: defaultBuildingVariant,
|
||||
});
|
||||
if (!entity) {
|
||||
logger.warn("Failed to place emitter:", building);
|
||||
return "failed-to-place-emitter";
|
||||
}
|
||||
|
||||
entity.components.ConstantSignal.signal = item;
|
||||
break;
|
||||
}
|
||||
case "goal": {
|
||||
const item = this.parseItemCode(root, building.item);
|
||||
if (!item) {
|
||||
return "bad-item:" + building.item;
|
||||
}
|
||||
const entity = root.logic.tryPlaceBuilding({
|
||||
origin: new Vector(building.pos.x, building.pos.y),
|
||||
building: gMetaBuildingRegistry.findByClass(MetaGoalAcceptorBuilding),
|
||||
originalRotation: building.pos.r,
|
||||
rotation: building.pos.r,
|
||||
rotationVariant: 0,
|
||||
variant: defaultBuildingVariant,
|
||||
});
|
||||
if (!entity) {
|
||||
logger.warn("Failed to place goal:", building);
|
||||
return "failed-to-place-goal";
|
||||
}
|
||||
|
||||
entity.components.GoalAcceptor.item = item;
|
||||
break;
|
||||
}
|
||||
case "block": {
|
||||
const entity = root.logic.tryPlaceBuilding({
|
||||
origin: new Vector(building.pos.x, building.pos.y),
|
||||
building: gMetaBuildingRegistry.findByClass(MetaBlockBuilding),
|
||||
originalRotation: building.pos.r,
|
||||
rotation: building.pos.r,
|
||||
rotationVariant: 0,
|
||||
variant: defaultBuildingVariant,
|
||||
});
|
||||
if (!entity) {
|
||||
logger.warn("Failed to place block:", building);
|
||||
return "failed-to-place-block";
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// @ts-ignore
|
||||
return "invalid-building-type: " + building.type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,13 @@ 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.hud.parts.pinnedShapes ? root.hud.parts.pinnedShapes.serialize() : null,
|
||||
waypoints: root.hud.parts.waypoints ? root.hud.parts.waypoints.serialize() : null,
|
||||
};
|
||||
|
||||
if (G_IS_DEV) {
|
||||
@@ -130,12 +133,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.hud.parts.pinnedShapes) {
|
||||
errorReason = errorReason || root.hud.parts.pinnedShapes.deserialize(savegame.pinnedShapes);
|
||||
}
|
||||
|
||||
if (root.hud.parts.waypoints) {
|
||||
errorReason = errorReason || root.hud.parts.waypoints.deserialize(savegame.waypoints);
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if (errorReason) {
|
||||
return ExplainedResult.bad(errorReason);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
* time: any,
|
||||
* entityMgr: any,
|
||||
* map: any,
|
||||
* gameMode: object,
|
||||
* hubGoals: any,
|
||||
* pinnedShapes: any,
|
||||
* waypoints: any,
|
||||
@@ -40,4 +41,61 @@
|
||||
* }} SavegamesData
|
||||
*/
|
||||
|
||||
import { MetaBuilding } from "../game/meta_building";
|
||||
|
||||
// Notice: Update backend too
|
||||
/**
|
||||
* @typedef {{
|
||||
* id: number;
|
||||
* shortKey: string;
|
||||
* likes: number;
|
||||
* downloads: number;
|
||||
* completions: number;
|
||||
* difficulty: number | null;
|
||||
* averageTime: number | null;
|
||||
* title: string;
|
||||
* author: string;
|
||||
* completed: boolean;
|
||||
* }} PuzzleMetadata
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* type: "emitter";
|
||||
* item: string;
|
||||
* pos: { x: number; y: number; r: number }
|
||||
* }} PuzzleGameBuildingConstantProducer
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* type: "goal";
|
||||
* item: string;
|
||||
* pos: { x: number; y: number; r: number }
|
||||
* }} PuzzleGameBuildingGoal
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* type: "block";
|
||||
* pos: { x: number; y: number; r: number }
|
||||
* }} PuzzleGameBuildingBlock
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* version: number;
|
||||
* bounds: { w: number; h: number; },
|
||||
* buildings: (PuzzleGameBuildingGoal | PuzzleGameBuildingConstantProducer | PuzzleGameBuildingBlock)[],
|
||||
* excludedBuildings: Array<string>,
|
||||
* }} PuzzleGameData
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* meta: PuzzleMetadata,
|
||||
* game: PuzzleGameData
|
||||
* }} PuzzleFullData
|
||||
*/
|
||||
|
||||
export default {};
|
||||
|
||||
34
src/js/savegame/schemas/1009.js
Normal file
34
src/js/savegame/schemas/1009.js
Normal 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: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
5
src/js/savegame/schemas/1009.json
Normal file
5
src/js/savegame/schemas/1009.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [],
|
||||
"additionalProperties": true
|
||||
}
|
||||
Reference in New Issue
Block a user