1
0
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:
tobspr
2021-05-23 16:32:05 +02:00
committed by GitHub
parent 5f0a95ba11
commit 931c8a5821
167 changed files with 14001 additions and 8193 deletions

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

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

View File

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

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
}