import config from "./config.js"; const hashText = text => { let value = 2166136261; for (let index = 0; index < text.length; index++) { value ^= text.charCodeAt(index); value = Math.imul(value, 16777619); } return value >>> 0; }; export const nextRandom = run => { let value = Number(run.randomState || run.seed || Date.now()) >>> 0; value += 0x6d2b79f5; run.randomState = value >>> 0; let result = value; result = Math.imul(result ^ result >>> 15, result | 1); result ^= result + Math.imul(result ^ result >>> 7, result | 61); return ((result ^ result >>> 14) >>> 0) / 4294967296; }; export const randomGet = (run, list) => list[Math.floor(nextRandom(run) * list.length)]; const weightedType = (run, weights) => { const entries = Object.entries(weights); const total = entries.reduce((sum, entry) => sum + entry[1], 0); let cursor = nextRandom(run) * total; for (const [type, weight] of entries) { cursor -= weight; if (cursor <= 0) return type; } return entries[0][0]; }; const connectFloors = (run, edges, currentFloor, nextFloor) => { const addEdge = (source, target) => { if (!edges.some(edge => edge[0] == source.id && edge[1] == target.id)) { edges.push([source.id, target.id]); } }; currentFloor.forEach(source => { const targets = nextFloor.slice().sort((left, right) => { const distance = Math.abs(left.routeIndex - source.routeIndex) - Math.abs(right.routeIndex - source.routeIndex); return distance || left.routeIndex - right.routeIndex; }); addEdge(source, targets[0]); if (targets.length > 1 && Math.abs(targets[1].routeIndex - source.routeIndex) <= 1 && nextRandom(run) < 0.45) { addEdge(source, targets[1]); } }); nextFloor.forEach(target => { if (!edges.some(edge => edge[1] == target.id)) { const source = currentFloor.slice().sort((left, right) => { const distance = Math.abs(left.routeIndex - target.routeIndex) - Math.abs(right.routeIndex - target.routeIndex); return distance || left.routeIndex - right.routeIndex; })[0]; addEdge(source, target); } }); }; const getFloorRoutes = (run, act, floor, lastFloor) => { const routeCount = Math.max(1, config.routeCount); if (floor == 0 || floor == lastFloor) return [(routeCount - 1) / 2]; const maximum = Math.max(1, Math.min(routeCount, act.floorNodes[floor] || routeCount)); const configuredMinimum = act.minFloorNodes?.[floor] ?? Math.max(2, maximum - 1); const minimum = Math.max(1, Math.min(maximum, configuredMinimum)); const count = minimum + Math.floor(nextRandom(run) * (maximum - minimum + 1)); const routes = Array.from({ length: routeCount }, (_, index) => index); for (let index = routes.length - 1; index > 0; index--) { const target = Math.floor(nextRandom(run) * (index + 1)); [routes[index], routes[target]] = [routes[target], routes[index]]; } return routes.slice(0, count).sort((left, right) => left - right); }; export const generateActMap = (run, actIndex) => { const act = config.acts[actIndex]; const nodes = []; const edges = []; const floors = []; const lastFloor = act.floorNodes.length - 1; act.floorNodes.forEach((count, floor) => { const floorNodes = []; const routes = getFloorRoutes(run, act, floor, lastFloor); routes.forEach((routeIndex, index) => { let type; if (floor == lastFloor) type = "boss"; else if (floor == 0) type = "battle"; else type = weightedType(run, act.nodeWeights); const id = `${act.id}_f${floor}_n${index}_${Math.floor(nextRandom(run) * 1e6)}`; const node = { id, floor, type, routeIndex, x: Math.round((routeIndex + 1) * 100 / (config.routeCount + 1)), y: Math.round(90 - floor * 80 / Math.max(1, lastFloor)), completed: false, }; nodes.push(node); floorNodes.push(node); }); floors.push(floorNodes); }); for (let floor = 0; floor < floors.length - 1; floor++) { connectFloors(run, edges, floors[floor], floors[floor + 1]); } return { layoutVersion: config.mapLayoutVersion, routeCount: config.routeCount, actIndex, actId: act.id, nodes, edges, currentNodeId: null, completedNodeIds: [], }; }; const createDeck = () => config.startingDeck.map((entry, index) => ({ id: `mengsan_card_${Date.now()}_${index}`, suit: entry[0], number: entry[1], name: entry[2], nature: entry[3] || null, affixes: entry[4] ? entry[4].slice() : [], upgrade: 0, })); export const createRun = character => { const seed = (Date.now() ^ Math.floor(Math.random() * 0x7fffffff)) >>> 0; const run = { schemaVersion: config.schemaVersion, runId: `mengsan_${Date.now()}`, seed, randomState: seed, status: "running", revision: 0, actIndex: 0, player: { id: "player_1", ownerId: "local", character, hp: null, maxHp: null, gold: 0, deck: createDeck(), permanentSkills: [], temporarySkills: [], items: [], handLimitBonus: 0, }, statistics: { completedNodes: 0, defeatedEnemies: 0, goldEarned: 0, }, }; run.map = generateActMap(run, 0); return run; }; export const getSelectableNodes = run => { const map = run.map; if (!map.currentNodeId) { return map.nodes.filter(node => node.floor == 0 && !node.completed); } const ids = map.edges.filter(edge => edge[0] == map.currentNodeId).map(edge => edge[1]); return map.nodes.filter(node => ids.includes(node.id) && !node.completed); }; export const completeNode = (run, nodeId) => { const node = run.map.nodes.find(current => current.id == nodeId); if (!node || node.completed) return false; node.completed = true; run.map.currentNodeId = node.id; run.map.completedNodeIds.push(node.id); run.statistics.completedNodes++; run.revision++; return true; }; export const insertStoryNode = (run, sourceId) => { const map = run.map; const source = map.nodes.find(node => node.id == sourceId); const edgeIndex = map.edges.findIndex(edge => edge[0] == sourceId); if (!source || edgeIndex < 0) return null; const oldEdge = map.edges[edgeIndex]; const target = map.nodes.find(node => node.id == oldEdge[1]); if (!target) return null; const node = { id: `story_${hashText(`${run.runId}_${sourceId}_${run.revision}`)}`, floor: source.floor + 0.45, type: "story", x: Math.max(8, Math.min(92, Math.round((source.x + target.x) / 2 + (nextRandom(run) - 0.5) * 16))), y: Math.round((source.y + target.y) / 2), completed: false, dynamic: true, }; map.edges.splice(edgeIndex, 1, [source.id, node.id], [node.id, target.id]); map.nodes.push(node); return node; }; export const enterNextAct = run => { run.actIndex++; if (run.actIndex >= config.acts.length) { run.status = "completed"; return false; } run.map = generateActMap(run, run.actIndex); run.revision++; return true; }; export const getNodeEncounter = (run, node) => { const act = config.acts[run.actIndex]; let pool = act.enemies; if (node.type == "elite" || node.type == "story") pool = act.eliteEnemies?.length ? act.eliteEnemies : act.enemies; if (node.type == "boss") return { enemy: act.boss, gold: act.baseGold * 3, boss: true }; return { enemy: randomGet(run, pool.filter(Boolean)), gold: node.type == "elite" || node.type == "story" ? act.baseGold * 2 : act.baseGold, boss: false, }; };