Introduce game modes and get rid of global level definitions etc

This commit is contained in:
tobspr 2020-10-07 08:36:02 +02:00
parent 816fd37b55
commit 94266173d8
16 changed files with 1021 additions and 926 deletions

View File

@ -1,13 +1,9 @@
import { globalConfig } from "../core/config"; import { globalConfig } from "../core/config";
import { DrawParameters } from "../core/draw_parameters"; import { DrawParameters } from "../core/draw_parameters";
import { createLogger } from "../core/logging";
import { findNiceIntegerValue } from "../core/utils"; import { findNiceIntegerValue } from "../core/utils";
import { Vector } from "../core/vector"; import { Vector } from "../core/vector";
import { Entity } from "./entity"; import { Entity } from "./entity";
import { GameRoot } from "./root"; import { GameRoot } from "./root";
import { blueprintShape } from "./upgrades";
const logger = createLogger("blueprint");
export class Blueprint { export class Blueprint {
/** /**
@ -142,7 +138,7 @@ export class Blueprint {
* @param {GameRoot} root * @param {GameRoot} root
*/ */
canAfford(root) { canAfford(root) {
return root.hubGoals.getShapesStoredByKey(blueprintShape) >= this.getCost(); return root.hubGoals.getShapesStoredByKey(root.gameMode.getBlueprintShapeKey()) >= this.getCost();
} }
/** /**

View File

@ -31,6 +31,7 @@ import { KeyActionMapper } from "./key_action_mapper";
import { GameLogic } from "./logic"; import { GameLogic } from "./logic";
import { MapView } from "./map_view"; import { MapView } from "./map_view";
import { defaultBuildingVariant } from "./meta_building"; import { defaultBuildingVariant } from "./meta_building";
import { RegularGameMode } from "./modes/regular";
import { ProductionAnalytics } from "./production_analytics"; import { ProductionAnalytics } from "./production_analytics";
import { GameRoot } from "./root"; import { GameRoot } from "./root";
import { ShapeDefinitionManager } from "./shape_definition_manager"; import { ShapeDefinitionManager } from "./shape_definition_manager";
@ -101,6 +102,9 @@ export class GameCore {
// Needs to come first // Needs to come first
root.dynamicTickrate = new DynamicTickrate(root); root.dynamicTickrate = new DynamicTickrate(root);
// Init game mode
root.gameMode = new RegularGameMode(root);
// Init classes // Init classes
root.camera = new Camera(root); root.camera = new Camera(root);
root.map = new MapView(root); root.map = new MapView(root);

71
src/js/game/game_mode.js Normal file
View File

@ -0,0 +1,71 @@
/* typehints:start */
import { enumHubGoalRewards } from "./tutorial_goals";
/* typehints:end */
import { GameRoot } from "./root";
/** @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 */
export class GameMode {
/**
*
* @param {GameRoot} root
*/
constructor(root) {
this.root = root;
}
/**
* 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
* @returns {boolean}
*/
getIsFreeplayAvailable() {
return true;
}
}

View File

@ -1,14 +1,13 @@
import { globalConfig, IS_DEMO } from "../core/config"; import { globalConfig, IS_DEMO } from "../core/config";
import { RandomNumberGenerator } from "../core/rng"; import { RandomNumberGenerator } from "../core/rng";
import { clamp, findNiceIntegerValue, randomChoice, randomInt } from "../core/utils"; import { clamp } from "../core/utils";
import { BasicSerializableObject, types } from "../savegame/serialization"; import { BasicSerializableObject, types } from "../savegame/serialization";
import { enumColors } from "./colors"; import { enumColors } from "./colors";
import { enumItemProcessorTypes } from "./components/item_processor"; import { enumItemProcessorTypes } from "./components/item_processor";
import { enumAnalyticsDataSource } from "./production_analytics"; import { enumAnalyticsDataSource } from "./production_analytics";
import { GameRoot } from "./root"; import { GameRoot } from "./root";
import { enumSubShape, ShapeDefinition } from "./shape_definition"; import { enumSubShape, ShapeDefinition } from "./shape_definition";
import { enumHubGoalRewards, tutorialGoals } from "./tutorial_goals"; import { enumHubGoalRewards } from "./tutorial_goals";
import { UPGRADES } from "./upgrades";
export class HubGoals extends BasicSerializableObject { export class HubGoals extends BasicSerializableObject {
static getId() { static getId() {
@ -23,27 +22,36 @@ export class HubGoals extends BasicSerializableObject {
}; };
} }
deserialize(data) { /**
*
* @param {*} data
* @param {GameRoot} root
*/
deserialize(data, root) {
const errorCode = super.deserialize(data); const errorCode = super.deserialize(data);
if (errorCode) { if (errorCode) {
return errorCode; return errorCode;
} }
if (IS_DEMO) { const levels = root.gameMode.getLevelDefinitions();
this.level = Math.min(this.level, tutorialGoals.length);
// If freeplay is not available, clamp the level
if (!root.gameMode.getIsFreeplayAvailable()) {
this.level = Math.min(this.level, levels.length);
} }
// Compute gained rewards // Compute gained rewards
for (let i = 0; i < this.level - 1; ++i) { for (let i = 0; i < this.level - 1; ++i) {
if (i < tutorialGoals.length) { if (i < levels.length) {
const reward = tutorialGoals[i].reward; const reward = levels[i].reward;
this.gainedRewards[reward] = (this.gainedRewards[reward] || 0) + 1; this.gainedRewards[reward] = (this.gainedRewards[reward] || 0) + 1;
} }
} }
// Compute upgrade improvements // Compute upgrade improvements
for (const upgradeId in UPGRADES) { const upgrades = this.root.gameMode.getUpgrades();
const tiers = UPGRADES[upgradeId]; for (const upgradeId in upgrades) {
const tiers = upgrades[upgradeId];
const level = this.upgradeLevels[upgradeId] || 0; const level = this.upgradeLevels[upgradeId] || 0;
let totalImprovement = 1; let totalImprovement = 1;
for (let i = 0; i < level; ++i) { for (let i = 0; i < level; ++i) {
@ -84,17 +92,16 @@ export class HubGoals extends BasicSerializableObject {
*/ */
this.upgradeLevels = {}; this.upgradeLevels = {};
// Reset levels
for (const key in UPGRADES) {
this.upgradeLevels[key] = 0;
}
/** /**
* Stores the improvements for all upgrades * Stores the improvements for all upgrades
* @type {Object<string, number>} * @type {Object<string, number>}
*/ */
this.upgradeImprovements = {}; this.upgradeImprovements = {};
for (const key in UPGRADES) {
// Reset levels first
const upgrades = this.root.gameMode.getUpgrades();
for (const key in upgrades) {
this.upgradeLevels[key] = 0;
this.upgradeImprovements[key] = 1; this.upgradeImprovements[key] = 1;
} }
@ -120,7 +127,10 @@ export class HubGoals extends BasicSerializableObject {
* @returns {boolean} * @returns {boolean}
*/ */
isEndOfDemoReached() { isEndOfDemoReached() {
return IS_DEMO && this.level >= tutorialGoals.length; return (
!this.root.gameMode.getIsFreeplayAvailable() &&
this.level >= this.root.gameMode.getLevelDefinitions().length
);
} }
/** /**
@ -215,8 +225,9 @@ export class HubGoals extends BasicSerializableObject {
*/ */
computeNextGoal() { computeNextGoal() {
const storyIndex = this.level - 1; const storyIndex = this.level - 1;
if (storyIndex < tutorialGoals.length) { const levels = this.root.gameMode.getLevelDefinitions();
const { shape, required, reward, throughputOnly } = tutorialGoals[storyIndex]; if (storyIndex < levels.length) {
const { shape, required, reward, throughputOnly } = levels[storyIndex];
this.currentGoal = { this.currentGoal = {
/** @type {ShapeDefinition} */ /** @type {ShapeDefinition} */
definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(shape), definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(shape),
@ -254,7 +265,7 @@ export class HubGoals extends BasicSerializableObject {
* Returns whether we are playing in free-play * Returns whether we are playing in free-play
*/ */
isFreePlay() { isFreePlay() {
return this.level >= tutorialGoals.length; return this.level >= this.root.gameMode.getLevelDefinitions().length;
} }
/** /**
@ -262,7 +273,7 @@ export class HubGoals extends BasicSerializableObject {
* @param {string} upgradeId * @param {string} upgradeId
*/ */
canUnlockUpgrade(upgradeId) { canUnlockUpgrade(upgradeId) {
const tiers = UPGRADES[upgradeId]; const tiers = this.root.gameMode.getUpgrades()[upgradeId];
const currentLevel = this.getUpgradeLevel(upgradeId); const currentLevel = this.getUpgradeLevel(upgradeId);
if (currentLevel >= tiers.length) { if (currentLevel >= tiers.length) {
@ -296,7 +307,7 @@ export class HubGoals extends BasicSerializableObject {
*/ */
getAvailableUpgradeCount() { getAvailableUpgradeCount() {
let count = 0; let count = 0;
for (const upgradeId in UPGRADES) { for (const upgradeId in this.root.gameMode.getUpgrades()) {
if (this.canUnlockUpgrade(upgradeId)) { if (this.canUnlockUpgrade(upgradeId)) {
++count; ++count;
} }
@ -314,7 +325,7 @@ export class HubGoals extends BasicSerializableObject {
return false; return false;
} }
const upgradeTiers = UPGRADES[upgradeId]; const upgradeTiers = this.root.gameMode.getUpgrades()[upgradeId];
const currentLevel = this.getUpgradeLevel(upgradeId); const currentLevel = this.getUpgradeLevel(upgradeId);
const tierData = upgradeTiers[currentLevel]; const tierData = upgradeTiers[currentLevel];

View File

@ -1,202 +1,203 @@
import { DrawParameters } from "../../../core/draw_parameters"; import { DrawParameters } from "../../../core/draw_parameters";
import { STOP_PROPAGATION } from "../../../core/signal"; import { STOP_PROPAGATION } from "../../../core/signal";
import { TrackedState } from "../../../core/tracked_state"; import { TrackedState } from "../../../core/tracked_state";
import { makeDiv } from "../../../core/utils"; import { makeDiv } from "../../../core/utils";
import { Vector } from "../../../core/vector"; import { Vector } from "../../../core/vector";
import { T } from "../../../translations"; import { SOUNDS } from "../../../platform/sound";
import { enumMouseButton } from "../../camera"; import { T } from "../../../translations";
import { KEYMAPPINGS } from "../../key_action_mapper"; import { Blueprint } from "../../blueprint";
import { blueprintShape } from "../../upgrades"; import { enumMouseButton } from "../../camera";
import { BaseHUDPart } from "../base_hud_part"; import { KEYMAPPINGS } from "../../key_action_mapper";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { BaseHUDPart } from "../base_hud_part";
import { Blueprint } from "../../blueprint"; import { DynamicDomAttach } from "../dynamic_dom_attach";
import { SOUNDS } from "../../../platform/sound";
export class HUDBlueprintPlacer extends BaseHUDPart {
export class HUDBlueprintPlacer extends BaseHUDPart { createElements(parent) {
createElements(parent) { const blueprintCostShape = this.root.shapeDefinitionMgr.getShapeFromShortKey(
const blueprintCostShape = this.root.shapeDefinitionMgr.getShapeFromShortKey(blueprintShape); this.root.gameMode.getBlueprintShapeKey()
const blueprintCostShapeCanvas = blueprintCostShape.generateAsCanvas(80); );
const blueprintCostShapeCanvas = blueprintCostShape.generateAsCanvas(80);
this.costDisplayParent = makeDiv(parent, "ingame_HUD_BlueprintPlacer", [], ``);
this.costDisplayParent = makeDiv(parent, "ingame_HUD_BlueprintPlacer", [], ``);
makeDiv(this.costDisplayParent, null, ["label"], T.ingame.blueprintPlacer.cost);
const costContainer = makeDiv(this.costDisplayParent, null, ["costContainer"], ""); makeDiv(this.costDisplayParent, null, ["label"], T.ingame.blueprintPlacer.cost);
this.costDisplayText = makeDiv(costContainer, null, ["costText"], ""); const costContainer = makeDiv(this.costDisplayParent, null, ["costContainer"], "");
costContainer.appendChild(blueprintCostShapeCanvas); this.costDisplayText = makeDiv(costContainer, null, ["costText"], "");
} costContainer.appendChild(blueprintCostShapeCanvas);
}
initialize() {
this.root.hud.signals.buildingsSelectedForCopy.add(this.createBlueprintFromBuildings, this); initialize() {
this.root.hud.signals.buildingsSelectedForCopy.add(this.createBlueprintFromBuildings, this);
/** @type {TypedTrackedState<Blueprint?>} */
this.currentBlueprint = new TrackedState(this.onBlueprintChanged, this); /** @type {TypedTrackedState<Blueprint?>} */
/** @type {Blueprint?} */ this.currentBlueprint = new TrackedState(this.onBlueprintChanged, this);
this.lastBlueprintUsed = null; /** @type {Blueprint?} */
this.lastBlueprintUsed = null;
const keyActionMapper = this.root.keyMapper;
keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.abortPlacement, this); const keyActionMapper = this.root.keyMapper;
keyActionMapper.getBinding(KEYMAPPINGS.placement.pipette).add(this.abortPlacement, this); keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.abortPlacement, this);
keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.rotateBlueprint, this); keyActionMapper.getBinding(KEYMAPPINGS.placement.pipette).add(this.abortPlacement, this);
keyActionMapper.getBinding(KEYMAPPINGS.massSelect.pasteLastBlueprint).add(this.pasteBlueprint, this); keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.rotateBlueprint, this);
keyActionMapper.getBinding(KEYMAPPINGS.massSelect.pasteLastBlueprint).add(this.pasteBlueprint, this);
this.root.camera.downPreHandler.add(this.onMouseDown, this);
this.root.camera.movePreHandler.add(this.onMouseMove, this); this.root.camera.downPreHandler.add(this.onMouseDown, this);
this.root.camera.movePreHandler.add(this.onMouseMove, this);
this.root.hud.signals.selectedPlacementBuildingChanged.add(this.abortPlacement, this);
this.root.signals.editModeChanged.add(this.onEditModeChanged, this); this.root.hud.signals.selectedPlacementBuildingChanged.add(this.abortPlacement, this);
this.root.signals.editModeChanged.add(this.onEditModeChanged, this);
this.domAttach = new DynamicDomAttach(this.root, this.costDisplayParent);
this.trackedCanAfford = new TrackedState(this.onCanAffordChanged, this); this.domAttach = new DynamicDomAttach(this.root, this.costDisplayParent);
} this.trackedCanAfford = new TrackedState(this.onCanAffordChanged, this);
}
abortPlacement() {
if (this.currentBlueprint.get()) { abortPlacement() {
this.currentBlueprint.set(null); if (this.currentBlueprint.get()) {
this.currentBlueprint.set(null);
return STOP_PROPAGATION;
} return STOP_PROPAGATION;
} }
}
/**
* Called when the layer was changed /**
* @param {Layer} layer * Called when the layer was changed
*/ * @param {Layer} layer
onEditModeChanged(layer) { */
// Check if the layer of the blueprint differs and thus we have to deselect it onEditModeChanged(layer) {
const blueprint = this.currentBlueprint.get(); // Check if the layer of the blueprint differs and thus we have to deselect it
if (blueprint) { const blueprint = this.currentBlueprint.get();
if (blueprint.layer !== layer) { if (blueprint) {
this.currentBlueprint.set(null); if (blueprint.layer !== layer) {
} this.currentBlueprint.set(null);
} }
} }
}
/**
* Called when the blueprint is now affordable or not /**
* @param {boolean} canAfford * Called when the blueprint is now affordable or not
*/ * @param {boolean} canAfford
onCanAffordChanged(canAfford) { */
this.costDisplayParent.classList.toggle("canAfford", canAfford); onCanAffordChanged(canAfford) {
} this.costDisplayParent.classList.toggle("canAfford", canAfford);
}
update() {
const currentBlueprint = this.currentBlueprint.get(); update() {
this.domAttach.update(currentBlueprint && currentBlueprint.getCost() > 0); const currentBlueprint = this.currentBlueprint.get();
this.trackedCanAfford.set(currentBlueprint && currentBlueprint.canAfford(this.root)); this.domAttach.update(currentBlueprint && currentBlueprint.getCost() > 0);
} this.trackedCanAfford.set(currentBlueprint && currentBlueprint.canAfford(this.root));
}
/**
* Called when the blueprint was changed /**
* @param {Blueprint} blueprint * Called when the blueprint was changed
*/ * @param {Blueprint} blueprint
onBlueprintChanged(blueprint) { */
if (blueprint) { onBlueprintChanged(blueprint) {
this.lastBlueprintUsed = blueprint; if (blueprint) {
this.costDisplayText.innerText = "" + blueprint.getCost(); this.lastBlueprintUsed = blueprint;
} this.costDisplayText.innerText = "" + blueprint.getCost();
} }
}
/**
* mouse down pre handler /**
* @param {Vector} pos * mouse down pre handler
* @param {enumMouseButton} button * @param {Vector} pos
*/ * @param {enumMouseButton} button
onMouseDown(pos, button) { */
if (button === enumMouseButton.right) { onMouseDown(pos, button) {
if (this.currentBlueprint.get()) { if (button === enumMouseButton.right) {
this.abortPlacement(); if (this.currentBlueprint.get()) {
return STOP_PROPAGATION; this.abortPlacement();
} return STOP_PROPAGATION;
} }
}
const blueprint = this.currentBlueprint.get();
if (!blueprint) { const blueprint = this.currentBlueprint.get();
return; if (!blueprint) {
} return;
}
if (!blueprint.canAfford(this.root)) {
this.root.soundProxy.playUiError(); if (!blueprint.canAfford(this.root)) {
return; this.root.soundProxy.playUiError();
} return;
}
const worldPos = this.root.camera.screenToWorld(pos);
const tile = worldPos.toTileSpace(); const worldPos = this.root.camera.screenToWorld(pos);
if (blueprint.tryPlace(this.root, tile)) { const tile = worldPos.toTileSpace();
const cost = blueprint.getCost(); if (blueprint.tryPlace(this.root, tile)) {
this.root.hubGoals.takeShapeByKey(blueprintShape, cost); const cost = blueprint.getCost();
this.root.soundProxy.playUi(SOUNDS.placeBuilding); this.root.hubGoals.takeShapeByKey(this.root.gameMode.getBlueprintShapeKey(), cost);
} this.root.soundProxy.playUi(SOUNDS.placeBuilding);
} }
}
/**
* Mose move handler /**
*/ * Mose move handler
onMouseMove() { */
// Prevent movement while blueprint is selected onMouseMove() {
if (this.currentBlueprint.get()) { // Prevent movement while blueprint is selected
return STOP_PROPAGATION; if (this.currentBlueprint.get()) {
} return STOP_PROPAGATION;
} }
}
/**
* Called when an array of bulidings was selected /**
* @param {Array<number>} uids * Called when an array of bulidings was selected
*/ * @param {Array<number>} uids
createBlueprintFromBuildings(uids) { */
if (uids.length === 0) { createBlueprintFromBuildings(uids) {
return; if (uids.length === 0) {
} return;
this.currentBlueprint.set(Blueprint.fromUids(this.root, uids)); }
} this.currentBlueprint.set(Blueprint.fromUids(this.root, uids));
}
/**
* Attempts to rotate the current blueprint /**
*/ * Attempts to rotate the current blueprint
rotateBlueprint() { */
if (this.currentBlueprint.get()) { rotateBlueprint() {
if (this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateInverseModifier).pressed) { if (this.currentBlueprint.get()) {
this.currentBlueprint.get().rotateCcw(); if (this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateInverseModifier).pressed) {
} else { this.currentBlueprint.get().rotateCcw();
this.currentBlueprint.get().rotateCw(); } else {
} this.currentBlueprint.get().rotateCw();
} }
} }
}
/**
* Attempts to paste the last blueprint /**
*/ * Attempts to paste the last blueprint
pasteBlueprint() { */
if (this.lastBlueprintUsed !== null) { pasteBlueprint() {
if (this.lastBlueprintUsed.layer !== this.root.currentLayer) { if (this.lastBlueprintUsed !== null) {
// Not compatible if (this.lastBlueprintUsed.layer !== this.root.currentLayer) {
this.root.soundProxy.playUiError(); // Not compatible
return; this.root.soundProxy.playUiError();
} return;
}
this.root.hud.signals.pasteBlueprintRequested.dispatch();
this.currentBlueprint.set(this.lastBlueprintUsed); this.root.hud.signals.pasteBlueprintRequested.dispatch();
} else { this.currentBlueprint.set(this.lastBlueprintUsed);
this.root.soundProxy.playUiError(); } else {
} this.root.soundProxy.playUiError();
} }
}
/**
* /**
* @param {DrawParameters} parameters *
*/ * @param {DrawParameters} parameters
draw(parameters) { */
const blueprint = this.currentBlueprint.get(); draw(parameters) {
if (!blueprint) { const blueprint = this.currentBlueprint.get();
return; if (!blueprint) {
} return;
const mousePosition = this.root.app.mousePosition; }
if (!mousePosition) { const mousePosition = this.root.app.mousePosition;
// Not on screen if (!mousePosition) {
return; // Not on screen
} return;
}
const worldPos = this.root.camera.screenToWorld(mousePosition);
const tile = worldPos.toTileSpace(); const worldPos = this.root.camera.screenToWorld(mousePosition);
blueprint.draw(parameters, tile); const tile = worldPos.toTileSpace();
} blueprint.draw(parameters, tile);
} }
}

View File

@ -1,9 +1,7 @@
import { BaseHUDPart } from "../base_hud_part";
import { makeDiv } from "../../../core/utils"; import { makeDiv } from "../../../core/utils";
import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
import { blueprintShape, UPGRADES } from "../../upgrades";
import { enumNotificationType } from "./notifications"; import { enumNotificationType } from "./notifications";
import { tutorialGoals } from "../../tutorial_goals";
export class HUDSandboxController extends BaseHUDPart { export class HUDSandboxController extends BaseHUDPart {
createElements(parent) { createElements(parent) {
@ -75,10 +73,11 @@ export class HUDSandboxController extends BaseHUDPart {
} }
giveBlueprints() { giveBlueprints() {
if (!this.root.hubGoals.storedShapes[blueprintShape]) { const shape = this.root.gameMode.getBlueprintShapeKey();
this.root.hubGoals.storedShapes[blueprintShape] = 0; if (!this.root.hubGoals.storedShapes[shape]) {
this.root.hubGoals.storedShapes[shape] = 0;
} }
this.root.hubGoals.storedShapes[blueprintShape] += 1e9; this.root.hubGoals.storedShapes[shape] += 1e9;
} }
maxOutAll() { maxOutAll() {
@ -89,7 +88,7 @@ export class HUDSandboxController extends BaseHUDPart {
} }
modifyUpgrade(id, amount) { modifyUpgrade(id, amount) {
const upgradeTiers = UPGRADES[id]; const upgradeTiers = this.root.gameMode.getUpgrades()[id];
const maxLevel = upgradeTiers.length; const maxLevel = upgradeTiers.length;
this.root.hubGoals.upgradeLevels[id] = Math.max( this.root.hubGoals.upgradeLevels[id] = Math.max(
@ -122,9 +121,10 @@ export class HUDSandboxController extends BaseHUDPart {
// Compute gained rewards // Compute gained rewards
hubGoals.gainedRewards = {}; hubGoals.gainedRewards = {};
const levels = this.root.gameMode.getLevelDefinitions();
for (let i = 0; i < hubGoals.level - 1; ++i) { for (let i = 0; i < hubGoals.level - 1; ++i) {
if (i < tutorialGoals.length) { if (i < levels.length) {
const reward = tutorialGoals[i].reward; const reward = levels[i].reward;
hubGoals.gainedRewards[reward] = (hubGoals.gainedRewards[reward] || 0) + 1; hubGoals.gainedRewards[reward] = (hubGoals.gainedRewards[reward] || 0) + 1;
} }
} }

View File

@ -3,7 +3,6 @@ import { InputReceiver } from "../../../core/input_receiver";
import { formatBigNumber, makeDiv } from "../../../core/utils"; import { formatBigNumber, makeDiv } from "../../../core/utils";
import { T } from "../../../translations"; import { T } from "../../../translations";
import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper";
import { UPGRADES } from "../../upgrades";
import { BaseHUDPart } from "../base_hud_part"; import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
@ -21,7 +20,7 @@ export class HUDShop extends BaseHUDPart {
this.upgradeToElements = {}; this.upgradeToElements = {};
// Upgrades // Upgrades
for (const upgradeId in UPGRADES) { for (const upgradeId in this.root.gameMode.getUpgrades()) {
const handle = {}; const handle = {};
handle.requireIndexToElement = []; handle.requireIndexToElement = [];
@ -59,7 +58,7 @@ export class HUDShop extends BaseHUDPart {
rerenderFull() { rerenderFull() {
for (const upgradeId in this.upgradeToElements) { for (const upgradeId in this.upgradeToElements) {
const handle = this.upgradeToElements[upgradeId]; const handle = this.upgradeToElements[upgradeId];
const upgradeTiers = UPGRADES[upgradeId]; const upgradeTiers = this.root.gameMode.getUpgrades()[upgradeId];
const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId); const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId);
const currentTierMultiplier = this.root.hubGoals.upgradeImprovements[upgradeId]; const currentTierMultiplier = this.root.hubGoals.upgradeImprovements[upgradeId];

View File

@ -1,14 +1,14 @@
import { globalConfig } from "../../../core/config"; import { globalConfig } from "../../../core/config";
import { gMetaBuildingRegistry } from "../../../core/global_registries"; import { gMetaBuildingRegistry } from "../../../core/global_registries";
import { InputReceiver } from "../../../core/input_receiver";
import { makeDiv } from "../../../core/utils"; import { makeDiv } from "../../../core/utils";
import { SOUNDS } from "../../../platform/sound"; import { SOUNDS } from "../../../platform/sound";
import { T } from "../../../translations"; import { T } from "../../../translations";
import { defaultBuildingVariant } from "../../meta_building"; import { defaultBuildingVariant } from "../../meta_building";
import { enumHubGoalRewards, tutorialGoals } from "../../tutorial_goals"; import { enumHubGoalRewards } from "../../tutorial_goals";
import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings";
import { BaseHUDPart } from "../base_hud_part"; import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings";
import { InputReceiver } from "../../../core/input_receiver";
import { enumNotificationType } from "./notifications"; import { enumNotificationType } from "./notifications";
export class HUDUnlockNotification extends BaseHUDPart { export class HUDUnlockNotification extends BaseHUDPart {
@ -53,7 +53,9 @@ export class HUDUnlockNotification extends BaseHUDPart {
showForLevel(level, reward) { showForLevel(level, reward) {
this.root.soundProxy.playUi(SOUNDS.levelComplete); this.root.soundProxy.playUi(SOUNDS.levelComplete);
if (level > tutorialGoals.length) { const levels = this.root.gameMode.getLevelDefinitions();
// Don't use getIsFreeplay() because we want the freeplay level up to show
if (level > levels.length) {
this.root.hud.signals.notification.dispatch( this.root.hud.signals.notification.dispatch(
T.ingame.notifications.freeplayLevelComplete.replace("<level>", String(level)), T.ingame.notifications.freeplayLevelComplete.replace("<level>", String(level)),
enumNotificationType.success enumNotificationType.success

View File

@ -0,0 +1,445 @@
import { IS_DEMO } from "../../core/config";
import { findNiceIntegerValue } from "../../core/utils";
import { GameMode } from "../game_mode";
import { ShapeDefinition } from "../shape_definition";
import { enumHubGoalRewards } from "../tutorial_goals";
const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw";
const finalGameShape = "RuCw--Cw:----Ru--";
const preparementShape = "CpRpCp--:SwSwSwSw";
const blueprintShape = "CbCbCbRb:CwCwCwCw";
const fixedImprovements = [0.5, 0.5, 1, 1, 2, 1, 1];
const numEndgameUpgrades = !IS_DEMO ? 20 - fixedImprovements.length - 1 : 0;
function generateEndgameUpgrades() {
return new Array(numEndgameUpgrades).fill(null).map((_, i) => ({
required: [
{ shape: preparementShape, amount: 30000 + i * 10000 },
{ shape: finalGameShape, amount: 20000 + i * 5000 },
{ shape: rocketShape, amount: 20000 + i * 5000 },
],
excludePrevious: true,
}));
}
for (let i = 0; i < numEndgameUpgrades; ++i) {
fixedImprovements.push(0.1);
}
/** @type {Object<string, import("../game_mode").UpgradeTiers>} */
const cachedUpgrades = {
belt: [
{
required: [{ shape: "CuCuCuCu", amount: 60 }],
},
{
required: [{ shape: "--CuCu--", amount: 500 }],
},
{
required: [{ shape: "CpCpCpCp", amount: 1000 }],
},
{
required: [{ shape: "SrSrSrSr:CyCyCyCy", amount: 6000 }],
},
{
required: [{ shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", amount: 25000 }],
},
{
required: [{ shape: preparementShape, amount: 25000 }],
excludePrevious: true,
},
{
required: [
{ shape: preparementShape, amount: 25000 },
{ shape: finalGameShape, amount: 50000 },
],
excludePrevious: true,
},
...generateEndgameUpgrades(),
],
miner: [
{
required: [{ shape: "RuRuRuRu", amount: 300 }],
},
{
required: [{ shape: "Cu------", amount: 800 }],
},
{
required: [{ shape: "ScScScSc", amount: 3500 }],
},
{
required: [{ shape: "CwCwCwCw:WbWbWbWb", amount: 23000 }],
},
{
required: [{ shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", amount: 50000 }],
},
{
required: [{ shape: preparementShape, amount: 25000 }],
excludePrevious: true,
},
{
required: [
{ shape: preparementShape, amount: 25000 },
{ shape: finalGameShape, amount: 50000 },
],
excludePrevious: true,
},
...generateEndgameUpgrades(),
],
processors: [
{
required: [{ shape: "SuSuSuSu", amount: 500 }],
},
{
required: [{ shape: "RuRu----", amount: 600 }],
},
{
required: [{ shape: "CgScScCg", amount: 3500 }],
},
{
required: [{ shape: "CwCrCwCr:SgSgSgSg", amount: 25000 }],
},
{
required: [{ shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", amount: 50000 }],
},
{
required: [{ shape: preparementShape, amount: 25000 }],
excludePrevious: true,
},
{
required: [
{ shape: preparementShape, amount: 25000 },
{ shape: finalGameShape, amount: 50000 },
],
excludePrevious: true,
},
...generateEndgameUpgrades(),
],
painting: [
{
required: [{ shape: "RbRb----", amount: 600 }],
},
{
required: [{ shape: "WrWrWrWr", amount: 3800 }],
},
{
required: [{ shape: "RpRpRpRp:CwCwCwCw", amount: 6500 }],
},
{
required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp", amount: 25000 }],
},
{
required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp:CwCwCwCw", amount: 50000 }],
},
{
required: [{ shape: preparementShape, amount: 25000 }],
excludePrevious: true,
},
{
required: [
{ shape: preparementShape, amount: 25000 },
{ shape: finalGameShape, amount: 50000 },
],
excludePrevious: true,
},
...generateEndgameUpgrades(),
],
};
// Tiers need % of the previous tier as requirement too
const tierGrowth = 2.5;
// Automatically generate tier levels
for (const upgradeId in cachedUpgrades) {
const upgradeTiers = cachedUpgrades[upgradeId];
let currentTierRequirements = [];
for (let i = 0; i < upgradeTiers.length; ++i) {
const tierHandle = upgradeTiers[i];
tierHandle.improvement = fixedImprovements[i];
const originalRequired = tierHandle.required.slice();
for (let k = currentTierRequirements.length - 1; k >= 0; --k) {
const oldTierRequirement = currentTierRequirements[k];
if (!tierHandle.excludePrevious) {
tierHandle.required.unshift({
shape: oldTierRequirement.shape,
amount: oldTierRequirement.amount,
});
}
}
currentTierRequirements.push(
...originalRequired.map(req => ({
amount: req.amount,
shape: req.shape,
}))
);
currentTierRequirements.forEach(tier => {
tier.amount = findNiceIntegerValue(tier.amount * tierGrowth);
});
}
}
// VALIDATE
if (G_IS_DEV) {
for (const upgradeId in cachedUpgrades) {
cachedUpgrades[upgradeId].forEach(tier => {
tier.required.forEach(({ shape }) => {
try {
ShapeDefinition.fromShortKey(shape);
} catch (ex) {
throw new Error("Invalid upgrade goal: '" + ex + "' for shape" + shape);
}
});
});
}
}
const levelDefinitions = [
// 1
// Circle
{
shape: "CuCuCuCu", // belts t1
required: 30,
reward: enumHubGoalRewards.reward_cutter_and_trash,
},
// 2
// Cutter
{
shape: "----CuCu", //
required: 40,
reward: enumHubGoalRewards.no_reward,
},
// 3
// Rectangle
{
shape: "RuRuRuRu", // miners t1
required: 70,
reward: enumHubGoalRewards.reward_balancer,
},
// 4
{
shape: "RuRu----", // processors t2
required: 70,
reward: enumHubGoalRewards.reward_rotater,
},
// 5
// Rotater
{
shape: "Cu----Cu", // belts t2
required: 170,
reward: enumHubGoalRewards.reward_tunnel,
},
// 6
{
shape: "Cu------", // miners t2
required: 270,
reward: enumHubGoalRewards.reward_painter,
},
// 7
// Painter
{
shape: "CrCrCrCr", // unused
required: 300,
reward: enumHubGoalRewards.reward_rotater_ccw,
},
// 8
{
shape: "RbRb----", // painter t2
required: 480,
reward: enumHubGoalRewards.reward_mixer,
},
// 9
// Mixing (purple)
{
shape: "CpCpCpCp", // belts t3
required: 600,
reward: enumHubGoalRewards.reward_merger,
},
// 10
// STACKER: Star shape + cyan
{
shape: "ScScScSc", // miners t3
required: 800,
reward: enumHubGoalRewards.reward_stacker,
},
// 11
// Chainable miner
{
shape: "CgScScCg", // processors t3
required: 1000,
reward: enumHubGoalRewards.reward_miner_chainable,
},
// 12
// Blueprints
{
shape: "CbCbCbRb:CwCwCwCw",
required: 1000,
reward: enumHubGoalRewards.reward_blueprints,
},
// 13
// Tunnel Tier 2
{
shape: "RpRpRpRp:CwCwCwCw", // painting t3
required: 3800,
reward: enumHubGoalRewards.reward_underground_belt_tier_2,
},
// DEMO STOPS HERE
...(IS_DEMO
? [
{
shape: "RpRpRpRp:CwCwCwCw",
required: 0,
reward: enumHubGoalRewards.reward_demo_end,
},
]
: [
// 14
// Belt reader
{
shape: "--Cg----:--Cr----", // unused
required: 16, // Per second!
reward: enumHubGoalRewards.reward_belt_reader,
throughputOnly: true,
},
// 15
// Storage
{
shape: "SrSrSrSr:CyCyCyCy", // unused
required: 10000,
reward: enumHubGoalRewards.reward_storage,
},
// 16
// Quad Cutter
{
shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", // belts t4 (two variants)
required: 6000,
reward: enumHubGoalRewards.reward_cutter_quad,
},
// 17
// Double painter
{
shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", // miner t4 (two variants)
required: 20000,
reward: enumHubGoalRewards.reward_painter_double,
},
// 18
// Rotater (180deg)
{
shape: "Sg----Sg:CgCgCgCg:--CyCy--", // unused
required: 20000,
reward: enumHubGoalRewards.reward_rotater_180,
},
// 19
// Compact splitter
{
shape: "CpRpCp--:SwSwSwSw",
required: 25000,
reward: enumHubGoalRewards.reward_splitter,
},
// 20
// WIRES
{
shape: finalGameShape,
required: 25000,
reward: enumHubGoalRewards.reward_wires_painter_and_levers,
},
// 21
// Filter
{
shape: "CrCwCrCw:CwCrCwCr:CrCwCrCw:CwCrCwCr",
required: 25000,
reward: enumHubGoalRewards.reward_filter,
},
// 22
// Constant signal
{
shape: "Cg----Cr:Cw----Cw:Sy------:Cy----Cy",
required: 25000,
reward: enumHubGoalRewards.reward_constant_signal,
},
// 23
// Display
{
shape: "CcSyCcSy:SyCcSyCc:CcSyCcSy",
required: 25000,
reward: enumHubGoalRewards.reward_display,
},
// 24 Logic gates
{
shape: "CcRcCcRc:RwCwRwCw:Sr--Sw--:CyCyCyCy",
required: 25000,
reward: enumHubGoalRewards.reward_logic_gates,
},
// 25 Virtual Processing
{
shape: "Rg--Rg--:CwRwCwRw:--Rg--Rg",
required: 25000,
reward: enumHubGoalRewards.reward_virtual_processing,
},
// 26 Freeplay
{
shape: "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw",
required: 50000,
reward: enumHubGoalRewards.reward_freeplay,
},
]),
];
if (G_IS_DEV) {
levelDefinitions.forEach(({ shape }) => {
try {
ShapeDefinition.fromShortKey(shape);
} catch (ex) {
throw new Error("Invalid tutorial goal: '" + ex + "' for shape" + shape);
}
});
}
export class RegularGameMode extends GameMode {
constructor(root) {
super(root);
}
getUpgrades() {
return cachedUpgrades;
}
getBlueprintShapeKey() {
return blueprintShape;
}
getLevelDefinitions() {
return levelDefinitions;
}
}

View File

@ -1,221 +1,225 @@
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
import { Signal } from "../core/signal"; import { Signal } from "../core/signal";
import { RandomNumberGenerator } from "../core/rng"; import { RandomNumberGenerator } from "../core/rng";
import { createLogger } from "../core/logging"; import { createLogger } from "../core/logging";
// Type hints // Type hints
/* typehints:start */ /* typehints:start */
import { GameTime } from "./time/game_time"; import { GameTime } from "./time/game_time";
import { EntityManager } from "./entity_manager"; import { EntityManager } from "./entity_manager";
import { GameSystemManager } from "./game_system_manager"; import { GameSystemManager } from "./game_system_manager";
import { GameHUD } from "./hud/hud"; import { GameHUD } from "./hud/hud";
import { MapView } from "./map_view"; import { MapView } from "./map_view";
import { Camera } from "./camera"; import { Camera } from "./camera";
import { InGameState } from "../states/ingame"; import { InGameState } from "../states/ingame";
import { AutomaticSave } from "./automatic_save"; import { AutomaticSave } from "./automatic_save";
import { Application } from "../application"; import { Application } from "../application";
import { SoundProxy } from "./sound_proxy"; import { SoundProxy } from "./sound_proxy";
import { Savegame } from "../savegame/savegame"; import { Savegame } from "../savegame/savegame";
import { GameLogic } from "./logic"; import { GameLogic } from "./logic";
import { ShapeDefinitionManager } from "./shape_definition_manager"; import { ShapeDefinitionManager } from "./shape_definition_manager";
import { HubGoals } from "./hub_goals"; import { HubGoals } from "./hub_goals";
import { BufferMaintainer } from "../core/buffer_maintainer"; import { BufferMaintainer } from "../core/buffer_maintainer";
import { ProductionAnalytics } from "./production_analytics"; import { ProductionAnalytics } from "./production_analytics";
import { Entity } from "./entity"; import { Entity } from "./entity";
import { ShapeDefinition } from "./shape_definition"; import { ShapeDefinition } from "./shape_definition";
import { BaseItem } from "./base_item"; import { BaseItem } from "./base_item";
import { DynamicTickrate } from "./dynamic_tickrate"; import { DynamicTickrate } from "./dynamic_tickrate";
import { KeyActionMapper } from "./key_action_mapper"; import { KeyActionMapper } from "./key_action_mapper";
import { Vector } from "../core/vector"; import { Vector } from "../core/vector";
/* typehints:end */ import { GameMode } from "./game_mode";
/* typehints:end */
const logger = createLogger("game/root");
const logger = createLogger("game/root");
/** @type {Array<Layer>} */
export const layers = ["regular", "wires"]; /** @type {Array<Layer>} */
export const layers = ["regular", "wires"];
/**
* The game root is basically the whole game state at a given point, /**
* combining all important classes. We don't have globals, but this * The game root is basically the whole game state at a given point,
* class is passed to almost all game classes. * combining all important classes. We don't have globals, but this
*/ * class is passed to almost all game classes.
export class GameRoot { */
/** export class GameRoot {
* Constructs a new game root /**
* @param {Application} app * Constructs a new game root
*/ * @param {Application} app
constructor(app) { */
this.app = app; constructor(app) {
this.app = app;
/** @type {Savegame} */
this.savegame = null; /** @type {Savegame} */
this.savegame = null;
/** @type {InGameState} */
this.gameState = null; /** @type {InGameState} */
this.gameState = null;
/** @type {KeyActionMapper} */
this.keyMapper = null; /** @type {KeyActionMapper} */
this.keyMapper = null;
// Store game dimensions
this.gameWidth = 500; // Store game dimensions
this.gameHeight = 500; this.gameWidth = 500;
this.gameHeight = 500;
// Stores whether the current session is a fresh game (true), or was continued (false)
/** @type {boolean} */ // Stores whether the current session is a fresh game (true), or was continued (false)
this.gameIsFresh = true; /** @type {boolean} */
this.gameIsFresh = true;
// Stores whether the logic is already initialized
/** @type {boolean} */ // Stores whether the logic is already initialized
this.logicInitialized = false; /** @type {boolean} */
this.logicInitialized = false;
// Stores whether the game is already initialized, that is, all systems etc have been created
/** @type {boolean} */ // Stores whether the game is already initialized, that is, all systems etc have been created
this.gameInitialized = false; /** @type {boolean} */
this.gameInitialized = false;
/**
* Whether a bulk operation is running /**
*/ * Whether a bulk operation is running
this.bulkOperationRunning = false; */
this.bulkOperationRunning = false;
//////// Other properties ///////
//////// Other properties ///////
/** @type {Camera} */
this.camera = null; /** @type {Camera} */
this.camera = null;
/** @type {HTMLCanvasElement} */
this.canvas = null; /** @type {HTMLCanvasElement} */
this.canvas = null;
/** @type {CanvasRenderingContext2D} */
this.context = null; /** @type {CanvasRenderingContext2D} */
this.context = null;
/** @type {MapView} */
this.map = null; /** @type {MapView} */
this.map = null;
/** @type {GameLogic} */
this.logic = null; /** @type {GameLogic} */
this.logic = null;
/** @type {EntityManager} */
this.entityMgr = null; /** @type {EntityManager} */
this.entityMgr = null;
/** @type {GameHUD} */
this.hud = null; /** @type {GameHUD} */
this.hud = null;
/** @type {GameSystemManager} */
this.systemMgr = null; /** @type {GameSystemManager} */
this.systemMgr = null;
/** @type {GameTime} */
this.time = null; /** @type {GameTime} */
this.time = null;
/** @type {HubGoals} */
this.hubGoals = null; /** @type {HubGoals} */
this.hubGoals = null;
/** @type {BufferMaintainer} */
this.buffers = null; /** @type {BufferMaintainer} */
this.buffers = null;
/** @type {AutomaticSave} */
this.automaticSave = null; /** @type {AutomaticSave} */
this.automaticSave = null;
/** @type {SoundProxy} */
this.soundProxy = null; /** @type {SoundProxy} */
this.soundProxy = null;
/** @type {ShapeDefinitionManager} */
this.shapeDefinitionMgr = null; /** @type {ShapeDefinitionManager} */
this.shapeDefinitionMgr = null;
/** @type {ProductionAnalytics} */
this.productionAnalytics = null; /** @type {ProductionAnalytics} */
this.productionAnalytics = null;
/** @type {DynamicTickrate} */
this.dynamicTickrate = null; /** @type {DynamicTickrate} */
this.dynamicTickrate = null;
/** @type {Layer} */
this.currentLayer = "regular"; /** @type {Layer} */
this.currentLayer = "regular";
this.signals = {
// Entities /** @type {GameMode} */
entityManuallyPlaced: /** @type {TypedSignal<[Entity]>} */ (new Signal()), this.gameMode = null;
entityAdded: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityChanged: /** @type {TypedSignal<[Entity]>} */ (new Signal()), this.signals = {
entityGotNewComponent: /** @type {TypedSignal<[Entity]>} */ (new Signal()), // Entities
entityComponentRemoved: /** @type {TypedSignal<[Entity]>} */ (new Signal()), entityManuallyPlaced: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityQueuedForDestroy: /** @type {TypedSignal<[Entity]>} */ (new Signal()), entityAdded: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityDestroyed: /** @type {TypedSignal<[Entity]>} */ (new Signal()), entityChanged: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityGotNewComponent: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
// Global entityComponentRemoved: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
resized: /** @type {TypedSignal<[number, number]>} */ (new Signal()), entityQueuedForDestroy: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
readyToRender: /** @type {TypedSignal<[]>} */ (new Signal()), entityDestroyed: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
aboutToDestruct: /** @type {TypedSignal<[]>} */ new Signal(),
// Global
// Game Hooks resized: /** @type {TypedSignal<[number, number]>} */ (new Signal()),
gameSaved: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got saved readyToRender: /** @type {TypedSignal<[]>} */ (new Signal()),
gameRestored: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got restored aboutToDestruct: /** @type {TypedSignal<[]>} */ new Signal(),
gameFrameStarted: /** @type {TypedSignal<[]>} */ (new Signal()), // New frame // Game Hooks
gameSaved: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got saved
storyGoalCompleted: /** @type {TypedSignal<[number, string]>} */ (new Signal()), gameRestored: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got restored
upgradePurchased: /** @type {TypedSignal<[string]>} */ (new Signal()),
gameFrameStarted: /** @type {TypedSignal<[]>} */ (new Signal()), // New frame
// Called right after game is initialized
postLoadHook: /** @type {TypedSignal<[]>} */ (new Signal()), storyGoalCompleted: /** @type {TypedSignal<[number, string]>} */ (new Signal()),
upgradePurchased: /** @type {TypedSignal<[string]>} */ (new Signal()),
shapeDelivered: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()),
itemProduced: /** @type {TypedSignal<[BaseItem]>} */ (new Signal()), // Called right after game is initialized
postLoadHook: /** @type {TypedSignal<[]>} */ (new Signal()),
bulkOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()),
shapeDelivered: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()),
editModeChanged: /** @type {TypedSignal<[Layer]>} */ (new Signal()), itemProduced: /** @type {TypedSignal<[BaseItem]>} */ (new Signal()),
// Called to check if an entity can be placed, second parameter is an additional offset. bulkOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()),
// Use to introduce additional placement checks
prePlacementCheck: /** @type {TypedSignal<[Entity, Vector]>} */ (new Signal()), editModeChanged: /** @type {TypedSignal<[Layer]>} */ (new Signal()),
// Called before actually placing an entity, use to perform additional logic // Called to check if an entity can be placed, second parameter is an additional offset.
// for freeing space before actually placing. // Use to introduce additional placement checks
freeEntityAreaBeforeBuild: /** @type {TypedSignal<[Entity]>} */ (new Signal()), prePlacementCheck: /** @type {TypedSignal<[Entity, Vector]>} */ (new Signal()),
};
// Called before actually placing an entity, use to perform additional logic
// RNG's // for freeing space before actually placing.
/** @type {Object.<string, Object.<string, RandomNumberGenerator>>} */ freeEntityAreaBeforeBuild: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
this.rngs = {}; };
// Work queue // RNG's
this.queue = { /** @type {Object.<string, Object.<string, RandomNumberGenerator>>} */
requireRedraw: false, this.rngs = {};
};
} // Work queue
this.queue = {
/** requireRedraw: false,
* Destructs the game root };
*/ }
destruct() {
logger.log("destructing root"); /**
this.signals.aboutToDestruct.dispatch(); * Destructs the game root
*/
this.reset(); destruct() {
} logger.log("destructing root");
this.signals.aboutToDestruct.dispatch();
/**
* Resets the whole root and removes all properties this.reset();
*/ }
reset() {
if (this.signals) { /**
// Destruct all signals * Resets the whole root and removes all properties
for (let i = 0; i < this.signals.length; ++i) { */
this.signals[i].removeAll(); reset() {
} if (this.signals) {
} // Destruct all signals
for (let i = 0; i < this.signals.length; ++i) {
if (this.hud) { this.signals[i].removeAll();
this.hud.cleanup(); }
} }
if (this.camera) {
this.camera.cleanup(); if (this.hud) {
} this.hud.cleanup();
}
// Finally free all properties if (this.camera) {
for (let prop in this) { this.camera.cleanup();
if (this.hasOwnProperty(prop)) { }
delete this[prop];
} // Finally free all properties
} for (let prop in this) {
} if (this.hasOwnProperty(prop)) {
} delete this[prop];
}
}
}
}

View File

@ -12,7 +12,6 @@ import { GameSystemWithFilter } from "../game_system_with_filter";
import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item"; import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item";
import { COLOR_ITEM_SINGLETONS } from "../items/color_item"; import { COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { ShapeDefinition } from "../shape_definition"; import { ShapeDefinition } from "../shape_definition";
import { blueprintShape } from "../upgrades";
export class ConstantSignalSystem extends GameSystemWithFilter { export class ConstantSignalSystem extends GameSystemWithFilter {
constructor(root) { constructor(root) {
@ -61,7 +60,9 @@ export class ConstantSignalSystem extends GameSystemWithFilter {
this.root.shapeDefinitionMgr.getShapeItemFromDefinition( this.root.shapeDefinitionMgr.getShapeItemFromDefinition(
this.root.hubGoals.currentGoal.definition this.root.hubGoals.currentGoal.definition
), ),
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(blueprintShape), this.root.shapeDefinitionMgr.getShapeItemFromShortKey(
this.root.gameMode.getBlueprintShapeKey()
),
...this.root.hud.parts.pinnedShapes.pinnedShapes.map(key => ...this.root.hud.parts.pinnedShapes.pinnedShapes.map(key =>
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key) this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key)
), ),

View File

@ -1,7 +1,3 @@
import { IS_DEMO } from "../core/config";
import { ShapeDefinition } from "./shape_definition";
import { finalGameShape } from "./upgrades";
/** /**
* Don't forget to also update tutorial_goals_mappings.js as well as the translations! * Don't forget to also update tutorial_goals_mappings.js as well as the translations!
* @enum {string} * @enum {string}
@ -40,229 +36,3 @@ export const enumHubGoalRewards = {
no_reward: "no_reward", no_reward: "no_reward",
no_reward_freeplay: "no_reward_freeplay", no_reward_freeplay: "no_reward_freeplay",
}; };
export const tutorialGoals = [
// 1
// Circle
{
shape: "CuCuCuCu", // belts t1
required: 30,
reward: enumHubGoalRewards.reward_cutter_and_trash,
},
// 2
// Cutter
{
shape: "----CuCu", //
required: 40,
reward: enumHubGoalRewards.no_reward,
},
// 3
// Rectangle
{
shape: "RuRuRuRu", // miners t1
required: 70,
reward: enumHubGoalRewards.reward_balancer,
},
// 4
{
shape: "RuRu----", // processors t2
required: 70,
reward: enumHubGoalRewards.reward_rotater,
},
// 5
// Rotater
{
shape: "Cu----Cu", // belts t2
required: 170,
reward: enumHubGoalRewards.reward_tunnel,
},
// 6
{
shape: "Cu------", // miners t2
required: 270,
reward: enumHubGoalRewards.reward_painter,
},
// 7
// Painter
{
shape: "CrCrCrCr", // unused
required: 300,
reward: enumHubGoalRewards.reward_rotater_ccw,
},
// 8
{
shape: "RbRb----", // painter t2
required: 480,
reward: enumHubGoalRewards.reward_mixer,
},
// 9
// Mixing (purple)
{
shape: "CpCpCpCp", // belts t3
required: 600,
reward: enumHubGoalRewards.reward_merger,
},
// 10
// STACKER: Star shape + cyan
{
shape: "ScScScSc", // miners t3
required: 800,
reward: enumHubGoalRewards.reward_stacker,
},
// 11
// Chainable miner
{
shape: "CgScScCg", // processors t3
required: 1000,
reward: enumHubGoalRewards.reward_miner_chainable,
},
// 12
// Blueprints
{
shape: "CbCbCbRb:CwCwCwCw",
required: 1000,
reward: enumHubGoalRewards.reward_blueprints,
},
// 13
// Tunnel Tier 2
{
shape: "RpRpRpRp:CwCwCwCw", // painting t3
required: 3800,
reward: enumHubGoalRewards.reward_underground_belt_tier_2,
},
// DEMO STOPS HERE
...(IS_DEMO
? [
{
shape: "RpRpRpRp:CwCwCwCw",
required: 0,
reward: enumHubGoalRewards.reward_demo_end,
},
]
: [
// 14
// Belt reader
{
shape: "--Cg----:--Cr----", // unused
required: 16, // Per second!
reward: enumHubGoalRewards.reward_belt_reader,
throughputOnly: true,
},
// 15
// Storage
{
shape: "SrSrSrSr:CyCyCyCy", // unused
required: 10000,
reward: enumHubGoalRewards.reward_storage,
},
// 16
// Quad Cutter
{
shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", // belts t4 (two variants)
required: 6000,
reward: enumHubGoalRewards.reward_cutter_quad,
},
// 17
// Double painter
{
shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", // miner t4 (two variants)
required: 20000,
reward: enumHubGoalRewards.reward_painter_double,
},
// 18
// Rotater (180deg)
{
shape: "Sg----Sg:CgCgCgCg:--CyCy--", // unused
required: 20000,
reward: enumHubGoalRewards.reward_rotater_180,
},
// 19
// Compact splitter
{
shape: "CpRpCp--:SwSwSwSw",
required: 25000,
reward: enumHubGoalRewards.reward_splitter,
},
// 20
// WIRES
{
shape: finalGameShape,
required: 25000,
reward: enumHubGoalRewards.reward_wires_painter_and_levers,
},
// 21
// Filter
{
shape: "CrCwCrCw:CwCrCwCr:CrCwCrCw:CwCrCwCr",
required: 25000,
reward: enumHubGoalRewards.reward_filter,
},
// 22
// Constant signal
{
shape: "Cg----Cr:Cw----Cw:Sy------:Cy----Cy",
required: 25000,
reward: enumHubGoalRewards.reward_constant_signal,
},
// 23
// Display
{
shape: "CcSyCcSy:SyCcSyCc:CcSyCcSy",
required: 25000,
reward: enumHubGoalRewards.reward_display,
},
// 24 Logic gates
{
shape: "CcRcCcRc:RwCwRwCw:Sr--Sw--:CyCyCyCy",
required: 25000,
reward: enumHubGoalRewards.reward_logic_gates,
},
// 25 Virtual Processing
{
shape: "Rg--Rg--:CwRwCwRw:--Rg--Rg",
required: 25000,
reward: enumHubGoalRewards.reward_virtual_processing,
},
// 26 Freeplay
{
shape: "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw",
required: 50000,
reward: enumHubGoalRewards.reward_freeplay,
},
]),
];
if (G_IS_DEV) {
tutorialGoals.forEach(({ shape }) => {
try {
ShapeDefinition.fromShortKey(shape);
} catch (ex) {
throw new Error("Invalid tutorial goal: '" + ex + "' for shape" + shape);
}
});
}

View File

@ -1,212 +0,0 @@
import { IS_DEMO } from "../core/config";
import { findNiceIntegerValue } from "../core/utils";
import { ShapeDefinition } from "./shape_definition";
export const preparementShape = "CpRpCp--:SwSwSwSw";
export const finalGameShape = "RuCw--Cw:----Ru--";
export const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw";
export const blueprintShape = "CbCbCbRb:CwCwCwCw";
const fixedImprovements = [0.5, 0.5, 1, 1, 2, 1, 1];
const numEndgameUpgrades = !IS_DEMO ? 20 - fixedImprovements.length - 1 : 0;
function generateEndgameUpgrades() {
return new Array(numEndgameUpgrades).fill(null).map((_, i) => ({
required: [
{ shape: preparementShape, amount: 30000 + i * 10000 },
{ shape: finalGameShape, amount: 20000 + i * 5000 },
{ shape: rocketShape, amount: 20000 + i * 5000 },
],
excludePrevious: true,
}));
}
for (let i = 0; i < numEndgameUpgrades; ++i) {
fixedImprovements.push(0.1);
}
/** @typedef {{
* shape: string,
* amount: number
* }} UpgradeRequirement */
/** @typedef {{
* required: Array<UpgradeRequirement>
* improvement?: number,
* excludePrevious?: boolean
* }} TierRequirement */
/** @typedef {Array<TierRequirement>} UpgradeTiers */
/** @type {Object<string, UpgradeTiers>} */
export const UPGRADES = {
belt: [
{
required: [{ shape: "CuCuCuCu", amount: 60 }],
},
{
required: [{ shape: "--CuCu--", amount: 500 }],
},
{
required: [{ shape: "CpCpCpCp", amount: 1000 }],
},
{
required: [{ shape: "SrSrSrSr:CyCyCyCy", amount: 6000 }],
},
{
required: [{ shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", amount: 25000 }],
},
{
required: [{ shape: preparementShape, amount: 25000 }],
excludePrevious: true,
},
{
required: [
{ shape: preparementShape, amount: 25000 },
{ shape: finalGameShape, amount: 50000 },
],
excludePrevious: true,
},
...generateEndgameUpgrades(),
],
miner: [
{
required: [{ shape: "RuRuRuRu", amount: 300 }],
},
{
required: [{ shape: "Cu------", amount: 800 }],
},
{
required: [{ shape: "ScScScSc", amount: 3500 }],
},
{
required: [{ shape: "CwCwCwCw:WbWbWbWb", amount: 23000 }],
},
{
required: [{ shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", amount: 50000 }],
},
{
required: [{ shape: preparementShape, amount: 25000 }],
excludePrevious: true,
},
{
required: [
{ shape: preparementShape, amount: 25000 },
{ shape: finalGameShape, amount: 50000 },
],
excludePrevious: true,
},
...generateEndgameUpgrades(),
],
processors: [
{
required: [{ shape: "SuSuSuSu", amount: 500 }],
},
{
required: [{ shape: "RuRu----", amount: 600 }],
},
{
required: [{ shape: "CgScScCg", amount: 3500 }],
},
{
required: [{ shape: "CwCrCwCr:SgSgSgSg", amount: 25000 }],
},
{
required: [{ shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", amount: 50000 }],
},
{
required: [{ shape: preparementShape, amount: 25000 }],
excludePrevious: true,
},
{
required: [
{ shape: preparementShape, amount: 25000 },
{ shape: finalGameShape, amount: 50000 },
],
excludePrevious: true,
},
...generateEndgameUpgrades(),
],
painting: [
{
required: [{ shape: "RbRb----", amount: 600 }],
},
{
required: [{ shape: "WrWrWrWr", amount: 3800 }],
},
{
required: [{ shape: "RpRpRpRp:CwCwCwCw", amount: 6500 }],
},
{
required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp", amount: 25000 }],
},
{
required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp:CwCwCwCw", amount: 50000 }],
},
{
required: [{ shape: preparementShape, amount: 25000 }],
excludePrevious: true,
},
{
required: [
{ shape: preparementShape, amount: 25000 },
{ shape: finalGameShape, amount: 50000 },
],
excludePrevious: true,
},
...generateEndgameUpgrades(),
],
};
// Tiers need % of the previous tier as requirement too
const tierGrowth = 2.5;
// Automatically generate tier levels
for (const upgradeId in UPGRADES) {
const upgradeTiers = UPGRADES[upgradeId];
let currentTierRequirements = [];
for (let i = 0; i < upgradeTiers.length; ++i) {
const tierHandle = upgradeTiers[i];
tierHandle.improvement = fixedImprovements[i];
const originalRequired = tierHandle.required.slice();
for (let k = currentTierRequirements.length - 1; k >= 0; --k) {
const oldTierRequirement = currentTierRequirements[k];
if (!tierHandle.excludePrevious) {
tierHandle.required.unshift({
shape: oldTierRequirement.shape,
amount: oldTierRequirement.amount,
});
}
}
currentTierRequirements.push(
...originalRequired.map(req => ({
amount: req.amount,
shape: req.shape,
}))
);
currentTierRequirements.forEach(tier => {
tier.amount = findNiceIntegerValue(tier.amount * tierGrowth);
});
}
}
// VALIDATE
if (G_IS_DEV) {
for (const upgradeId in UPGRADES) {
UPGRADES[upgradeId].forEach(tier => {
tier.required.forEach(({ shape }) => {
try {
ShapeDefinition.fromShortKey(shape);
} catch (ex) {
throw new Error("Invalid upgrade goal: '" + ex + "' for shape" + shape);
}
});
});
}
}

View File

@ -1,14 +1,12 @@
import { globalConfig } from "../../core/config"; import { globalConfig } from "../../core/config";
import { createLogger } from "../../core/logging"; import { createLogger } from "../../core/logging";
import { queryParamOptions } from "../../core/query_parameters";
import { BeltComponent } from "../../game/components/belt";
import { StaticMapEntityComponent } from "../../game/components/static_map_entity";
import { GameRoot } from "../../game/root"; import { GameRoot } from "../../game/root";
import { InGameState } from "../../states/ingame"; import { InGameState } from "../../states/ingame";
import { GameAnalyticsInterface } from "../game_analytics"; import { GameAnalyticsInterface } from "../game_analytics";
import { FILE_NOT_FOUND } from "../storage"; import { FILE_NOT_FOUND } from "../storage";
import { blueprintShape, UPGRADES } from "../../game/upgrades";
import { tutorialGoals } from "../../game/tutorial_goals";
import { BeltComponent } from "../../game/components/belt";
import { StaticMapEntityComponent } from "../../game/components/static_map_entity";
import { queryParamOptions } from "../../core/query_parameters";
const logger = createLogger("game_analytics"); const logger = createLogger("game_analytics");
@ -190,23 +188,26 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface {
/** /**
* Returns true if the shape is interesting * Returns true if the shape is interesting
* @param {GameRoot} root
* @param {string} key * @param {string} key
*/ */
isInterestingShape(key) { isInterestingShape(root, key) {
if (key === blueprintShape) { if (key === root.gameMode.getBlueprintShapeKey()) {
return true; return true;
} }
// Check if its a story goal // Check if its a story goal
for (let i = 0; i < tutorialGoals.length; ++i) { const levels = root.gameMode.getLevelDefinitions();
if (key === tutorialGoals[i].shape) { for (let i = 0; i < levels.length; ++i) {
if (key === levels[i].shape) {
return true; return true;
} }
} }
// Check if its required to unlock an upgrade // Check if its required to unlock an upgrade
for (const upgradeKey in UPGRADES) { const upgrades = root.gameMode.getUpgrades();
const upgradeTiers = UPGRADES[upgradeKey]; for (const upgradeKey in upgrades) {
const upgradeTiers = upgrades[upgradeKey];
for (let i = 0; i < upgradeTiers.length; ++i) { for (let i = 0; i < upgradeTiers.length; ++i) {
const tier = upgradeTiers[i]; const tier = upgradeTiers[i];
const required = tier.required; const required = tier.required;
@ -226,7 +227,9 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface {
* @param {GameRoot} root * @param {GameRoot} root
*/ */
generateGameDump(root) { generateGameDump(root) {
const shapeIds = Object.keys(root.hubGoals.storedShapes).filter(this.isInterestingShape.bind(this)); const shapeIds = Object.keys(root.hubGoals.storedShapes).filter(key =>
this.isInterestingShape(root, key)
);
let shapes = {}; let shapes = {};
for (let i = 0; i < shapeIds.length; ++i) { for (let i = 0; i < shapeIds.length; ++i) {
shapes[shapeIds[i]] = root.hubGoals.storedShapes[shapeIds[i]]; shapes[shapeIds[i]] = root.hubGoals.storedShapes[shapeIds[i]];

View File

@ -130,7 +130,7 @@ export class SavegameSerializer {
errorReason = errorReason || root.time.deserialize(savegame.time); errorReason = errorReason || root.time.deserialize(savegame.time);
errorReason = errorReason || root.camera.deserialize(savegame.camera); errorReason = errorReason || root.camera.deserialize(savegame.camera);
errorReason = errorReason || root.map.deserialize(savegame.map); errorReason = errorReason || root.map.deserialize(savegame.map);
errorReason = errorReason || root.hubGoals.deserialize(savegame.hubGoals); errorReason = errorReason || root.hubGoals.deserialize(savegame.hubGoals, root);
errorReason = errorReason || root.hud.parts.pinnedShapes.deserialize(savegame.pinnedShapes); errorReason = errorReason || root.hud.parts.pinnedShapes.deserialize(savegame.pinnedShapes);
errorReason = errorReason || root.hud.parts.waypoints.deserialize(savegame.waypoints); errorReason = errorReason || root.hud.parts.waypoints.deserialize(savegame.waypoints);
errorReason = errorReason || this.internal.deserializeEntityArray(root, savegame.entities); errorReason = errorReason || this.internal.deserializeEntityArray(root, savegame.entities);

View File

@ -19,7 +19,6 @@ import { getCodeFromBuildingData } from "../../game/building_codes.js";
import { StaticMapEntityComponent } from "../../game/components/static_map_entity.js"; import { StaticMapEntityComponent } from "../../game/components/static_map_entity.js";
import { Entity } from "../../game/entity.js"; import { Entity } from "../../game/entity.js";
import { defaultBuildingVariant, MetaBuilding } from "../../game/meta_building.js"; import { defaultBuildingVariant, MetaBuilding } from "../../game/meta_building.js";
import { finalGameShape } from "../../game/upgrades.js";
import { SavegameInterface_V1005 } from "./1005.js"; import { SavegameInterface_V1005 } from "./1005.js";
const schema = require("./1006.json"); const schema = require("./1006.json");
@ -152,7 +151,8 @@ export class SavegameInterface_V1006 extends SavegameInterface_V1005 {
stored[shapeKey] = rebalance(stored[shapeKey]); stored[shapeKey] = rebalance(stored[shapeKey]);
} }
stored[finalGameShape] = 0; // Reset final game shape
stored["RuCw--Cw:----Ru--"] = 0;
// Reduce goals // Reduce goals
if (dump.hubGoals.currentGoal) { if (dump.hubGoals.currentGoal) {