diff --git a/src/data/planes.tsx b/src/data/planes.tsx new file mode 100644 index 0000000..bb8814a --- /dev/null +++ b/src/data/planes.tsx @@ -0,0 +1,41 @@ +import { jsx } from "features/feature"; +import MainDisplayVue from "features/resources/MainDisplay.vue"; +import { createResource } from "features/resources/resource"; +import { BaseLayer, createLayer } from "game/layers"; +import { persistent } from "game/persistence"; +import { DecimalSource } from "util/bignum"; +import { Resources } from "./projEntry"; +import { getColor, getName, sfc32 } from "./utils"; + +export function createPlane(id: string, tier: Resources, seed: number) { + return createLayer(id, function (this: BaseLayer) { + const random = sfc32(0, seed >> 0, seed >> 32, 1); + for (let i = 0; i < 12; i++) random(); + + const name = getName(random); + const color = getColor([0.64, 0.75, 0.55], random); + const background = getColor([0.18, 0.2, 0.25], random); + const resource = createResource(0, getName(random)); + + return { + tier: persistent(tier), + seed: persistent(seed), + name, + color, + resource, + background, + style: { + background, + "--background": background + }, + display: jsx(() => ( + <> +

{name}

+ + + )) + }; + }); +} + +export type GenericPlane = ReturnType; diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx index e3a3a43..357a23e 100644 --- a/src/data/projEntry.tsx +++ b/src/data/projEntry.tsx @@ -14,16 +14,16 @@ import { jsx } from "features/feature"; import { createResource } from "features/resources/resource"; import { createTabFamily } from "features/tabs/tabFamily"; import Formula, { calculateCost } from "game/formulas/formulas"; -import type { BaseLayer, GenericLayer } from "game/layers"; -import { createLayer } from "game/layers"; +import { GenericFormula, InvertibleIntegralFormula } from "game/formulas/types"; +import { BaseLayer, GenericLayer, addLayer, createLayer, layers } from "game/layers"; import { Modifier, createAdditiveModifier, createMultiplicativeModifier, createSequentialModifier } from "game/modifiers"; -import { State, persistent } from "game/persistence"; -import type { Player } from "game/player"; +import { State } from "game/persistence"; +import type { LayerData, Player } from "game/player"; import player from "game/player"; import settings from "game/settings"; import Decimal, { DecimalSource } from "lib/break_eternity"; @@ -34,7 +34,7 @@ import { ComputedRef, computed, nextTick, reactive, ref, watch } from "vue"; import { useToast } from "vue-toastification"; import { Section, createCollapsibleModifierSections, createFormulaPreview } from "./common"; import "./main.css"; -import { GenericFormula, InvertibleIntegralFormula } from "game/formulas/types"; +import { GenericPlane, createPlane } from "./planes"; const toast = useToast(); @@ -64,6 +64,16 @@ export interface EmpowererState { powered: boolean; } +export interface PortalGeneratorState { + tier: Resources | undefined; + influences: string[]; +} + +export interface PortalState { + id: string; + powered: boolean; +} + const mineLootTable = { dirt: 120, sand: 60, @@ -132,7 +142,8 @@ const tools = { iron: { cost: 1e10, name: "Portal Generator", - type: "portalGenerator" + type: "portalGenerator", + state: { tier: null, influences: [] } }, silver: { cost: 1e12, @@ -227,7 +238,8 @@ export const main = createLayer("main", function (this: BaseLayer) { }, {} as Record), sand: board.types.dowsing.nodes.value[0], wood: board.types.quarry.nodes.value[0], - coal: board.types.empowerer.nodes.value[0] + coal: board.types.empowerer.nodes.value[0], + iron: board.types.portalGenerator.nodes.value[0] })); const resourceLevels = computed(() => @@ -854,6 +866,156 @@ export const main = createLayer("main", function (this: BaseLayer) { running: isPowered(node) }), draggable: true + }, + portalGenerator: { + shape: Shape.Diamond, + size: 50, + title: "⛩ī¸", + label: node => { + if (node === board.selectedNode.value) { + return { + text: + (node.state as unknown as PortalGeneratorState).tier == null + ? "Portal Spawner - Drag a resource to me!" + : `Spawning ${ + (node.state as unknown as PortalGeneratorState).tier + }-tier portal` + }; + } + if ((board as GenericBoard).draggingNode.value?.type === "resource") { + const resource = ( + (board as GenericBoard).draggingNode.value + ?.state as unknown as ResourceState + ).type; + const text = + (node.state as unknown as PortalGeneratorState).tier === resource + ? "Disconnect" + : `${camelToTitle(resource)}-tier Portal`; + return { + text, + color: "var(--accent2)" + }; + } + // TODO handle influences + return null; + }, + actionDistance: Math.PI / 4, + actions: [ + { + id: "deselect", + icon: "close", + tooltip: { text: "Disconnect all" }, + onClick(node: BoardNode) { + node.state = { + ...(node.state as object), + tier: undefined, + influences: [] + }; + board.selectedAction.value = null; + board.selectedNode.value = null; + }, + visibility: (node: BoardNode) => { + const { tier, influences } = + node.state as unknown as PortalGeneratorState; + return tier != null || influences.length > 0; + } + }, + { + id: "makePortal", + icon: "done", + tooltip: node => ({ + text: `Spawn ${ + (node.state as unknown as PortalGeneratorState).tier + }-tier portal` + }), + onClick(node) { + let id = 0; + while (`portal-${id}` in layers) { + id++; + } + addLayer( + createPlane( + `portal-${id}`, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (node.state as unknown as PortalGeneratorState).tier!, + Math.floor(Math.random() * 4294967296) + ), + player + ); + const newNode = { + id: getUniqueNodeID(board as GenericBoard), + position: { ...node.position }, + type: "portal", + state: { id: `portal-${id}`, powered: false } + }; + board.placeInAvailableSpace(newNode); + board.nodes.value.push(newNode); + board.selectedAction.value = null; + board.selectedNode.value = null; + node.state = { tier: undefined, influences: [] }; + }, + visibility: node => + (node.state as unknown as PortalGeneratorState).tier != null + } + ], + canAccept(node, otherNode) { + return otherNode.type === "resource" || otherNode.type === "influence"; + }, + onDrop(node, otherNode) { + if (otherNode.type === "resource") { + const droppedType = (otherNode.state as unknown as ResourceState).type; + const currentType = (node.state as unknown as PortalGeneratorState).tier; + node.state = { + ...(node.state as object), + tier: droppedType === currentType ? undefined : droppedType + }; + } else if (otherNode.type === "influence") { + const droppedInfluence = otherNode.state as string; + const currentInfluences = (node.state as unknown as PortalGeneratorState) + .influences; + if (currentInfluences.includes(droppedInfluence)) { + node.state = { + ...(node.state as object), + influences: currentInfluences.filter(i => i !== droppedInfluence) + }; + } else { + node.state = { + ...(node.state as object), + influences: [...currentInfluences, droppedInfluence] + }; + } + } + board.selectedNode.value = node; + }, + draggable: true + }, + portal: { + shape: Shape.Diamond, + size: 50, + title: "🌀", + label: node => + node === board.selectedNode.value + ? { + text: `Portal to ${ + ( + layers[ + (node.state as unknown as PortalState).id + ] as GenericPlane + ).name + }`, + color: ( + layers[(node.state as unknown as PortalState).id] as GenericPlane + ).color + } + : null, + actionDistance: Math.PI / 4, + actions: [togglePoweredAction], + classes: node => ({ + running: isPowered(node) + }), + outlineColor: node => + (layers[(node.state as unknown as PortalState).id] as GenericPlane).background, + draggable: true } }, style: { @@ -929,6 +1091,23 @@ export const main = createLayer("main", function (this: BaseLayer) { }); }); } + if (portalGenerator.value != null) { + if ((portalGenerator.value.state as unknown as PortalGeneratorState).tier != null) { + links.push({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + startNode: portalGenerator.value!, + endNode: + resourceNodes.value[ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (portalGenerator.value.state as unknown as PortalGeneratorState) + .tier! + ], + stroke: "var(--foreground)", + strokeWidth: 4 + }); + } + // TODO link to influences + } return links; } })); @@ -944,6 +1123,9 @@ export const main = createLayer("main", function (this: BaseLayer) { const dowsing: ComputedRef = computed(() => toolNodes.value.sand); const quarry: ComputedRef = computed(() => toolNodes.value.wood); const empowerer: ComputedRef = computed(() => toolNodes.value.coal); + const portalGenerator: ComputedRef = computed( + () => toolNodes.value.iron + ); function grantResource(type: Resources, amount: DecimalSource) { let node = resourceNodes.value[type]; @@ -1304,6 +1486,17 @@ export const main = createLayer("main", function (this: BaseLayer) { energyProductionChange ); + const activePortals = computed(() => board.types.portal.nodes.value.filter(n => isPowered(n))); + + watch(activePortals, activePortals => { + nextTick(() => { + player.tabs = [ + "main", + ...activePortals.map(node => (node.state as unknown as PortalState).id) + ]; + }); + }); + return { name: "World", board, @@ -1368,7 +1561,22 @@ export const main = createLayer("main", function (this: BaseLayer) { export const getInitialLayers = ( /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ player: Partial -): Array => [main]; +): Array => { + const layers: GenericLayer[] = [main]; + let id = 0; + while (`portal-${id}` in (player.layers ?? {})) { + const layer = player.layers?.[`portal-${id}`] as LayerData; + layers.push( + createPlane( + `portal-${id}`, + layer.tier ?? "dirt", + layer.seed ?? Math.floor(Math.random() * 4294967296) + ) + ); + id++; + } + return layers; +}; /** * A computed ref whose value is true whenever the game is over. diff --git a/src/data/projInfo.json b/src/data/projInfo.json index e67ecbe..e334b2e 100644 --- a/src/data/projInfo.json +++ b/src/data/projInfo.json @@ -11,7 +11,7 @@ "versionNumber": "0.0", "versionTitle": "Initial Commit", - "allowGoBack": true, + "allowGoBack": false, "defaultShowSmall": false, "defaultDecimalsShown": 2, "useHeader": true, diff --git a/src/data/utils.tsx b/src/data/utils.tsx new file mode 100644 index 0000000..e24b883 --- /dev/null +++ b/src/data/utils.tsx @@ -0,0 +1,235 @@ +import { camelToTitle } from "util/common"; + +// Simple Fast Counter is a part of PractRand suite by Chris Doty-Humphrey. +export function sfc32(a: number, b: number, c: number, d: number) { + return function () { + a >>>= 0; + b >>>= 0; + c >>>= 0; + d >>>= 0; + let t = (a + b) | 0; + a = b ^ (b >>> 9); + b = (c + (c << 3)) | 0; + c = (c << 21) | (c >>> 11); + d = (d + 1) | 0; + t = (t + d) | 0; + c = (c + t) | 0; + return (t >>> 0) / 4294967296; + }; +} + +// Modified version of this lib to use seed: https://github.com/hbi99/namegen +const morphemes = { + 1: [ + "b", + "c", + "d", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "p", + "q", + "r", + "s", + "t", + "v", + "w", + "x", + "y", + "z" + ], + 2: ["a", "e", "o", "u"], + 3: [ + "br", + "cr", + "dr", + "fr", + "gr", + "pr", + "str", + "tr", + "bl", + "cl", + "fl", + "gl", + "pl", + "sl", + "sc", + "sk", + "sm", + "sn", + "sp", + "st", + "sw", + "ch", + "sh", + "th", + "wh" + ], + 4: [ + "ae", + "ai", + "ao", + "au", + "a", + "ay", + "ea", + "ei", + "eo", + "eu", + "e", + "ey", + "ua", + "ue", + "ui", + "uo", + "u", + "uy", + "ia", + "ie", + "iu", + "io", + "iy", + "oa", + "oe", + "ou", + "oi", + "o", + "oy" + ], + 5: [ + "turn", + "ter", + "nus", + "rus", + "tania", + "hiri", + "hines", + "gawa", + "nides", + "carro", + "rilia", + "stea", + "lia", + "lea", + "ria", + "nov", + "phus", + "mia", + "nerth", + "wei", + "ruta", + "tov", + "zuno", + "vis", + "lara", + "nia", + "liv", + "tera", + "gantu", + "yama", + "tune", + "ter", + "nus", + "cury", + "bos", + "pra", + "thea", + "nope", + "tis", + "clite" + ], + 6: [ + "una", + "ion", + "iea", + "iri", + "illes", + "ides", + "agua", + "olla", + "inda", + "eshan", + "oria", + "ilia", + "erth", + "arth", + "orth", + "oth", + "illon", + "ichi", + "ov", + "arvis", + "ara", + "ars", + "yke", + "yria", + "onoe", + "ippe", + "osie", + "one", + "ore", + "ade", + "adus", + "urn", + "ypso", + "ora", + "iuq", + "orix", + "apus", + "ion", + "eon", + "eron", + "ao", + "omia" + ] +}; +const templates = [ + [1, 2, 5], + [2, 3, 6], + [3, 4, 5], + [4, 3, 6], + [3, 4, 2, 5], + [2, 1, 3, 6], + [3, 4, 2, 5], + [4, 3, 1, 6], + [3, 4, 1, 4, 5], + [4, 1, 4, 3, 6] +] as const; +export function getName(random: () => number) { + const template = templates[Math.floor(random() * templates.length)]; + let name = ""; + for (let i = 0; i < template.length; i++) { + const morphemeSet = morphemes[template[i]]; + name += morphemeSet[Math.floor(random() * morphemeSet.length)]; + } + return camelToTitle(name); +} + +export function getColor(base: [number, number, number], random: () => number) { + const [h, s, v] = rgb2hsv(...base); + const [r, g, b] = hsv2rgb(Math.floor(random() * 360), s, v); + return `rgb(${r * 255}, ${g * 255}, ${b * 255})`; +} + +// https://stackoverflow.com/a/54070620/4376101 +// input: r,g,b in [0,1], out: h in [0,360) and s,v in [0,1] +function rgb2hsv(r: number, g: number, b: number) { + const v = Math.max(r, g, b), + c = v - Math.min(r, g, b); + const h = c && (v == r ? (g - b) / c : v == g ? 2 + (b - r) / c : 4 + (r - g) / c); + return [60 * (h < 0 ? h + 6 : h), v && c / v, v]; +} + +// https://stackoverflow.com/a/54024653/4376101 +// input: h in [0,360] and s,v in [0,1] - output: r,g,b in [0,1] +function hsv2rgb(h: number, s: number, v: number) { + const f = (n: number, k = (n + h / 60) % 6) => v - v * s * Math.max(Math.min(k, 4 - k, 1), 0); + return [f(5), f(3), f(1)]; +}