diff --git a/src/js/GeoZ/main.js b/src/js/GeoZ/main.js index 4f9804c1..31837af6 100644 --- a/src/js/GeoZ/main.js +++ b/src/js/GeoZ/main.js @@ -12,6 +12,7 @@ import { registerBuildingVariant } from "../game/building_codes"; import { supportedBuildings } from "../game/hud/parts/buildings_toolbar"; import { KEYMAPPINGS, key } from "../game/key_action_mapper"; import { T } from "../translations"; +import { ShapeData, allShapeData, initShapes } from "../game/shapes"; import { globalConfig } from "../core/config"; export { MetaModBuilding } from "./mod_building"; @@ -28,6 +29,7 @@ export { ModSystem, ModSystemWithFilter } from "./mod_system"; * @property {Array=} items * @property {Array=} processors * @property {Array=} systems + * @property {Array=} shapes */ const logger = createLogger("GeoZ"); @@ -50,6 +52,9 @@ export const ModItems = []; /** @type {Array} */ export const ModBuildings = []; +/** @type {Array} */ +export const ModShapes = []; + const GameSystemManager_internalInitSystems_original = GameSystemManager.prototype.internalInitSystems; GameSystemManager.prototype.internalInitSystems = function () { GameSystemManager_internalInitSystems_original.call(this); @@ -224,6 +229,14 @@ export async function initMods() { T.buildings[base_id][variant] = translations.variants[variant]; } } + + if(mod.shapes) { + mod_infos += `${mod.shapes.length} shapes, `; + for (const shape of mod.shapes) { + ModShapes.push(shape); + allShapeData[shape.id] = shape; + } + } } logger.log(mod_infos); diff --git a/src/js/game/hub_goals.js b/src/js/game/hub_goals.js index 71817ebd..b342ebeb 100644 --- a/src/js/game/hub_goals.js +++ b/src/js/game/hub_goals.js @@ -2,9 +2,10 @@ import { globalConfig } from "../core/config"; import { clamp, findNiceIntegerValue, randomChoice, randomInt } from "../core/utils"; import { BasicSerializableObject, types } from "../savegame/serialization"; import { enumColors } from "./colors"; +import { allShapeData } from "./shapes"; import { enumItemProcessorTypes } from "./components/item_processor"; import { GameRoot } from "./root"; -import { enumSubShape, ShapeDefinition } from "./shape_definition"; +import { ShapeDefinition, ShapeLayer } from "./shape_definition"; import { enumHubGoalRewards, tutorialGoals } from "./tutorial_goals"; import { UPGRADES } from "./upgrades"; @@ -324,16 +325,16 @@ export class HubGoals extends BasicSerializableObject { */ createRandomShape() { const layerCount = clamp(this.level / 25, 2, 4); - /** @type {Array} */ + /** @type {Array} */ let layers = []; const randomColor = () => randomChoice(Object.values(enumColors)); - const randomShape = () => randomChoice(Object.values(enumSubShape)); + const randomShape = () => randomChoice(Object.values(allShapeData).map(d => d.id)); let anyIsMissingTwo = false; for (let i = 0; i < layerCount; ++i) { - /** @type {import("./shape_definition").ShapeLayer} */ + /** @type {ShapeLayer} */ const layer = [null, null, null, null]; for (let quad = 0; quad < 4; ++quad) { diff --git a/src/js/game/map_chunk.js b/src/js/game/map_chunk.js index 54af1125..773499e8 100644 --- a/src/js/game/map_chunk.js +++ b/src/js/game/map_chunk.js @@ -5,10 +5,11 @@ import { clamp, fastArrayDeleteValueIfContained, make2DUndefinedArray } from ".. import { Vector } from "../core/vector"; import { BaseItem } from "./base_item"; import { enumColors } from "./colors"; +import { allShapeData } from "./shapes"; import { Entity } from "./entity"; import { COLOR_ITEM_SINGLETONS } from "./items/color_item"; import { GameRoot } from "./root"; -import { enumSubShape } from "./shape_definition"; +import { enumSubShape } from "./shapes"; import { Rectangle } from "../core/rectangle"; const logger = createLogger("map_chunk"); @@ -180,56 +181,58 @@ export class MapChunk { */ internalGenerateShapePatch(rng, shapePatchSize, distanceToOriginInChunks) { /** @type {[enumSubShape, enumSubShape, enumSubShape, enumSubShape]} */ - let subShapes = null; + let quads = null; let weights = {}; - - // Later there is a mix of everything - weights = { - [enumSubShape.rect]: 100, - [enumSubShape.circle]: Math.round(50 + clamp(distanceToOriginInChunks * 2, 0, 50)), - [enumSubShape.star]: Math.round(20 + clamp(distanceToOriginInChunks, 0, 30)), - [enumSubShape.windmill]: Math.round(6 + clamp(distanceToOriginInChunks / 2, 0, 20)), - }; - - if (distanceToOriginInChunks < 7) { - // Initial chunks can not spawn the good stuff - weights[enumSubShape.star] = 0; - weights[enumSubShape.windmill] = 0; - } - - if (distanceToOriginInChunks < 10) { - // Initial chunk patches always have the same shape - const subShape = this.internalGenerateRandomSubShape(rng, weights); - subShapes = [subShape, subShape, subShape, subShape]; - } else if (distanceToOriginInChunks < 15) { - // Later patches can also have mixed ones - const subShapeA = this.internalGenerateRandomSubShape(rng, weights); - const subShapeB = this.internalGenerateRandomSubShape(rng, weights); - subShapes = [subShapeA, subShapeA, subShapeB, subShapeB]; - } else { - // Finally there is a mix of everything - subShapes = [ - this.internalGenerateRandomSubShape(rng, weights), - this.internalGenerateRandomSubShape(rng, weights), - this.internalGenerateRandomSubShape(rng, weights), - this.internalGenerateRandomSubShape(rng, weights), - ]; - } - - // Makes sure windmills never spawn as whole - let windmillCount = 0; - for (let i = 0; i < subShapes.length; ++i) { - if (subShapes[i] === enumSubShape.windmill) { - ++windmillCount; + for (let s in allShapeData) { + const data = allShapeData[s]; + if (!data.spawnData || distanceToOriginInChunks < data.spawnData.minDistance) { + continue; + } + const chances = data.spawnData.chances; + const chance = Math.round( + clamp( + chances.min + (distanceToOriginInChunks - data.spawnData.minDistance) * chances.distanceMultiplier, + 0, + chances.max + ) + ); + if (chance) { + weights[data.id] = chance; } } - if (windmillCount > 1) { - subShapes[0] = enumSubShape.rect; - subShapes[1] = enumSubShape.rect; + quads = [ + this.internalGenerateRandomSubShape(rng, weights), + this.internalGenerateRandomSubShape(rng, weights), + this.internalGenerateRandomSubShape(rng, weights), + this.internalGenerateRandomSubShape(rng, weights), + ]; + if (distanceToOriginInChunks < 10) { + // Initial chunk patches always have the same shape + quads = [quads[0], quads[0], quads[0], quads[0]]; + } else if (distanceToOriginInChunks < 15) { + // Later patches can also have mixed ones + quads = [quads[0], quads[0], quads[1], quads[1]]; + } else { + // if (quads[0] == quads[2] && quads[0] != quads[3] && quads[0] != quads[1]) { + // quads = [quads[0], quads[2], quads[1], quads[3]]; + // } + // if (quads[1] == quads[3] && quads[1] != quads[0] && quads[1] != quads[2]) { + // quads = [quads[0], quads[2], quads[1], quads[3]]; + // } } - const definition = this.root.shapeDefinitionMgr.getDefinitionFromSimpleShapes(subShapes); + if ( + quads.filter(q => q == quads[0]).length > allShapeData[quads[0]].spawnData.maxQuarters || + quads.filter(q => q == quads[1]).length > allShapeData[quads[1]].spawnData.maxQuarters || + quads.filter(q => q == quads[2]).length > allShapeData[quads[2]].spawnData.maxQuarters + ) { + return this.internalGenerateShapePatch(rng, shapePatchSize, distanceToOriginInChunks); + } + + let colors = /** @type {[string, string, string, string]} */ (quads.map(q => allShapeData[q].spawnData.color)); + + const definition = this.root.shapeDefinitionMgr.getDefinitionFromSimpleShapesAndColors(quads, colors); this.internalGeneratePatch( rng, shapePatchSize, diff --git a/src/js/game/shape_definition.js b/src/js/game/shape_definition.js index 65b72a1a..924dbed0 100644 --- a/src/js/game/shape_definition.js +++ b/src/js/game/shape_definition.js @@ -6,6 +6,7 @@ import { Vector } from "../core/vector"; import { BasicSerializableObject, types } from "../savegame/serialization"; import { enumColors, enumColorsToHexCode, enumColorToShortcode, enumShortcodeToColor } from "./colors"; import { THEME } from "./theme"; +import { allShapeData, ShapeData, enumShortcodeToSubShape, enumSubShapeToShortcode, enumSubShape } from "./shapes"; /** * @typedef {{ @@ -26,28 +27,6 @@ const arrayQuadrantIndexToOffset = [ new Vector(-1, -1), // tl ]; -/** @enum {string} */ -export const enumSubShape = { - rect: "rect", - circle: "circle", - star: "star", - windmill: "windmill", -}; - -/** @enum {string} */ -export const enumSubShapeToShortcode = { - [enumSubShape.rect]: "R", - [enumSubShape.circle]: "C", - [enumSubShape.star]: "S", - [enumSubShape.windmill]: "W", -}; - -/** @enum {enumSubShape} */ -export const enumShortcodeToSubShape = {}; -for (const key in enumSubShapeToShortcode) { - enumShortcodeToSubShape[enumSubShapeToShortcode[key]] = key; -} - /** * Converts the given parameters to a valid shape definition * @param {*} layers @@ -85,6 +64,7 @@ export class ShapeDefinition extends BasicSerializableObject { return errorCode; } const definition = ShapeDefinition.fromShortKey(data); + /** @type {Array} */ this.layers = /** @type {Array} */ (definition.layers); } @@ -336,97 +316,98 @@ export class ShapeDefinition extends BasicSerializableObject { for (let layerIndex = 0; layerIndex < this.layers.length; ++layerIndex) { const quadrants = this.layers[layerIndex]; + let quads = quadrants + .map((e, i) => ({ e, i })) + .filter(e => e.e) + .map(e => ({ ...e.e, quadrantIndex: e.i })) const layerScale = Math.max(0.1, 0.9 - layerIndex * 0.22); - for (let quadrantIndex = 0; quadrantIndex < 4; ++quadrantIndex) { - if (!quadrants[quadrantIndex]) { + for (let quad of quads) { + if (!quad) { + continue; + } + const { subShape, color, quadrantIndex } = quad; + if (subShape == "-") { continue; } - const { subShape, color } = quadrants[quadrantIndex]; const quadrantPos = arrayQuadrantIndexToOffset[quadrantIndex]; + const centerQuadrantX = quadrantPos.x * quadrantHalfSize; const centerQuadrantY = quadrantPos.y * quadrantHalfSize; const rotation = Math.radians(quadrantIndex * 90); + context.save(); context.translate(centerQuadrantX, centerQuadrantY); context.rotate(rotation); context.fillStyle = enumColorsToHexCode[color]; context.strokeStyle = THEME.items.outline; - context.lineWidth = THEME.items.outlineWidth; + const lineWidth = THEME.items.outlineWidth * Math.pow(0.8, layerIndex); + context.lineWidth = lineWidth; const insetPadding = 0.0; - switch (subShape) { - case enumSubShape.rect: { - context.beginPath(); - const dims = quadrantSize * layerScale; - context.rect( - insetPadding + -quadrantHalfSize, - -insetPadding + quadrantHalfSize - dims, - dims, - dims - ); - - break; + const dims = quadrantSize * layerScale; + const innerDims = insetPadding - quadrantHalfSize; + + let began = null; + // eslint-disable-next-line no-inner-declarations + /** @type {import("./shapes").BeginDrawShape} */ + function begin(args) { + context.save(); + context.translate(innerDims, -innerDims); + context.scale(dims, -dims); + context.lineWidth = lineWidth / dims / (args.scale || 1); + if (args.scale) { + context.scale(args.scale, args.scale); } - case enumSubShape.star: { + if (args.beginPath) { context.beginPath(); - const dims = quadrantSize * layerScale; - - let originX = insetPadding - quadrantHalfSize; - let originY = -insetPadding + quadrantHalfSize - dims; - - const moveInwards = dims * 0.4; - context.moveTo(originX, originY + moveInwards); - context.lineTo(originX + dims, originY); - context.lineTo(originX + dims - moveInwards, originY + dims); - context.lineTo(originX, originY + dims); + } + if (args.moveToZero) { + context.moveTo(0, 0); + } + began = args; + } + // eslint-disable-next-line no-inner-declarations + function end() { + if (!began) { + return; + } + if (began.path) { context.closePath(); - break; - } - - case enumSubShape.windmill: { - context.beginPath(); - const dims = quadrantSize * layerScale; - - let originX = insetPadding - quadrantHalfSize; - let originY = -insetPadding + quadrantHalfSize - dims; - const moveInwards = dims * 0.4; - context.moveTo(originX, originY + moveInwards); - context.lineTo(originX + dims, originY); - context.lineTo(originX + dims, originY + dims); - context.lineTo(originX, originY + dims); - context.closePath(); - break; - } - - case enumSubShape.circle: { - context.beginPath(); - context.moveTo(insetPadding + -quadrantHalfSize, -insetPadding + quadrantHalfSize); - context.arc( - insetPadding + -quadrantHalfSize, - -insetPadding + quadrantHalfSize, - quadrantSize * layerScale, - -Math.PI * 0.5, - 0 - ); - context.closePath(); - break; - } - - default: { - assertAlways(false, "Unkown sub shape: " + subShape); } + context.restore(); } - context.fill(); - context.stroke(); + /** @type {ShapeData} */ + let shape = allShapeData[subShape]; + assertAlways(shape.draw, "shape should be drawable!"); + if (typeof shape.draw === "string") { + let draw = shape.draw; + begin({ scale: 1 }); + let p = new Path2D(draw); + context.fill(p); + context.stroke(p); + end(); + } else { + shape.draw({ + dims, + innerDims, + layer: layerIndex, + quadrant: quadrantIndex, + context, + color, + begin, + }); + end(); + context.fill(); + context.stroke(); + } - context.rotate(-rotation); - context.translate(-centerQuadrantX, -centerQuadrantY); + context.restore(); } } } diff --git a/src/js/game/shape_definition_manager.js b/src/js/game/shape_definition_manager.js index ef0d592f..7e80bb6e 100644 --- a/src/js/game/shape_definition_manager.js +++ b/src/js/game/shape_definition_manager.js @@ -3,7 +3,8 @@ import { BasicSerializableObject } from "../savegame/serialization"; import { enumColors } from "./colors"; import { ShapeItem } from "./items/shape_item"; import { GameRoot } from "./root"; -import { enumSubShape, ShapeDefinition } from "./shape_definition"; +import { ShapeDefinition } from "./shape_definition"; +import { enumSubShape } from "./shapes"; const logger = createLogger("shape_definition_manager"); @@ -256,4 +257,18 @@ export class ShapeDefinitionManager extends BasicSerializableObject { return this.registerOrReturnHandle(new ShapeDefinition({ layers: [shapeLayer] })); } + + /** + * + * @param {[enumSubShape, enumSubShape, enumSubShape, enumSubShape]} subShapes + * @param {[string, string, string, string]} colors + * @returns {ShapeDefinition} + */ + getDefinitionFromSimpleShapesAndColors(subShapes, colors) { + const shapeLayer = /** @type {import("./shape_definition").ShapeLayer} */ (subShapes.map( + (subShape, i) => ({ subShape, color: colors[i] }) + )); + + return this.registerOrReturnHandle(new ShapeDefinition({ layers: [shapeLayer] })); + } } diff --git a/src/js/game/shapes.js b/src/js/game/shapes.js new file mode 100644 index 00000000..1e3b66bb --- /dev/null +++ b/src/js/game/shapes.js @@ -0,0 +1,181 @@ +/** @enum {string} */ +export const enumSubShape = { + rect: "rect", + circle: "circle", + star: "star", + windmill: "windmill", +}; + +/** @enum {string} */ +export const enumSubShapeToShortcode = { + [enumSubShape.rect]: "R", + [enumSubShape.circle]: "C", + [enumSubShape.star]: "S", + [enumSubShape.windmill]: "W", +}; + +/** @enum {enumSubShape} */ +export const enumShortcodeToSubShape = {}; +for (const key in enumSubShapeToShortcode) { + enumShortcodeToSubShape[enumSubShapeToShortcode[key]] = key; +} + +/** + * @callback BeginDrawShape + * @param {{ + * scale?: number, + * beginPath?: boolean, + * moveToZero?: true + * }} args + */ + +/** + * @typedef {Object} DrawShapeParams + * @property {number} dims + * @property {number} innerDims + * @property {number} layer + * @property {number} quadrant + * @property {CanvasRenderingContext2D} context + * @property {string} color + * @property {BeginDrawShape} begin + */ + +/** + * @callback DrawShape + * @param {DrawShapeParams} args + */ + +/** + * @typedef {Object} SpawnChanceData + * @property {number} [min=0] + * @property {number} [max=100] + * @property {number} [distanceMultiplier=1] + */ + +/** + * @typedef {Object} ShapeSpawnData + * @property {string} [color="uncolored"] + * @property {number} [minDistance=0] + * @property {number} [maxQuarters=4] + * @property {SpawnChanceData} [chances] + */ + +/** + * @typedef {Object} ShapeData + * @property {string} id + * @property {string} code + * @property {DrawShape | string} draw + * @property {number} tier + * @property {ShapeSpawnData} [spawnData] + */ + +/** @type {Object} */ +export const allShapeData = { + rect: { + id: "rect", + code: "R", + draw: "M 0 0 v 1 h 1 v -1 z", + tier: 0, + spawnData: { + color: "uncolored", + maxQuarters: 4, + minDistance: 0, + chances: { + min: 100, + distanceMultiplier: 0, + max: 100, + }, + }, + }, + circle: { + id: "circle", + code: "C", + draw: "M 0 0 l 1 0 a 1 1 0 0 1 -1 1 z ", + tier: 0, + spawnData: { + color: "uncolored", + maxQuarters: 4, + minDistance: 0, + chances: { + min: 50, + distanceMultiplier: 15, + max: 100, + }, + }, + }, + star: { + id: "star", + code: "S", + draw: "M 0 0 L 0 0.6 1 1 0.6 0 z", + tier: 0.5, + spawnData: { + color: "uncolored", + maxQuarters: 4, + minDistance: 5, + chances: { + min: 20, + distanceMultiplier: 10, + max: 100, + }, + }, + }, + windmill: { + id: "windmill", + code: "W", + draw: "M 0 0 L 0 0.6 1 1 1 0 z", + tier: 1, + spawnData: { + color: "uncolored", + maxQuarters: 3, + minDistance: 7, + chances: { + min: 20, + distanceMultiplier: 5, + max: 100, + }, + }, + }, +}; + +export function initShapes() { + for (let k in enumSubShape) { + delete enumSubShape[k]; + } + for (let k in enumSubShapeToShortcode) { + delete enumSubShapeToShortcode[k]; + } + for (let k in enumShortcodeToSubShape) { + delete enumShortcodeToSubShape[k]; + } + + for (let s in allShapeData) { + let data = allShapeData[s]; + assert(data.id == s); + assert(data.code.toUpperCase() == data.code); + assert(data.draw); + + enumSubShape[data.id] = data.id; + enumSubShapeToShortcode[data.id] = data.code; + enumShortcodeToSubShape[data.code] = data.id; + + if (data.spawnData) { + const sdata = data.spawnData; + sdata.color = sdata.color || "uncolored"; + sdata.maxQuarters = sdata.maxQuarters || 4; + sdata.minDistance = sdata.minDistance || 0; + + if(sdata.chances) { + const chances = sdata.chances; + chances.min = chances.min || 0; + chances.max = chances.max || 100; + chances.distanceMultiplier = chances.distanceMultiplier || 1; + } else { + sdata.chances = { + min: 0, + max: 100, + distanceMultiplier: 1 + }; + } + } + } +}