From e39b72ef6df1bed8b0097c7285db4009138f7cd7 Mon Sep 17 00:00:00 2001 From: Dimava Date: Thu, 11 Jun 2020 13:44:06 +0300 Subject: [PATCH] rewrite shape model --- src/js/game/custom/shapes.js | 48 +++++++ src/js/game/map_chunk.js | 94 +++++++------- src/js/game/shape_definition.js | 120 ++++++----------- src/js/game/shape_definition_manager.js | 8 ++ src/js/game/shapes.js | 164 ++++++++++++++++++++++++ 5 files changed, 310 insertions(+), 124 deletions(-) create mode 100644 src/js/game/custom/shapes.js create mode 100644 src/js/game/shapes.js diff --git a/src/js/game/custom/shapes.js b/src/js/game/custom/shapes.js new file mode 100644 index 00000000..ecf3d8b8 --- /dev/null +++ b/src/js/game/custom/shapes.js @@ -0,0 +1,48 @@ +/** @enum {string} */ +export const customShapes = []; + +/** + * @callback DrawShape + * @param {Object} args + */ + +/** + * @typedef {Object} ShapeData + * @property {string} id + * @property {string} code + * @property {boolean} [spawnable] + * @property {string} [spawnColor] + * @property {number} [maxQuarters] + * @property {number} [minDistance] + * @property {number} [minChance] + * @property {number} [distChance] + * @property {number} [maxChance] + * @property {DrawShape} draw + */ + +/** + * @param {ShapeData} shapeData + */ +export function registerCustomShape(shapeData) { + customShapes.push(shapeData); +} + +// registerCustomShape({ +// id: "clover", +// code: "L", +// spawnable: true, +// spawnColor: "green", +// maxQuarters: 4, +// minDistance: 6, +// minChance: 4, +// distChance: 1/3, +// maxChance: 12, +// draw({ dims, innerDims, layer, quad, context, color, begin }) { +// begin({ size: 1.3, path: true, zero: true }); +// const inner = 0.5; +// const inner_center = 0.45; +// context.lineTo(0, inner); +// context.bezierCurveTo(0, 1, inner, 1, inner_center, inner_center); +// context.bezierCurveTo(1, inner, 1, 0, inner, 0); +// }, +// }); diff --git a/src/js/game/map_chunk.js b/src/js/game/map_chunk.js index 5ebdd975..ba4c39ef 100644 --- a/src/js/game/map_chunk.js +++ b/src/js/game/map_chunk.js @@ -9,6 +9,7 @@ import { clamp, fastArrayDeleteValueIfContained, make2DUndefinedArray } from ".. import { Vector } from "../core/vector"; import { BaseItem } from "./base_item"; import { enumColors, allColorData } from "./colors"; +import { allShapeData } from "./shapes"; import { Entity } from "./entity"; import { ColorItem } from "./items/color_item"; import { ShapeItem } from "./items/shape_item"; @@ -158,56 +159,61 @@ 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.disabled || + !data.spawnable || + distanceToOriginInChunks < data.minDistance + ) { + continue; + } + const chance = Math_round( + clamp( + data.minChance + (distanceToOriginInChunks - data.minDistance) * data.distChance, + 0, + data.maxChance + ) + ); + 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]].maxQuarters || + quads.filter(q => q == quads[1]).length > allShapeData[quads[1]].maxQuarters || + quads.filter(q => q == quads[2]).length > allShapeData[quads[2]].maxQuarters + ) { + return this.internalGenerateShapePatch(rng, shapePatchSize, distanceToOriginInChunks); + } + + let colors = quads.map(q => allShapeData[q].spawnColor); + + const definition = this.root.shapeDefinitionMgr.getDefinitionFromSimpleShapesAndColors(quads, colors); this.internalGeneratePatch(rng, shapePatchSize, new ShapeItem(definition)); } diff --git a/src/js/game/shape_definition.js b/src/js/game/shape_definition.js index 64cc3eab..c5281c76 100644 --- a/src/js/game/shape_definition.js +++ b/src/js/game/shape_definition.js @@ -1,3 +1,5 @@ +import { enumSubShape, enumSubShapeToShortcode, enumShortcodeToSubShape, allShapeData } from "./shapes.js"; +export { enumSubShape, enumSubShapeToShortcode, enumShortcodeToSubShape } from "./shapes.js"; import { makeOffscreenBuffer } from "../core/buffer_utils"; import { JSON_parse, JSON_stringify, Math_max, Math_PI, Math_radians } from "../core/builtins"; import { globalConfig } from "../core/config"; @@ -32,28 +34,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 @@ -271,83 +251,63 @@ export class ShapeDefinition extends BasicSerializableObject { 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; + context.lineWidth = THEME.items.outlineWidth * Math.pow(0.8, layerIndex); const insetPadding = 0.0; - switch (subShape) { - case enumSubShape.rect: { - context.beginPath(); - const dims = quadrantSize * layerScale; - context.rect( - insetPadding + -quadrantHalfSize, - -insetPadding + quadrantHalfSize - dims, - dims, - dims - ); + const dims = quadrantSize * layerScale; + const innerDims = insetPadding - quadrantHalfSize; + let began = null; - break; + function begin(args) { + context.save(); + context.translate(innerDims, -innerDims); + context.scale(dims, -dims); + if (args.size) { + context.scale(args.size, args.size); } - case enumSubShape.star: { + if (args.path) { 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.zero) { + context.moveTo(0, 0); + } + began = args; + } + function end() { + if (!began) { + return; + } + if (began.path) { context.closePath(); - break; } + context.restore(); + } - 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); - } + let shape = allShapeData[subShape]; + if (shape.draw) { + shape.draw({ + dims, + innerDims, + layer: layerIndex, + quad: 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 ad682bf0..a169021e 100644 --- a/src/js/game/shape_definition_manager.js +++ b/src/js/game/shape_definition_manager.js @@ -205,4 +205,12 @@ export class ShapeDefinitionManager extends BasicSerializableObject { return this.registerOrReturnHandle(new ShapeDefinition({ layers: [shapeLayer] })); } + + 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..238a42d0 --- /dev/null +++ b/src/js/game/shapes.js @@ -0,0 +1,164 @@ +import { enumColors } from "./colors"; +import { customShapes } from "./custom/shapes"; + +/** @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 DrawShape + * @param {Object} args + */ + +/** + * @typedef {Object} ShapeData + * @property {string} id + * @property {string} code + * @property {boolean} [spawnable] + * @property {string} [spawnColor] + * @property {number} [maxQuarters] + * @property {number} [minDistance] + * @property {number} [minChance] + * @property {number} [distChance] + * @property {number} [maxChance] + * @property {DrawShape} draw + */ + +/** @enum {ShapeData} */ +export const allShapeData = { + rect: { + id: "rect", + code: "R", + spawnable: true, + spawnColor: "uncolored", + maxQuarters: 4, + minDistance: 0, + minChance: 100, + distChance: 0, + maxChance: 100, + draw: drawRect, + }, + circle: { + id: "circle", + code: "C", + spawnable: true, + spawnColor: "uncolored", + maxQuarters: 4, + minDistance: 0, + minChance: 50, + distChance: 2, + maxChance: 100, + draw: drawCircle, + }, + star: { + id: "star", + code: "S", + spawnable: true, + spawnColor: "uncolored", + maxQuarters: 4, + minDistance: 7, + minChance: 20 + 7, + distChance: 1, + maxChance: 50, + draw: drawStar, + }, + windmill: { + id: "windmill", + code: "W", + spawnable: true, + spawnColor: "uncolored", + maxQuarters: 2, + minDistance: 7, + minChance: 6 + 7 / 2, + distChance: 1 / 2, + maxChance: 26, + draw: drawWindmill, + }, +}; + +for (let data of customShapes) { + allShapeData[data.id] = data; +} + +initShapes(); + +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); + enumSubShape[data.id] = data.id; + enumSubShapeToShortcode[data.id] = data.code; + enumShortcodeToSubShape[data.code] = data.id; + if (data.spawnable) { + data.spawnColor = data.spawnColor || "uncolored"; + assert(enumColors[data.spawnColor], "should have known initial color"); + data.maxQuarters = data.maxQuarters || 4; + data.minDistance = data.minDistance || 0; + assert(data.minChance > 0 || data.distChance > 0, "should have chance to spawn"); + data.minChance = data.minChance || 0; + data.distChance = data.distChance || 0; + data.maxChance = data.maxChance || 999999; + } + } +} + +/** @type {DrawShape} */ +function drawRect({ dims, innerDims, layer, quad, context, color, begin }) { + begin({ size: 1, path: true, zero: true }); + context.lineTo(0, 1); + context.lineTo(1, 1); + context.lineTo(1, 0); +} + +/** @type {DrawShape} */ +function drawCircle({ dims, innerDims, layer, quad, context, color, begin }) { + begin({ size: 1, path: true, zero: true }); + context.arc(0, 0, 1, 0, 0.5 * Math.PI); +} + +/** @type {DrawShape} */ +function drawStar({ dims, innerDims, layer, quad, context, color, begin }) { + begin({ size: 1, path: true, zero: true }); + const inner = 0.6; + context.lineTo(0, inner); + context.lineTo(1, 1); + context.lineTo(inner, 0); +} + +/** @type {DrawShape} */ +function drawWindmill({ dims, innerDims, layer, quad, context, color, begin }) { + begin({ size: 1, path: true, zero: true }); + const inner = 0.6; + context.lineTo(0, inner); + context.lineTo(1, 1); + context.lineTo(1, 0); +}