(function (root, factory) { "use strict"; const data = root.PrincessLimaData || (typeof require === "function" ? require("./princess-lima-data.js") : null); const stateApi = root.PrincessLimaState || (typeof require === "function" ? require("./princess-lima-state.js") : null); const api = factory(data, stateApi); if (typeof module === "object" && module.exports) module.exports = api; root.PrincessLimaSystems = api; }(typeof globalThis !== "undefined" ? globalThis : this, function (Data, State) { "use strict"; function copy(state) { return State.normalize(JSON.parse(JSON.stringify(state))); } function normalizedVector(x, y, speed) { const length = Math.hypot(Number(x) || 0, Number(y) || 0); return length ? { x: x / length * speed, y: y / length * speed } : { x: 0, y: 0 }; } function approachVelocity(currentX, currentY, inputX, inputY, deltaMs, boots) { const target = normalizedVector(inputX, inputY, 176); const moving = Boolean(inputX || inputY); const rate = moving ? (boots ? 1900 : 1500) : 2300; const change = Math.min(50, Math.max(0, Number(deltaMs) || 0)) / 1000 * rate; function approach(value, goal) { return Math.abs(goal - value) <= change ? goal : value + Math.sign(goal - value) * change; } const velocity = { x: approach(currentX || 0, target.x), y: approach(currentY || 0, target.y) }; const length = Math.hypot(velocity.x, velocity.y); return length > 176 ? normalizedVector(velocity.x, velocity.y, 176) : velocity; } function pointInShape(x, y, shape, padding) { const pad = Number(padding) || 0; if (shape.shape === "circle") return Math.hypot(x - shape.x, y - shape.y) <= shape.radius + pad; return x >= shape.x - pad && x <= shape.x + shape.width + pad && y >= shape.y - pad && y <= shape.y + shape.height + pad; } function activeObstacles(state, regionId) { const map = Data.MAPS[regionId]; if (!map) return []; return map.obstacles.concat((map.dynamicObstacles || []).filter((item) => !state.unlockedRoutes.includes(item.opensWith))); } function isSafePosition(state, regionId, x, y) { if (!Data.MAPS[regionId] || x < 30 || y < 30 || x > Data.WIDTH - 30 || y > Data.HEIGHT - 30) return false; return !activeObstacles(state, regionId).some((shape) => pointInShape(x, y, shape, 14)); } function nearestSafeSpawn(state, regionId, x, y) { const map = Data.MAPS[regionId] || Data.MAPS.village; if (isSafePosition(state, regionId, x, y)) return { region: regionId, spawn: "saved", x, y }; const candidates = Object.entries(map.spawns).filter(([, point]) => isSafePosition(state, regionId, point.x, point.y)); const best = candidates.sort((a, b) => Math.hypot(a[1].x - x, a[1].y - y) - Math.hypot(b[1].x - x, b[1].y - y))[0]; return best ? { region: regionId, spawn: best[0], x: best[1].x, y: best[1].y } : { region: "village", spawn: "start", x: State.START.x, y: State.START.y }; } function quantity(state, id) { const found = state.inventory.find((entry) => entry.id === id); return found ? found.quantity : 0; } function addItem(state, id, amount) { const next = copy(state); const item = Data.ITEMS[id]; if (!next || !item) return next; const cap = item.stack || 1; const existing = next.inventory.find((entry) => entry.id === id); if (existing) existing.quantity = Math.min(cap, existing.quantity + Math.max(1, Math.floor(amount || 1))); else next.inventory.push({ id, quantity: Math.min(cap, Math.max(1, Math.floor(amount || 1))) }); return State.normalize(next); } function removeItem(state, id, amount) { const next = copy(state); const item = Data.ITEMS[id]; if (!next || !item || item.protected) return { state: next, removed: false }; const existing = next.inventory.find((entry) => entry.id === id); if (!existing || existing.quantity < amount) return { state: next, removed: false }; existing.quantity -= amount; next.inventory = next.inventory.filter((entry) => entry.quantity > 0); return { state: State.normalize(next), removed: true }; } function useItem(state, id) { const item = Data.ITEMS[id]; const next = copy(state); if (!next || !item || item.type !== "consumable" || quantity(next, id) < 1 || next.health >= next.maxHealth) { return { state: next, used: false, amount: 0 }; } const amount = Math.min(next.maxHealth - next.health, item.heal); next.health += amount; const removed = removeItem(next, id, 1); return { state: removed.state, used: true, amount }; } function startQuest(state, id) { const next = copy(state); if (!next || !Data.QUESTS[id]) return next; if (next.quests[id].status === "locked") next.quests[id] = { status: "active", count: 0, rewarded: false }; return State.normalize(next); } function progressQuest(state, id, amount) { let next = startQuest(state, id); if (!next || next.quests[id].status === "complete") return next; next.quests[id].count = Math.min(Data.QUESTS[id].target, next.quests[id].count + Math.max(1, amount || 1)); return next.quests[id].count >= Data.QUESTS[id].target ? completeQuest(next, id) : State.normalize(next); } function applyQuestConsequences(state, id) { const routes = { village_defence: "village_defended", find_guide: "guide_found", ruins_light: "ruins_complete", wolf_miniboss: "briar_defeated", repair_bridge: "bridge_repaired", stone_guardian: "guardian_defeated", free_scout: "emblem_found", break_wards: "wards_broken", defeat_malrec: "malrec_defeated" }; if (routes[id] && !state.unlockedRoutes.includes(routes[id])) state.unlockedRoutes.push(routes[id]); if (id === "aftermath") state.flags.aftermath_complete = true; if (id === "find_guide") state.chapter = Math.max(state.chapter, 2); if (id === "repair_bridge") state.chapter = Math.max(state.chapter, 3); if (id === "free_scout") state.chapter = Math.max(state.chapter, 4); if (id === "defeat_malrec") state.story = "rescue"; return state; } function completeQuest(state, id) { let next = startQuest(state, id); if (!next || next.quests[id].status === "complete") return next; next.quests[id].status = "complete"; if (!next.quests[id].rewarded) { (Data.QUESTS[id].reward || []).forEach(([itemId, amount]) => { next = addItem(next, itemId, amount); }); next.quests[id].rewarded = true; } next = applyQuestConsequences(next, id); return State.normalize(next); } function solvePuzzle(state, id) { const next = copy(state); if (!next || next.solvedPuzzles.includes(id)) return next; next.solvedPuzzles.push(id); if (id === "forest_stones") return completeQuest(next, "find_guide"); if (id === "ruin_braziers") return completeQuest(next, "ruins_light"); if (id === "bridge_winches") return completeQuest(next, "repair_bridge"); if (id === "shadow_wards") return completeQuest(next, "break_wards"); return State.normalize(next); } function damage(state, amount, now, lastHitAt) { const next = copy(state); if (!next || Number(now) - Number(lastHitAt || 0) < 850) return { state: next, hit: false, defeated: false }; const dealt = Math.max(1, Math.round(amount - next.defence)); next.health = Math.max(0, next.health - dealt); return { state: State.normalize(next), hit: true, defeated: next.health <= 0, amount: dealt }; } function bossPhase(health, maximum, phases) { const count = Math.max(1, Math.floor(Number(phases) || 1)); const ratio = Math.max(0, Number(health) || 0) / Math.max(1, Number(maximum) || 1); if (count === 1) return 1; if (count === 2) return ratio > 0.5 ? 1 : 2; return ratio > 0.66 ? 1 : ratio > 0.33 ? 2 : 3; } const ATTACK_TIMING = Object.freeze({ windup: 85, active: 105, recovery: 135, cooldown: 360 }); function attackPhase(elapsed, timing) { const value = Math.max(0, Number(elapsed) || 0); const config = timing || ATTACK_TIMING; if (value < config.windup) return "windup"; if (value < config.windup + config.active) return "active"; if (value < config.windup + config.active + config.recovery) return "recovery"; return "complete"; } function attackHitbox(facing, x, y) { const boxes = { north: { x: x - 25, y: y - 76, width: 50, height: 66, angle: 270 }, south: { x: x - 25, y: y + 8, width: 50, height: 66, angle: 90 }, west: { x: x - 76, y: y - 27, width: 66, height: 54, angle: 180 }, east: { x: x + 10, y: y - 27, width: 66, height: 54, angle: 0 } }; return boxes[facing] || boxes.south; } function wallSafeKnockback(state, regionId, x, y, directionX, directionY, distance) { const push = normalizedVector(directionX, directionY, Math.max(0, Number(distance) || 0)); const target = { x: x + push.x, y: y + push.y }; if (isSafePosition(state, regionId, target.x, target.y)) return target; for (let scale = 0.75; scale >= 0; scale -= 0.25) { const candidate = { x: x + push.x * scale, y: y + push.y * scale }; if (isSafePosition(state, regionId, candidate.x, candidate.y)) return candidate; } return { x, y }; } function respawn(state) { let next = copy(state); if (!next) return next; const safe = nearestSafeSpawn(next, next.checkpoint.region, next.checkpoint.x, next.checkpoint.y); next.health = Math.max(50, Math.ceil(next.maxHealth * 0.65)); next.region = safe.region; next.position = { spawn: safe.spawn, x: safe.x, y: safe.y, facing: "south" }; return State.normalize(next); } function recordBoss(state, id) { let next = copy(state); if (!next || !["briar_wolf", "stone_guardian", "captain", "malrec"].includes(id)) return next; if (!next.defeatedBosses.includes(id)) next.defeatedBosses.push(id); if (id === "briar_wolf") next = completeQuest(next, "wolf_miniboss"); if (id === "stone_guardian") next = completeQuest(next, "stone_guardian"); if (id === "captain") next = progressQuest(next, "free_scout", 1); if (id === "malrec") next = completeQuest(next, "defeat_malrec"); return State.normalize(next); } function currentObjective(state) { if (state.rescued) return "Princess Lima is safe. The road home is open."; const activeMain = Data.QUEST_IDS.find((id) => Data.QUESTS[id].main && state.quests[id].status === "active"); if (activeMain) return Data.QUESTS[activeMain].description; if (!state.flags.aftermath_complete) return "Speak with Elder Corin in the village square."; if (!state.unlockedRoutes.includes("village_defended")) return "Defend the village from the second raid."; if (!state.unlockedRoutes.includes("guide_found")) return "Find Guide Tovin in the Whispering Woods."; if (!state.unlockedRoutes.includes("ruins_complete")) return "Explore the Sunken Ruins."; if (!state.unlockedRoutes.includes("briar_defeated")) return "Defeat the Briar Wolf and open the mountain trail."; if (!state.unlockedRoutes.includes("bridge_repaired")) return "Repair the bridge across the Mountain Pass."; if (!state.unlockedRoutes.includes("guardian_defeated")) return "Defeat the Stone Guardian."; if (!state.unlockedRoutes.includes("emblem_found")) return "Free Scout Elowen at Blackridge Camp."; if (!state.unlockedRoutes.includes("wards_broken")) return "Break the three wards inside the fortress."; if (!state.unlockedRoutes.includes("malrec_defeated")) return "Confront Lord Malrec in the throne room."; return "Find Princess Lima beyond the throne room."; } return Object.freeze({ normalizedVector, approachVelocity, pointInShape, activeObstacles, isSafePosition, nearestSafeSpawn, quantity, addItem, removeItem, useItem, startQuest, progressQuest, completeQuest, solvePuzzle, damage, bossPhase, ATTACK_TIMING, attackPhase, attackHitbox, wallSafeKnockback, respawn, recordBoss, currentObjective }); }));