|
| 1 | +const EVENT_CADENCE = 4; |
| 2 | + |
| 3 | +const EVENT_TABLE = [ |
| 4 | + { |
| 5 | + id: 'ancient_ruins', |
| 6 | + title: 'Ancient ruins', |
| 7 | + description: 'Scouts uncover a buried archive near your border.', |
| 8 | + choices: [ |
| 9 | + { id: 'study', label: 'Study the tablets', effects: { science: 5, cityProd: 1 } }, |
| 10 | + { id: 'salvage', label: 'Salvage the stonework', effects: { prod: 4, cityProd: 2 } }, |
| 11 | + ], |
| 12 | + }, |
| 13 | + { |
| 14 | + id: 'bumper_harvest', |
| 15 | + title: 'Bumper harvest', |
| 16 | + description: 'A mild season gives your capital surplus grain.', |
| 17 | + choices: [ |
| 18 | + { id: 'store', label: 'Store grain', effects: { food: 4, cityFood: 4 } }, |
| 19 | + { id: 'trade', label: 'Trade surplus', effects: { prod: 2, science: 2 } }, |
| 20 | + ], |
| 21 | + }, |
| 22 | + { |
| 23 | + id: 'frontier_plague', |
| 24 | + title: 'Frontier sickness', |
| 25 | + description: 'A caravan brings illness and rumors from the frontier.', |
| 26 | + choices: [ |
| 27 | + { id: 'quarantine', label: 'Quarantine quickly', effects: { prod: -1, cityFood: 2 } }, |
| 28 | + { id: 'research', label: 'Fund healers', effects: { science: 4, food: -1 } }, |
| 29 | + ], |
| 30 | + }, |
| 31 | +]; |
| 32 | + |
| 33 | +function hashSeed(seed) { |
| 34 | + let hash = 2166136261; |
| 35 | + for (const char of String(seed)) { |
| 36 | + hash ^= char.codePointAt(0); |
| 37 | + hash = Math.imul(hash, 16777619); |
| 38 | + } |
| 39 | + return hash >>> 0; |
| 40 | +} |
| 41 | + |
| 42 | +export function createSeededRng(seed) { |
| 43 | + let state = hashSeed(seed) || 1; |
| 44 | + return () => { |
| 45 | + state = Math.imul(state, 1664525) + 1013904223; |
| 46 | + return (state >>> 0) / 4294967296; |
| 47 | + }; |
| 48 | +} |
| 49 | + |
| 50 | +function distance(a, b) { |
| 51 | + return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); |
| 52 | +} |
| 53 | + |
| 54 | +function isSafeTile(tile, safeZones, minDistance) { |
| 55 | + return safeZones.every(zone => distance(tile, zone) > minDistance); |
| 56 | +} |
| 57 | + |
| 58 | +export function planBarbarianCamps({ |
| 59 | + map, |
| 60 | + seed, |
| 61 | + count = 3, |
| 62 | + safeZones = [], |
| 63 | + minDistance = 3, |
| 64 | +}) { |
| 65 | + const rng = createSeededRng(seed); |
| 66 | + const candidates = []; |
| 67 | + |
| 68 | + for (let y = 0; y < map.length; y++) { |
| 69 | + for (let x = 0; x < map[y].length; x++) { |
| 70 | + const tile = { x, y }; |
| 71 | + if (map[y][x] !== 'water' && isSafeTile(tile, safeZones, minDistance)) { |
| 72 | + candidates.push({ ...tile, score: rng() }); |
| 73 | + } |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + const sortedCandidates = [...candidates]; |
| 78 | + sortedCandidates.sort((a, b) => a.score - b.score); |
| 79 | + |
| 80 | + return sortedCandidates |
| 81 | + .slice(0, count) |
| 82 | + .map((tile, index) => ({ |
| 83 | + id: `camp-${index + 1}`, |
| 84 | + name: `Camp ${index + 1}`, |
| 85 | + x: tile.x, |
| 86 | + y: tile.y, |
| 87 | + hp: 40, |
| 88 | + lastSpawnTurn: 1, |
| 89 | + cleared: false, |
| 90 | + })); |
| 91 | +} |
| 92 | + |
| 93 | +export function shouldSpawnFromCamp(camp, turn, cadence = 3) { |
| 94 | + return !camp.cleared && turn - camp.lastSpawnTurn >= cadence; |
| 95 | +} |
| 96 | + |
| 97 | +export function createBarbarianUnit(camp, id, turn) { |
| 98 | + return { |
| 99 | + id, |
| 100 | + owner: 'barbarian', |
| 101 | + x: camp.x, |
| 102 | + y: camp.y, |
| 103 | + hp: 70, |
| 104 | + atk: 22, |
| 105 | + def: 10, |
| 106 | + mov: 1, |
| 107 | + movLeft: 1, |
| 108 | + type: 'raider', |
| 109 | + campId: camp.id, |
| 110 | + spawnedTurn: turn, |
| 111 | + }; |
| 112 | +} |
| 113 | + |
| 114 | +export function buildRandomEvent({ seed, turn, cadence = EVENT_CADENCE }) { |
| 115 | + if (turn <= 1 || turn % cadence !== 0) return null; |
| 116 | + const rng = createSeededRng(`${seed}:${turn}`); |
| 117 | + const event = EVENT_TABLE[Math.floor(rng() * EVENT_TABLE.length)]; |
| 118 | + |
| 119 | + return { |
| 120 | + id: event.id, |
| 121 | + title: event.title, |
| 122 | + description: event.description, |
| 123 | + turn, |
| 124 | + choices: event.choices.map(choice => ({ |
| 125 | + id: choice.id, |
| 126 | + label: choice.label, |
| 127 | + effects: { ...choice.effects }, |
| 128 | + })), |
| 129 | + }; |
| 130 | +} |
| 131 | + |
| 132 | +export function applyEventChoice(empire, event, choiceId) { |
| 133 | + const choice = event.choices.find(item => item.id === choiceId); |
| 134 | + if (!choice) return { ...empire }; |
| 135 | + |
| 136 | + const next = { ...empire }; |
| 137 | + for (const [key, value] of Object.entries(choice.effects)) { |
| 138 | + next[key] = (next[key] ?? 0) + value; |
| 139 | + } |
| 140 | + return next; |
| 141 | +} |
| 142 | + |
| 143 | +export function formatEventEffects(effects) { |
| 144 | + return Object.entries(effects) |
| 145 | + .map(([key, value]) => `${value > 0 ? '+' : ''}${value} ${key}`) |
| 146 | + .join(', '); |
| 147 | +} |
0 commit comments