Spaces:
Running
Running
Game: free-roam overworld with biome-themed enemy spawning
Browse filesRebuild the Game page from a fixed arena into a free-roam map: the hero roams the whole three-band overworld and enemies spawn around it as you explore — RTS humans on the Forgotten Plains, orcs in the Orc Kingdom, the Dark Brotherhood in the Necropolis — culled as you move on.
- web/*.js: rebuilt bundles (comboBattler + map/classes/enemies sandboxes) from auto-battler.
- web/tiny.js: per-biome enemy rosters wired into the Game page.
- app.py: full-screen #battle-stage; relocate Gradio footer links into the sidebar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- app.py +28 -9
- web/classesSandbox.js +40 -14
- web/comboBattler.js +1483 -198
- web/enemiesSandbox.js +40 -14
- web/mapSandbox.js +60 -10
- web/tiny.js +54 -11
app.py
CHANGED
|
@@ -122,12 +122,23 @@ THEME = ('<style>'
|
|
| 122 |
# Gradio still hides it (display:none on the inactive tab's ancestor).
|
| 123 |
'.gradio-container .tabitem{padding:0 !important;}'
|
| 124 |
'.gradio-container .tabs{border:0 !important;}'
|
| 125 |
-
'#sprite-stage,#persona-stage,#diary-stage,#classes-stage,#enemies-stage,#worldmap-stage{position:fixed !important;top:0;bottom:0;'
|
| 126 |
'right:0;left:var(--tac-w,240px);height:auto !important;z-index:1;}'
|
| 127 |
'body.tac-collapsed #sprite-stage,body.tac-collapsed #persona-stage,'
|
| 128 |
'body.tac-collapsed #diary-stage,body.tac-collapsed #classes-stage,body.tac-collapsed #enemies-stage,'
|
| 129 |
-
'body.tac-collapsed #worldmap-stage{left:0;}'
|
| 130 |
-
'@media (max-width:768px){#sprite-stage,#persona-stage,#diary-stage,#classes-stage,#enemies-stage,#worldmap-stage{left:0;}}'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
'</style>')
|
| 132 |
# `upgrade-insecure-requests` is needed on the HTTPS Space (prevents mixed-content behind HF's
|
| 133 |
# TLS edge) but BREAKS plain-http LAN testing: it forces every asset/manifest/frame URL to https
|
|
@@ -145,7 +156,9 @@ HEAD = (_CSP
|
|
| 145 |
'<link rel="stylesheet" href="/web/shell/worldmap.css">'
|
| 146 |
'<script type="module" src="/web/tiny.js"></script>'
|
| 147 |
'<script src="/web/shell/sidebar.js"></script>')
|
| 148 |
-
|
|
|
|
|
|
|
| 149 |
|
| 150 |
|
| 151 |
# Shared app-shell sidebar: rendered from the SAME nav.json + sidebar.css +
|
|
@@ -176,11 +189,17 @@ def build_sidebar(nav):
|
|
| 176 |
out.append('<div class="tac-section">')
|
| 177 |
if sec.get("title"):
|
| 178 |
out.append(f'<div class="tac-section-title">{sec["title"]}</div>')
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
out.append('</div>')
|
| 185 |
out.append('</aside>')
|
| 186 |
out.append('<button class="tac-toggle tac-reopen" title="Open menu">›</button>')
|
|
|
|
| 122 |
# Gradio still hides it (display:none on the inactive tab's ancestor).
|
| 123 |
'.gradio-container .tabitem{padding:0 !important;}'
|
| 124 |
'.gradio-container .tabs{border:0 !important;}'
|
| 125 |
+
'#sprite-stage,#persona-stage,#diary-stage,#classes-stage,#enemies-stage,#worldmap-stage,#battle-stage{position:fixed !important;top:0;bottom:0;'
|
| 126 |
'right:0;left:var(--tac-w,240px);height:auto !important;z-index:1;}'
|
| 127 |
'body.tac-collapsed #sprite-stage,body.tac-collapsed #persona-stage,'
|
| 128 |
'body.tac-collapsed #diary-stage,body.tac-collapsed #classes-stage,body.tac-collapsed #enemies-stage,'
|
| 129 |
+
'body.tac-collapsed #worldmap-stage,body.tac-collapsed #battle-stage{left:0;}'
|
| 130 |
+
'@media (max-width:768px){#sprite-stage,#persona-stage,#diary-stage,#classes-stage,#enemies-stage,#worldmap-stage,#battle-stage{left:0;}}'
|
| 131 |
+
# Gradio's relocated footer links (Use via API / Built with Gradio / Settings), styled to look
|
| 132 |
+
# like the sidebar nav items WITHOUT the tac-nav-item class — that class makes sidebar.js hijack
|
| 133 |
+
# the click (mark them active = white, and swallow navigation). web/tiny.js copies the reference
|
| 134 |
+
# nav item's padding onto them; here we mirror the rest (icon on the left, hover, colour).
|
| 135 |
+
'#tac-extlinks .tac-extlink{display:flex !important;align-items:center;gap:8px !important;width:100%;'
|
| 136 |
+
'min-height:0 !important;background:none !important;border:0 !important;border-radius:0 !important;'
|
| 137 |
+
'box-shadow:none !important;justify-content:flex-start !important;text-align:left !important;'
|
| 138 |
+
'color:var(--tac-ink) !important;font-family:var(--tac-font) !important;font-size:14px !important;'
|
| 139 |
+
'font-weight:500 !important;text-decoration:none !important;cursor:pointer;}'
|
| 140 |
+
'#tac-extlinks .tac-extlink:hover{background:var(--tac-bg-2) !important;}'
|
| 141 |
+
'#tac-extlinks .tac-extlink img,#tac-extlinks .tac-extlink svg{order:-1;width:18px !important;height:18px !important;flex-shrink:0;margin:0 !important;}'
|
| 142 |
'</style>')
|
| 143 |
# `upgrade-insecure-requests` is needed on the HTTPS Space (prevents mixed-content behind HF's
|
| 144 |
# TLS edge) but BREAKS plain-http LAN testing: it forces every asset/manifest/frame URL to https
|
|
|
|
| 156 |
'<link rel="stylesheet" href="/web/shell/worldmap.css">'
|
| 157 |
'<script type="module" src="/web/tiny.js"></script>'
|
| 158 |
'<script src="/web/shell/sidebar.js"></script>')
|
| 159 |
+
# The Game stage fills the whole content area (full-screen map), like the other stages — the
|
| 160 |
+
# `#battle-stage` rules above lift it out of Gradio's flow; this just sets the load-time background.
|
| 161 |
+
STAGE = "background:#0b0e12"
|
| 162 |
|
| 163 |
|
| 164 |
# Shared app-shell sidebar: rendered from the SAME nav.json + sidebar.css +
|
|
|
|
| 189 |
out.append('<div class="tac-section">')
|
| 190 |
if sec.get("title"):
|
| 191 |
out.append(f'<div class="tac-section-title">{sec["title"]}</div>')
|
| 192 |
+
if sec.get("title") == "App":
|
| 193 |
+
# The App section is filled entirely by web/tiny.js, which relocates Gradio's footer links
|
| 194 |
+
# (Use via API, Built with Gradio, Settings) here — so all three share the same treatment
|
| 195 |
+
# instead of Settings being a special nav item routed back to the footer.
|
| 196 |
+
out.append('<div id="tac-extlinks"></div>')
|
| 197 |
+
else:
|
| 198 |
+
for it in items:
|
| 199 |
+
cls = "tac-nav-item active" if first else "tac-nav-item"
|
| 200 |
+
first = False
|
| 201 |
+
out.append(f'<a class="{cls}" data-target="{it["space"]}" href="#">'
|
| 202 |
+
f'<span class="tac-ico">{it.get("icon","")}</span><span>{it["label"]}</span></a>')
|
| 203 |
out.append('</div>')
|
| 204 |
out.append('</aside>')
|
| 205 |
out.append('<button class="tac-toggle tac-reopen" title="Open menu">›</button>')
|
web/classesSandbox.js
CHANGED
|
@@ -1022,11 +1022,11 @@ function makeActor(unit, team, id, slot) {
|
|
| 1022 |
// optional: idle until a foe is within this distance (else always engage)
|
| 1023 |
};
|
| 1024 |
}
|
| 1025 |
-
function makeTeamBattle({ seed = 1, players = [], enemies = [], sandbox = false, respawnDummies = 0, freeCast = false, world = null } = {}) {
|
| 1026 |
const actors = [];
|
| 1027 |
players.filter(Boolean).slice(0, 5).forEach((u, i) => actors.push(makeActor(u, "player", `P${i}`, i)));
|
| 1028 |
enemies.filter(Boolean).slice(0, 5).forEach((u, i) => actors.push(makeActor(u, "enemy", `E${i}`, i)));
|
| 1029 |
-
return { t: 0, rng: makeRng(seed), actors, projectiles: [], log: [], over: false, winner: null, sandbox, respawnDummies, freeCast, input: {}, world };
|
| 1030 |
}
|
| 1031 |
function setInput(b, id, cmd) {
|
| 1032 |
if (!b.input) b.input = {};
|
|
@@ -1319,9 +1319,10 @@ function applyEffect(b, src, tgt, e, delivery = "spell", s = null) {
|
|
| 1319 |
}
|
| 1320 |
}
|
| 1321 |
function shadowStep(b, a, tgt) {
|
|
|
|
| 1322 |
const dx = a.x - tgt.x, dy = a.y - tgt.y, len = Math.hypot(dx, dy) || 1;
|
| 1323 |
-
a.x = Math.max(0, Math.min(
|
| 1324 |
-
a.y = Math.max(0, Math.min(
|
| 1325 |
}
|
| 1326 |
function fireSpellProjectiles(b, src, tgt, amt, e, n) {
|
| 1327 |
const base = dist(src, tgt) / 900;
|
|
@@ -1622,9 +1623,10 @@ function timeToHit(px, py, rvx, rvy, R) {
|
|
| 1622 |
}
|
| 1623 |
var clampField = (v, r, max) => Math.max(r, Math.min(max - r, v));
|
| 1624 |
function stepMove(b, a, vx, vy, dt) {
|
|
|
|
| 1625 |
const w = b.world && b.world.walkable;
|
| 1626 |
-
let nx = clampField(a.x + vx * dt, a.radius,
|
| 1627 |
-
let ny = clampField(a.y + vy * dt, a.radius,
|
| 1628 |
if (w) {
|
| 1629 |
if (!w(nx, a.y)) nx = a.x;
|
| 1630 |
if (!w(nx, ny)) ny = a.y;
|
|
@@ -1633,6 +1635,7 @@ function stepMove(b, a, vx, vy, dt) {
|
|
| 1633 |
a.y = ny;
|
| 1634 |
}
|
| 1635 |
function resolveOverlaps(b) {
|
|
|
|
| 1636 |
const live = b.actors.filter((a) => a.alive);
|
| 1637 |
for (let it = 0; it < DEOVERLAP_ITERS; it++) {
|
| 1638 |
for (let i = 0; i < live.length; i++) {
|
|
@@ -1648,10 +1651,10 @@ function resolveOverlaps(b) {
|
|
| 1648 |
const aShare = aFix ? 0 : oFix ? 1 : 0.5;
|
| 1649 |
const oShare = oFix ? 0 : aFix ? 1 : 0.5;
|
| 1650 |
const yPush = uy / COLLISION_Y_WEIGHT;
|
| 1651 |
-
a.x = clampField(a.x - ux * push * aShare, a.radius,
|
| 1652 |
-
a.y = clampField(a.y - yPush * push * aShare, a.radius,
|
| 1653 |
-
o.x = clampField(o.x + ux * push * oShare, o.radius,
|
| 1654 |
-
o.y = clampField(o.y + yPush * push * oShare, o.radius,
|
| 1655 |
}
|
| 1656 |
}
|
| 1657 |
}
|
|
@@ -2063,10 +2066,10 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 2063 |
const actorOf = (id) => getBattle()?.actors.find((x) => x.id === id);
|
| 2064 |
const floats = [];
|
| 2065 |
const sheetsById = {};
|
| 2066 |
-
|
| 2067 |
if (!def?.idle) {
|
| 2068 |
sheetsById[id] = null;
|
| 2069 |
-
|
| 2070 |
}
|
| 2071 |
try {
|
| 2072 |
const load = (url) => url ? recoloredTexture(Texture, url, def.idle, def.recolor) : null;
|
|
@@ -2092,6 +2095,7 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 2092 |
sheetsById[id] = null;
|
| 2093 |
}
|
| 2094 |
}
|
|
|
|
| 2095 |
const skillIcons = {};
|
| 2096 |
for (const s of CB_SKILLS) {
|
| 2097 |
try {
|
|
@@ -2127,7 +2131,7 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 2127 |
} catch {
|
| 2128 |
}
|
| 2129 |
const skillPlay = {};
|
| 2130 |
-
|
| 2131 |
const cell = sheetsById[id]?.cell;
|
| 2132 |
const map = {};
|
| 2133 |
for (const [skillId, fxCfg] of Object.entries(def.skillFx || {})) {
|
|
@@ -2152,8 +2156,9 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 2152 |
}
|
| 2153 |
skillPlay[id] = map;
|
| 2154 |
}
|
|
|
|
| 2155 |
const view = {};
|
| 2156 |
-
|
| 2157 |
const sh = sheetsById[id];
|
| 2158 |
const c = new Container();
|
| 2159 |
let sp;
|
|
@@ -2202,6 +2207,25 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 2202 |
units.addChild(c);
|
| 2203 |
view[id] = { def, sheets: sh, container: c, sprite: sp, overlay, overlayKey: null, bars, status, statusSprites: {}, outline, castOutline, mode: null, facing: null, flash: 0, dead: false };
|
| 2204 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2205 |
function setLoop(v, mode, facing) {
|
| 2206 |
if (v.mode === mode && v.facing === facing) return;
|
| 2207 |
v.mode = mode;
|
|
@@ -2577,6 +2601,8 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 2577 |
floats,
|
| 2578 |
actorOf,
|
| 2579 |
skillPlay,
|
|
|
|
|
|
|
| 2580 |
setLoop,
|
| 2581 |
playOnce,
|
| 2582 |
resume,
|
|
|
|
| 1022 |
// optional: idle until a foe is within this distance (else always engage)
|
| 1023 |
};
|
| 1024 |
}
|
| 1025 |
+
function makeTeamBattle({ seed = 1, players = [], enemies = [], sandbox = false, respawnDummies = 0, freeCast = false, world = null, field = null } = {}) {
|
| 1026 |
const actors = [];
|
| 1027 |
players.filter(Boolean).slice(0, 5).forEach((u, i) => actors.push(makeActor(u, "player", `P${i}`, i)));
|
| 1028 |
enemies.filter(Boolean).slice(0, 5).forEach((u, i) => actors.push(makeActor(u, "enemy", `E${i}`, i)));
|
| 1029 |
+
return { t: 0, rng: makeRng(seed), actors, projectiles: [], log: [], over: false, winner: null, sandbox, respawnDummies, freeCast, input: {}, world, field: field || FIELD };
|
| 1030 |
}
|
| 1031 |
function setInput(b, id, cmd) {
|
| 1032 |
if (!b.input) b.input = {};
|
|
|
|
| 1319 |
}
|
| 1320 |
}
|
| 1321 |
function shadowStep(b, a, tgt) {
|
| 1322 |
+
const f = b.field || FIELD;
|
| 1323 |
const dx = a.x - tgt.x, dy = a.y - tgt.y, len = Math.hypot(dx, dy) || 1;
|
| 1324 |
+
a.x = Math.max(0, Math.min(f.w, tgt.x + dx / len * (BASIC_MELEE_GW * 0.8)));
|
| 1325 |
+
a.y = Math.max(0, Math.min(f.h, tgt.y + dy / len * (BASIC_MELEE_GW * 0.8)));
|
| 1326 |
}
|
| 1327 |
function fireSpellProjectiles(b, src, tgt, amt, e, n) {
|
| 1328 |
const base = dist(src, tgt) / 900;
|
|
|
|
| 1623 |
}
|
| 1624 |
var clampField = (v, r, max) => Math.max(r, Math.min(max - r, v));
|
| 1625 |
function stepMove(b, a, vx, vy, dt) {
|
| 1626 |
+
const f = b.field || FIELD;
|
| 1627 |
const w = b.world && b.world.walkable;
|
| 1628 |
+
let nx = clampField(a.x + vx * dt, a.radius, f.w);
|
| 1629 |
+
let ny = clampField(a.y + vy * dt, a.radius, f.h);
|
| 1630 |
if (w) {
|
| 1631 |
if (!w(nx, a.y)) nx = a.x;
|
| 1632 |
if (!w(nx, ny)) ny = a.y;
|
|
|
|
| 1635 |
a.y = ny;
|
| 1636 |
}
|
| 1637 |
function resolveOverlaps(b) {
|
| 1638 |
+
const f = b.field || FIELD;
|
| 1639 |
const live = b.actors.filter((a) => a.alive);
|
| 1640 |
for (let it = 0; it < DEOVERLAP_ITERS; it++) {
|
| 1641 |
for (let i = 0; i < live.length; i++) {
|
|
|
|
| 1651 |
const aShare = aFix ? 0 : oFix ? 1 : 0.5;
|
| 1652 |
const oShare = oFix ? 0 : aFix ? 1 : 0.5;
|
| 1653 |
const yPush = uy / COLLISION_Y_WEIGHT;
|
| 1654 |
+
a.x = clampField(a.x - ux * push * aShare, a.radius, f.w);
|
| 1655 |
+
a.y = clampField(a.y - yPush * push * aShare, a.radius, f.h);
|
| 1656 |
+
o.x = clampField(o.x + ux * push * oShare, o.radius, f.w);
|
| 1657 |
+
o.y = clampField(o.y + yPush * push * oShare, o.radius, f.h);
|
| 1658 |
}
|
| 1659 |
}
|
| 1660 |
}
|
|
|
|
| 2066 |
const actorOf = (id) => getBattle()?.actors.find((x) => x.id === id);
|
| 2067 |
const floats = [];
|
| 2068 |
const sheetsById = {};
|
| 2069 |
+
async function loadSheets(id, def) {
|
| 2070 |
if (!def?.idle) {
|
| 2071 |
sheetsById[id] = null;
|
| 2072 |
+
return;
|
| 2073 |
}
|
| 2074 |
try {
|
| 2075 |
const load = (url) => url ? recoloredTexture(Texture, url, def.idle, def.recolor) : null;
|
|
|
|
| 2095 |
sheetsById[id] = null;
|
| 2096 |
}
|
| 2097 |
}
|
| 2098 |
+
for (const [id, def] of Object.entries(defsById)) await loadSheets(id, def);
|
| 2099 |
const skillIcons = {};
|
| 2100 |
for (const s of CB_SKILLS) {
|
| 2101 |
try {
|
|
|
|
| 2131 |
} catch {
|
| 2132 |
}
|
| 2133 |
const skillPlay = {};
|
| 2134 |
+
async function buildSkillPlay(id, def) {
|
| 2135 |
const cell = sheetsById[id]?.cell;
|
| 2136 |
const map = {};
|
| 2137 |
for (const [skillId, fxCfg] of Object.entries(def.skillFx || {})) {
|
|
|
|
| 2156 |
}
|
| 2157 |
skillPlay[id] = map;
|
| 2158 |
}
|
| 2159 |
+
for (const [id, def] of Object.entries(defsById)) await buildSkillPlay(id, def);
|
| 2160 |
const view = {};
|
| 2161 |
+
function buildView(id, def) {
|
| 2162 |
const sh = sheetsById[id];
|
| 2163 |
const c = new Container();
|
| 2164 |
let sp;
|
|
|
|
| 2207 |
units.addChild(c);
|
| 2208 |
view[id] = { def, sheets: sh, container: c, sprite: sp, overlay, overlayKey: null, bars, status, statusSprites: {}, outline, castOutline, mode: null, facing: null, flash: 0, dead: false };
|
| 2209 |
}
|
| 2210 |
+
for (const [id, def] of Object.entries(defsById)) buildView(id, def);
|
| 2211 |
+
async function addActor(id, def) {
|
| 2212 |
+
if (view[id]) return view[id];
|
| 2213 |
+
defsById[id] = def;
|
| 2214 |
+
await loadSheets(id, def);
|
| 2215 |
+
await buildSkillPlay(id, def);
|
| 2216 |
+
buildView(id, def);
|
| 2217 |
+
return view[id];
|
| 2218 |
+
}
|
| 2219 |
+
function removeActor(id) {
|
| 2220 |
+
const v = view[id];
|
| 2221 |
+
if (!v) return;
|
| 2222 |
+
units.removeChild(v.container);
|
| 2223 |
+
v.container.destroy({ children: true });
|
| 2224 |
+
delete view[id];
|
| 2225 |
+
delete sheetsById[id];
|
| 2226 |
+
delete skillPlay[id];
|
| 2227 |
+
delete defsById[id];
|
| 2228 |
+
}
|
| 2229 |
function setLoop(v, mode, facing) {
|
| 2230 |
if (v.mode === mode && v.facing === facing) return;
|
| 2231 |
v.mode = mode;
|
|
|
|
| 2601 |
floats,
|
| 2602 |
actorOf,
|
| 2603 |
skillPlay,
|
| 2604 |
+
addActor,
|
| 2605 |
+
removeActor,
|
| 2606 |
setLoop,
|
| 2607 |
playOnce,
|
| 2608 |
resume,
|
web/comboBattler.js
CHANGED
|
@@ -1,84 +1,9 @@
|
|
| 1 |
-
// ../auto-battler/src/engine/rng.js
|
| 2 |
-
function makeRng(seed) {
|
| 3 |
-
let a = seed >>> 0;
|
| 4 |
-
return function rng() {
|
| 5 |
-
a |= 0;
|
| 6 |
-
a = a + 1831565813 | 0;
|
| 7 |
-
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
| 8 |
-
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
| 9 |
-
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
| 10 |
-
};
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
// ../auto-battler/src/engine/worldgen.js
|
| 14 |
-
function hash2(seed, x, y) {
|
| 15 |
-
let h = Math.imul(x | 0, 374761393) + Math.imul(y | 0, 668265263) + Math.imul(seed | 0, 2654435761);
|
| 16 |
-
h = Math.imul(h ^ h >>> 13, 1274126177);
|
| 17 |
-
h ^= h >>> 16;
|
| 18 |
-
return (h >>> 0) / 4294967296;
|
| 19 |
-
}
|
| 20 |
-
var smooth = (t) => t * t * (3 - 2 * t);
|
| 21 |
-
var lerp = (a, b, t) => a + (b - a) * t;
|
| 22 |
-
function valueNoise(seed, x, y) {
|
| 23 |
-
const xi = Math.floor(x), yi = Math.floor(y);
|
| 24 |
-
const xf = x - xi, yf = y - yi;
|
| 25 |
-
const v00 = hash2(seed, xi, yi), v10 = hash2(seed, xi + 1, yi);
|
| 26 |
-
const v01 = hash2(seed, xi, yi + 1), v11 = hash2(seed, xi + 1, yi + 1);
|
| 27 |
-
const u = smooth(xf), v = smooth(yf);
|
| 28 |
-
return lerp(lerp(v00, v10, u), lerp(v01, v11, u), v);
|
| 29 |
-
}
|
| 30 |
-
function fbm(seed, x, y, octaves = 5, lacunarity = 2, gain = 0.5) {
|
| 31 |
-
let amp = 1, freq = 1, sum = 0, norm = 0;
|
| 32 |
-
for (let o = 0; o < octaves; o++) {
|
| 33 |
-
sum += amp * valueNoise(seed + o * 1013, x * freq, y * freq);
|
| 34 |
-
norm += amp;
|
| 35 |
-
amp *= gain;
|
| 36 |
-
freq *= lacunarity;
|
| 37 |
-
}
|
| 38 |
-
return sum / norm;
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
// ../auto-battler/src/engine/fpGen.js
|
| 42 |
-
var sub = (seed, salt) => (Math.imul(seed | 0, 2654435761) ^ (salt | 0)) >>> 0;
|
| 43 |
-
var DIRT_SCALE = 0.04;
|
| 44 |
-
var DIRT_THRESHOLD = 0.56;
|
| 45 |
-
function isDirt(seed, x, y) {
|
| 46 |
-
return fbm(sub(seed, 53543), x * DIRT_SCALE, y * DIRT_SCALE) > DIRT_THRESHOLD;
|
| 47 |
-
}
|
| 48 |
-
var STONE_SCALE = 0.085;
|
| 49 |
-
var STONE_THRESHOLD = 0.66;
|
| 50 |
-
function isStone(seed, x, y) {
|
| 51 |
-
return fbm(sub(seed, 22286), x * STONE_SCALE, y * STONE_SCALE) > STONE_THRESHOLD;
|
| 52 |
-
}
|
| 53 |
-
var FOREST_SCALE = 0.05;
|
| 54 |
-
function forestField(seed, x, y) {
|
| 55 |
-
return fbm(sub(seed, 15740503), x * FOREST_SCALE, y * FOREST_SCALE);
|
| 56 |
-
}
|
| 57 |
-
var STONE_CLUMP_SCALE = 0.11;
|
| 58 |
-
function stoneClumpField(seed, x, y) {
|
| 59 |
-
return fbm(sub(seed, 5702849), x * STONE_CLUMP_SCALE, y * STONE_CLUMP_SCALE);
|
| 60 |
-
}
|
| 61 |
-
var ELEV_SCALE = 0.025;
|
| 62 |
-
var ELEV_THRESHOLD = 0.6;
|
| 63 |
-
function isRaised(seed, x, y) {
|
| 64 |
-
return fbm(sub(seed, 57836), x * ELEV_SCALE, y * ELEV_SCALE) > ELEV_THRESHOLD;
|
| 65 |
-
}
|
| 66 |
-
var RIVER_SCALE = 0.022;
|
| 67 |
-
var RIVER_WIDTH = 0.035;
|
| 68 |
-
var WARP_SCALE = 0.03;
|
| 69 |
-
var WARP_AMP = 22;
|
| 70 |
-
function isRiver(seed, x, y) {
|
| 71 |
-
const wx = x + WARP_AMP * (fbm(sub(seed, 1297), x * WARP_SCALE, y * WARP_SCALE) - 0.5);
|
| 72 |
-
const wy = y + WARP_AMP * (fbm(sub(seed, 1298), x * WARP_SCALE, y * WARP_SCALE) - 0.5);
|
| 73 |
-
return Math.abs(fbm(sub(seed, 8654), wx * RIVER_SCALE, wy * RIVER_SCALE) - 0.5) < RIVER_WIDTH;
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
// ../auto-battler/src/render/chunkedMap.js
|
| 77 |
function createChunkedMap(pixi, host, config) {
|
| 78 |
const { Application, Assets, Sprite, Container, Texture, Rectangle, RenderTexture } = pixi;
|
| 79 |
-
const
|
| 80 |
-
const
|
| 81 |
-
const CHUNKPX =
|
| 82 |
const BG = config.background ?? "#69636f";
|
| 83 |
const Z_DEFAULT = config.zoomDefault ?? 1;
|
| 84 |
const Z_MIN = config.zoomMin ?? 1 / 32;
|
|
@@ -91,6 +16,16 @@ function createChunkedMap(pixi, host, config) {
|
|
| 91 |
const MACRO_LEVEL_MAX = config.macroLevelMax ?? 6;
|
| 92 |
const DETAIL_BUDGET = config.detailBudget ?? 1;
|
| 93 |
const MACRO_BUDGET = config.macroBudget ?? 8;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
let app = null, root = null, genRoot = null, macroRoot = null, shadowLayer = null, propLayer = null;
|
| 95 |
let ctx = null;
|
| 96 |
let alive = true;
|
|
@@ -98,7 +33,7 @@ function createChunkedMap(pixi, host, config) {
|
|
| 98 |
let seed = config.seed ?? 1;
|
| 99 |
let zoom = Z_DEFAULT;
|
| 100 |
let cameraDirty = true, genPending = false, lastEmitTile = null;
|
| 101 |
-
const camera = { x: CHUNKPX / 2, y: CHUNKPX / 2 };
|
| 102 |
const chunks = /* @__PURE__ */ new Map();
|
| 103 |
const macroChunks = /* @__PURE__ */ new Map();
|
| 104 |
const fading = /* @__PURE__ */ new Set();
|
|
@@ -111,12 +46,12 @@ function createChunkedMap(pixi, host, config) {
|
|
| 111 |
const pinch = { on: false, dist: 0 };
|
| 112 |
const handlers = {};
|
| 113 |
const emit = () => listeners.forEach((fn) => fn(getSnapshot()));
|
| 114 |
-
const getSnapshot = () => ({ seed, zoom, cx: Math.round(camera.x /
|
| 115 |
function tex(source, c, r) {
|
| 116 |
const k = source.uid + ":" + c + "," + r;
|
| 117 |
let t = texCache.get(k);
|
| 118 |
if (!t) {
|
| 119 |
-
t = new Texture({ source, frame: new Rectangle(c *
|
| 120 |
texCache.set(k, t);
|
| 121 |
}
|
| 122 |
return t;
|
|
@@ -131,16 +66,16 @@ function createChunkedMap(pixi, host, config) {
|
|
| 131 |
return t;
|
| 132 |
}
|
| 133 |
function makeChunk(cx, cy) {
|
| 134 |
-
const x0 = cx *
|
| 135 |
const tmp = new Container();
|
| 136 |
const add = (source, c, r, tx, ty) => {
|
| 137 |
const sp = new Sprite(tex(source, c, r));
|
| 138 |
-
sp.x = tx *
|
| 139 |
-
sp.y = ty *
|
| 140 |
tmp.addChild(sp);
|
| 141 |
return sp;
|
| 142 |
};
|
| 143 |
-
const res = config.bake({ cx, cy, x0, y0, seed, chunk:
|
| 144 |
const rt = RenderTexture.create({ width: CHUNKPX, height: CHUNKPX, autoGenerateMipmaps: true, scaleMode: "nearest" });
|
| 145 |
rt.source.minFilter = "linear";
|
| 146 |
rt.source.mipmapFilter = "linear";
|
|
@@ -148,12 +83,12 @@ function createChunkedMap(pixi, host, config) {
|
|
| 148 |
rt.source.updateMipmaps();
|
| 149 |
tmp.destroy({ children: true });
|
| 150 |
const sprite = new Sprite(rt);
|
| 151 |
-
sprite.x = x0 *
|
| 152 |
-
sprite.y = y0 *
|
| 153 |
return { sprite, rt, meta: res.meta ?? null, live: res.live ?? null };
|
| 154 |
}
|
| 155 |
function chooseMacroLevel(z) {
|
| 156 |
-
return Math.max(0, Math.min(MACRO_LEVEL_MAX, Math.round(Math.log2(MACRO_TEXEL_PX / (
|
| 157 |
}
|
| 158 |
function makeMacroChunk(L, mcx, mcy) {
|
| 159 |
const step2 = 1 << L, t0x = mcx * MCHUNK * step2, t0y = mcy * MCHUNK * step2;
|
|
@@ -174,13 +109,26 @@ function createChunkedMap(pixi, host, config) {
|
|
| 174 |
const t = Texture.from(cv);
|
| 175 |
t.source.scaleMode = "linear";
|
| 176 |
const sprite = new Sprite(t);
|
| 177 |
-
sprite.x = t0x *
|
| 178 |
-
sprite.y = t0y *
|
| 179 |
-
sprite.width = sprite.height = MCHUNK * step2 *
|
| 180 |
return { sprite, tex: t };
|
| 181 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
function reconcile() {
|
| 183 |
if (!genRoot || !ctx || !app) return;
|
|
|
|
|
|
|
| 184 |
const sx = Math.round(app.screen.width / 2 - camera.x * zoom);
|
| 185 |
const sy = Math.round(app.screen.height / 2 - camera.y * zoom);
|
| 186 |
for (const L of [macroRoot, genRoot, shadowLayer, propLayer]) {
|
|
@@ -217,8 +165,14 @@ function createChunkedMap(pixi, host, config) {
|
|
| 217 |
}
|
| 218 |
function reconcileDetail() {
|
| 219 |
const halfW = app.screen.width / 2 / zoom, halfH = app.screen.height / 2 / zoom;
|
| 220 |
-
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
for (const [key, ch] of chunks) {
|
| 223 |
const [cx, cy] = key.split(",").map(Number);
|
| 224 |
if (cx < c0 - 1 || cx > c1 + 1 || cy < r0 - 1 || cy > r1 + 1) evictChunk(key, ch);
|
|
@@ -244,10 +198,17 @@ function createChunkedMap(pixi, host, config) {
|
|
| 244 |
return missing.length > DETAIL_BUDGET;
|
| 245 |
}
|
| 246 |
function reconcileMacro(L) {
|
| 247 |
-
const px = MCHUNK * (1 << L) *
|
| 248 |
const halfW = app.screen.width / 2 / zoom, halfH = app.screen.height / 2 / zoom;
|
| 249 |
-
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
for (const [key, mc] of macroChunks) {
|
| 252 |
const [ml, mx, my] = key.split(",").map(Number);
|
| 253 |
if (ml !== L || mx < c0 - 1 || mx > c1 + 1 || my < r0 - 1 || my > r1 + 1) {
|
|
@@ -284,7 +245,7 @@ function createChunkedMap(pixi, host, config) {
|
|
| 284 |
}
|
| 285 |
function zoomAt(factor, lx, ly) {
|
| 286 |
if (!app) return;
|
| 287 |
-
const nz = Math.min(Z_MAX, Math.max(Z_MIN, zoom * factor));
|
| 288 |
if (nz === zoom) return;
|
| 289 |
const sw = app.screen.width, sh = app.screen.height;
|
| 290 |
const wx = camera.x + (lx - sw / 2) / zoom, wy = camera.y + (ly - sh / 2) / zoom;
|
|
@@ -409,7 +370,7 @@ function createChunkedMap(pixi, host, config) {
|
|
| 409 |
if (cameraDirty || genPending) {
|
| 410 |
reconcile();
|
| 411 |
cameraDirty = false;
|
| 412 |
-
const tk = Math.round(camera.x /
|
| 413 |
if (tk !== lastEmitTile) {
|
| 414 |
lastEmitTile = tk;
|
| 415 |
emit();
|
|
@@ -470,11 +431,17 @@ function createChunkedMap(pixi, host, config) {
|
|
| 470 |
}
|
| 471 |
function tileIndexAt(wx, wy) {
|
| 472 |
if (!config.tileIndexAt) return null;
|
| 473 |
-
const cx = Math.floor(wx /
|
| 474 |
const ch = chunks.get(cx + "," + cy);
|
| 475 |
if (!ch) return null;
|
| 476 |
return config.tileIndexAt(wx, wy, ch.meta);
|
| 477 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
function setEnabled(v) {
|
| 479 |
enabled = v;
|
| 480 |
if (v) cameraDirty = true;
|
|
@@ -523,7 +490,207 @@ function createChunkedMap(pixi, host, config) {
|
|
| 523 |
ctx = null;
|
| 524 |
texCache.clear();
|
| 525 |
}
|
| 526 |
-
return { ready, regenerate, destroy, onChange, getSnapshot, getCamera, tileIndexAt, zoomBy, setEnabled, onTick, getEntityLayer, getApp, screenToWorld, worldToScreen, tile:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
}
|
| 528 |
|
| 529 |
// ../auto-battler/src/render/tileAutotile.js
|
|
@@ -532,6 +699,7 @@ var rect = (c, r, w, h) => {
|
|
| 532 |
for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) a.push([c + i, r + j]);
|
| 533 |
return a;
|
| 534 |
};
|
|
|
|
| 535 |
var rhash = (x, y, salt, seed) => Math.imul(x * 73856093 ^ y * 19349663 ^ seed + (salt | 0), 2654435761) >>> 0;
|
| 536 |
function hashU32(a, b, c) {
|
| 537 |
let h = Math.imul((a | 0) ^ 2654435769, 2654435761);
|
|
@@ -546,6 +714,45 @@ var sparse = (base, vars, x, y, salt, rate, seed) => {
|
|
| 546 |
return h % rate === 0 ? vars[(h >>> 5) % vars.length] : base;
|
| 547 |
};
|
| 548 |
var lerp3 = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
function blockTile([c0, r0], N, E, S, W) {
|
| 550 |
const cx = W && !E ? 0 : E && !W ? 2 : 1;
|
| 551 |
const cy = N && !S ? 0 : S && !N ? 2 : 1;
|
|
@@ -643,21 +850,215 @@ function bakeCliffs(cfg, { chunk, x0, y0, seed, raised, place, accept = () => tr
|
|
| 643 |
}
|
| 644 |
}
|
| 645 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 646 |
// ../auto-battler/src/render/forgottenPlains.js
|
| 647 |
var FP = "/assets/minifantasy/Minifantasy_ForgottenPlains_v3.6_Commercial_Version/Minifantasy_ForgottenPlains_Assets";
|
| 648 |
-
var
|
| 649 |
var SHADOW = `${FP}/Tileset/Minifantasy_ForgottenPlainsTilesShadows.png`;
|
| 650 |
var PROPS = `${FP}/props/Minifantasy_ForgottenPlainsProps.png`;
|
| 651 |
var PROP_SHADOW = `${FP}/props/Minifantasy_ForgottenPlainsPropsShadows.png`;
|
| 652 |
var FP_MOCKUP_URL = `${FP}/Minifantasy_ForgottenPlainsMockup.png`;
|
| 653 |
-
var
|
| 654 |
-
var
|
| 655 |
var GRASS_BASE = [37, 11];
|
| 656 |
var GRASS_VARS = [...rect(1, 1, 4, 1), ...rect(2, 3, 3, 5)];
|
| 657 |
var GRASS_RATE = 9;
|
| 658 |
var DIRT_BLOCK = [7, 3];
|
| 659 |
var DIRT_FILL = [8, 4];
|
| 660 |
-
var
|
| 661 |
var STONE_BLOCK = [12, 3];
|
| 662 |
var STONE_FILL = [13, 4];
|
| 663 |
var STONE_VARS = [[12, 1], [13, 1], [14, 1]];
|
|
@@ -795,8 +1196,8 @@ var STONE_BLOCK_Y = 2;
|
|
| 795 |
var STONE_CLUMP_THRESHOLD = 0.7;
|
| 796 |
var STONE_CLUMP_MIN_NB = 3;
|
| 797 |
var STONE_CLUMP_FILL = 0.7;
|
| 798 |
-
var
|
| 799 |
-
var
|
| 800 |
var COL_STONE = [120, 120, 122];
|
| 801 |
var COL_WATER = [74, 116, 196];
|
| 802 |
function loadImg(url) {
|
|
@@ -814,9 +1215,9 @@ async function buildShadowMask(url) {
|
|
| 814 |
cv.height = img.naturalHeight;
|
| 815 |
const g = cv.getContext("2d", { willReadFrequently: true });
|
| 816 |
g.drawImage(img, 0, 0);
|
| 817 |
-
const cols = img.naturalWidth /
|
| 818 |
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
|
| 819 |
-
const d = g.getImageData(c *
|
| 820 |
for (let i = 3; i < d.length; i += 4) {
|
| 821 |
if (d[i] > 8) {
|
| 822 |
set.add(c + "," + r);
|
|
@@ -826,9 +1227,9 @@ async function buildShadowMask(url) {
|
|
| 826 |
}
|
| 827 |
return set;
|
| 828 |
}
|
| 829 |
-
function
|
| 830 |
const WATER_BUFFER = 2;
|
| 831 |
-
const M = WATER_BUFFER + 4, SZ =
|
| 832 |
const idx = (i, j) => j * SZ + i;
|
| 833 |
const clean = (pred) => {
|
| 834 |
let cur = new Uint8Array(SZ * SZ);
|
|
@@ -886,14 +1287,24 @@ function chunkFields(seed, x0, y0) {
|
|
| 886 |
}
|
| 887 |
return { waterF, dirt, stone, raisedField, forestRegion, stoneClumpRegion, M, SZ, x0, y0 };
|
| 888 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 889 |
var fpConfig = (seed) => ({
|
| 890 |
seed,
|
| 891 |
-
tile:
|
| 892 |
-
chunk:
|
| 893 |
background: "#5a7b3a",
|
| 894 |
async load({ Assets }) {
|
| 895 |
const ctx = { tiles: null, shadow: null, shadowSet: null, props: null, propShadow: null };
|
| 896 |
-
const t = await Assets.load(
|
| 897 |
t.source.scaleMode = "nearest";
|
| 898 |
ctx.tiles = t.source;
|
| 899 |
try {
|
|
@@ -924,7 +1335,7 @@ var fpConfig = (seed) => ({
|
|
| 924 |
// `accept(tx,ty)` (chunk-local) gates which tiles this biome paints — defaults to all, so the
|
| 925 |
// standalone map is unchanged; the multi-biome overworld passes a per-biome dither mask.
|
| 926 |
bake({ x0, y0, seed: seed2, ctx, tmp, Sprite, tex, texFrame, add, accept = () => true }) {
|
| 927 |
-
const f =
|
| 928 |
const { waterF, dirt, stone, raisedField, forestRegion, stoneClumpRegion, M, SZ } = f;
|
| 929 |
const idx = (i, j) => j * SZ + i;
|
| 930 |
const atW = (wx, wy) => waterF[idx(wx - x0 + M, wy - y0 + M)] === 1;
|
|
@@ -935,8 +1346,8 @@ var fpConfig = (seed) => ({
|
|
| 935 |
add(ctx.tiles, coord[0], coord[1], tx, ty);
|
| 936 |
if (ctx.shadow && (!ctx.shadowSet || ctx.shadowSet.has(coord[0] + "," + coord[1]))) {
|
| 937 |
const sh = new Sprite(tex(ctx.shadow, coord[0], coord[1]));
|
| 938 |
-
sh.x = tx *
|
| 939 |
-
sh.y = ty *
|
| 940 |
tmp.addChild(sh);
|
| 941 |
}
|
| 942 |
};
|
|
@@ -957,24 +1368,24 @@ var fpConfig = (seed) => ({
|
|
| 957 |
} else cr = blockTile(BLOCK, N, E, S, W);
|
| 958 |
place(cr, tx, ty);
|
| 959 |
};
|
| 960 |
-
for (let ty = 0; ty <
|
| 961 |
if (!accept(tx, ty)) continue;
|
| 962 |
const wx = x0 + tx, wy = y0 + ty;
|
| 963 |
place(sparse(GRASS_BASE, GRASS_VARS, wx, wy, 0, GRASS_RATE, seed2), tx, ty);
|
| 964 |
-
if (atD(wx, wy)) blob(atD, DIRT_BLOCK, DIRT_FILL,
|
| 965 |
if (atS(wx, wy)) blob(atS, STONE_BLOCK, STONE_FILL, STONE_VARS, 2, wx, wy, tx, ty);
|
| 966 |
if (atW(wx, wy)) blob(atW, WATER_BLOCK, WATER_FILL, WATER_VARS, 3, wx, wy, tx, ty);
|
| 967 |
}
|
| 968 |
const live = [];
|
| 969 |
const propSprite = (spec, wx, wy) => {
|
| 970 |
-
const sp = new Sprite(texFrame(ctx.props, spec.c *
|
| 971 |
sp.anchor.set(0.5, 1);
|
| 972 |
-
sp.x = wx *
|
| 973 |
-
sp.y = (wy + 1) *
|
| 974 |
-
sp.zIndex = (wy + 1) *
|
| 975 |
return sp;
|
| 976 |
};
|
| 977 |
-
if (ctx.props) for (let ty = 0; ty <
|
| 978 |
if (!accept(tx, ty)) continue;
|
| 979 |
const wx = x0 + tx, wy = y0 + ty;
|
| 980 |
if (atW(wx, wy)) {
|
|
@@ -997,9 +1408,9 @@ var fpConfig = (seed) => ({
|
|
| 997 |
continue;
|
| 998 |
}
|
| 999 |
const [c, r] = g.tiles[(h >>> 16) % g.tiles.length];
|
| 1000 |
-
const sp = new Sprite(texFrame(ctx.props, c *
|
| 1001 |
-
sp.x = tx *
|
| 1002 |
-
sp.y = ty *
|
| 1003 |
tmp.addChild(sp);
|
| 1004 |
}
|
| 1005 |
const stairAt = (wx, wy) => {
|
|
@@ -1010,15 +1421,15 @@ var fpConfig = (seed) => ({
|
|
| 1010 |
const off = hashU32(seed2 ^ 358940, Math.floor(wx / STAIR_SPACING), 0) % STAIR_SPACING;
|
| 1011 |
return ((wy - off) % STAIR_SPACING + STAIR_SPACING) % STAIR_SPACING === 0;
|
| 1012 |
};
|
| 1013 |
-
bakeCliffs(FP_CLIFF, { chunk:
|
| 1014 |
if (ctx.props) {
|
| 1015 |
const inGrove = (wx, wy) => forestRegion[idx(wx - x0 + M, wy - y0 + M)] === 1;
|
| 1016 |
-
const bx0 = Math.floor(x0 / FOREST_BLOCK_X), bx1 = Math.floor((x0 +
|
| 1017 |
-
const by0 = Math.floor(y0 / FOREST_BLOCK_Y), by1 = Math.floor((y0 +
|
| 1018 |
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
|
| 1019 |
const h = hashU32(seed2 ^ 15748695, bx, by);
|
| 1020 |
const wx = bx * FOREST_BLOCK_X + h % FOREST_BLOCK_X, wy = by * FOREST_BLOCK_Y + (h >>> 4) % FOREST_BLOCK_Y;
|
| 1021 |
-
if (wx < x0 || wx >= x0 +
|
| 1022 |
if (!accept(wx - x0, wy - y0)) continue;
|
| 1023 |
if (!inGrove(wx, wy)) continue;
|
| 1024 |
if ((h >>> 8 & 65535) / 65536 >= FOREST_FILL) continue;
|
|
@@ -1034,8 +1445,8 @@ var fpConfig = (seed) => ({
|
|
| 1034 |
const T = TREES[(h >>> 24) % TREES.length];
|
| 1035 |
const flip = (h >>> 20 & 255) / 256 < TREE_FLIP_RATE;
|
| 1036 |
const sc = TREE_SCALE_BASE * (1 + ((h >>> 12 & 255) / 256 - 0.5) * 2 * TREE_SCALE_JITTER);
|
| 1037 |
-
const px = wx *
|
| 1038 |
-
const py = wy *
|
| 1039 |
const tr = new Sprite(texFrame(ctx.props, ...T.frame));
|
| 1040 |
tr.anchor.set(T.ax, T.ay);
|
| 1041 |
tr.scale.set(flip ? -sc : sc, sc);
|
|
@@ -1055,12 +1466,12 @@ var fpConfig = (seed) => ({
|
|
| 1055 |
}
|
| 1056 |
if (ctx.props) {
|
| 1057 |
const inClump = (wx, wy) => stoneClumpRegion[idx(wx - x0 + M, wy - y0 + M)] === 1;
|
| 1058 |
-
const sbx0 = Math.floor(x0 / STONE_BLOCK_X), sbx1 = Math.floor((x0 +
|
| 1059 |
-
const sby0 = Math.floor(y0 / STONE_BLOCK_Y), sby1 = Math.floor((y0 +
|
| 1060 |
for (let by = sby0; by <= sby1; by++) for (let bx = sbx0; bx <= sbx1; bx++) {
|
| 1061 |
const h = hashU32(seed2 ^ 5702885, bx, by);
|
| 1062 |
const wx = bx * STONE_BLOCK_X + h % STONE_BLOCK_X, wy = by * STONE_BLOCK_Y + (h >>> 4) % STONE_BLOCK_Y;
|
| 1063 |
-
if (wx < x0 || wx >= x0 +
|
| 1064 |
if (!accept(wx - x0, wy - y0)) continue;
|
| 1065 |
if (!inClump(wx, wy)) continue;
|
| 1066 |
if ((h >>> 8 & 65535) / 65536 >= STONE_CLUMP_FILL) continue;
|
|
@@ -1089,14 +1500,14 @@ var fpConfig = (seed) => ({
|
|
| 1089 |
macroColor(seed2, tx, ty) {
|
| 1090 |
const raised = isRaised(seed2, tx, ty);
|
| 1091 |
if (!raised && isRiver(seed2, tx, ty)) return COL_WATER;
|
| 1092 |
-
let c = isStone(seed2, tx, ty) && !isDirt(seed2, tx, ty) ? COL_STONE : isDirt(seed2, tx, ty) ?
|
| 1093 |
if (raised) c = lerp3(c, COL_CLIFF, 0.25);
|
| 1094 |
return c;
|
| 1095 |
},
|
| 1096 |
tileIndexAt(wx, wy, meta) {
|
| 1097 |
if (!meta) return null;
|
| 1098 |
const { waterF, dirt, stone, M, SZ, x0, y0 } = meta;
|
| 1099 |
-
if (wx - x0 < 0 || wy - y0 < 0 || wx - x0 >=
|
| 1100 |
const idx = (i, j) => j * SZ + i;
|
| 1101 |
const pick = (fld, BLOCK, fill) => {
|
| 1102 |
const at = (x, y) => fld[idx(x - x0 + M, y - y0 + M)] === 1;
|
|
@@ -1115,8 +1526,752 @@ var fpConfig = (seed) => ({
|
|
| 1115 |
return GRASS_BASE;
|
| 1116 |
}
|
| 1117 |
});
|
| 1118 |
-
|
| 1119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1120 |
}
|
| 1121 |
|
| 1122 |
// ../auto-battler/src/engine/skills.js
|
|
@@ -2061,10 +3216,10 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 2061 |
const actorOf = (id) => getBattle()?.actors.find((x) => x.id === id);
|
| 2062 |
const floats = [];
|
| 2063 |
const sheetsById = {};
|
| 2064 |
-
|
| 2065 |
if (!def?.idle) {
|
| 2066 |
sheetsById[id] = null;
|
| 2067 |
-
|
| 2068 |
}
|
| 2069 |
try {
|
| 2070 |
const load = (url) => url ? recoloredTexture(Texture, url, def.idle, def.recolor) : null;
|
|
@@ -2090,6 +3245,7 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 2090 |
sheetsById[id] = null;
|
| 2091 |
}
|
| 2092 |
}
|
|
|
|
| 2093 |
const skillIcons = {};
|
| 2094 |
for (const s of CB_SKILLS) {
|
| 2095 |
try {
|
|
@@ -2125,7 +3281,7 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 2125 |
} catch {
|
| 2126 |
}
|
| 2127 |
const skillPlay = {};
|
| 2128 |
-
|
| 2129 |
const cell = sheetsById[id]?.cell;
|
| 2130 |
const map = {};
|
| 2131 |
for (const [skillId, fxCfg] of Object.entries(def.skillFx || {})) {
|
|
@@ -2150,8 +3306,9 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 2150 |
}
|
| 2151 |
skillPlay[id] = map;
|
| 2152 |
}
|
|
|
|
| 2153 |
const view = {};
|
| 2154 |
-
|
| 2155 |
const sh = sheetsById[id];
|
| 2156 |
const c = new Container();
|
| 2157 |
let sp;
|
|
@@ -2200,6 +3357,25 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 2200 |
units.addChild(c);
|
| 2201 |
view[id] = { def, sheets: sh, container: c, sprite: sp, overlay, overlayKey: null, bars, status, statusSprites: {}, outline, castOutline, mode: null, facing: null, flash: 0, dead: false };
|
| 2202 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2203 |
function setLoop(v, mode, facing) {
|
| 2204 |
if (v.mode === mode && v.facing === facing) return;
|
| 2205 |
v.mode = mode;
|
|
@@ -2575,6 +3751,8 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 2575 |
floats,
|
| 2576 |
actorOf,
|
| 2577 |
skillPlay,
|
|
|
|
|
|
|
| 2578 |
setLoop,
|
| 2579 |
playOnce,
|
| 2580 |
resume,
|
|
@@ -2709,11 +3887,21 @@ function makeActor(unit, team, id, slot) {
|
|
| 2709 |
// optional: idle until a foe is within this distance (else always engage)
|
| 2710 |
};
|
| 2711 |
}
|
| 2712 |
-
function makeTeamBattle({ seed = 1, players = [], enemies = [], sandbox = false, respawnDummies = 0, freeCast = false, world = null } = {}) {
|
| 2713 |
const actors = [];
|
| 2714 |
players.filter(Boolean).slice(0, 5).forEach((u, i) => actors.push(makeActor(u, "player", `P${i}`, i)));
|
| 2715 |
enemies.filter(Boolean).slice(0, 5).forEach((u, i) => actors.push(makeActor(u, "enemy", `E${i}`, i)));
|
| 2716 |
-
return { t: 0, rng: makeRng(seed), actors, projectiles: [], log: [], over: false, winner: null, sandbox, respawnDummies, freeCast, input: {}, world };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2717 |
}
|
| 2718 |
function setInput(b, id, cmd) {
|
| 2719 |
if (!b.input) b.input = {};
|
|
@@ -2728,12 +3916,12 @@ var CONTACT_SLOP = 2;
|
|
| 2728 |
var MAX_BATTLE_T = 90;
|
| 2729 |
var COLLISION_Y_WEIGHT = 3.2;
|
| 2730 |
var radiusOf = (unit, tpl) => unit.template?.radius ?? unit.stats?.radius ?? tpl.radius ?? BODY_RADIUS[tpl.role] ?? DEFAULT_RADIUS;
|
| 2731 |
-
var edgeGap = (a, t) =>
|
| 2732 |
var MELEE_REACH = 2;
|
| 2733 |
var reachOf = (a) => a.role === "ranged" ? a.weapon.range : MELEE_REACH;
|
| 2734 |
var SPELL_RANGE = 900;
|
| 2735 |
var ARMOR_IGNORING = /* @__PURE__ */ new Set(["shadow", "holy", "dark", "chaos"]);
|
| 2736 |
-
var
|
| 2737 |
var hasCond = (a, type) => a.conds.some((c) => c.type === type);
|
| 2738 |
var isKd = (b, a) => a.kd > b.t;
|
| 2739 |
var gainAdr = (a, n) => {
|
|
@@ -2744,7 +3932,7 @@ var alliesOf = (b, a) => b.actors.filter((x) => x.alive && x.team === a.team);
|
|
| 2744 |
function nearestFoe(b, a) {
|
| 2745 |
let best = null, bd = Infinity;
|
| 2746 |
for (const x of livingFoes(b, a)) {
|
| 2747 |
-
const d =
|
| 2748 |
if (d < bd) {
|
| 2749 |
bd = d;
|
| 2750 |
best = x;
|
|
@@ -2764,7 +3952,7 @@ function mostWoundedAlly(b, a, includeSelf = true) {
|
|
| 2764 |
}
|
| 2765 |
return best;
|
| 2766 |
}
|
| 2767 |
-
var adjacentTo = (b, tgt) => b.actors.filter((x) => x.alive && x.team === tgt.team && x !== tgt &&
|
| 2768 |
var addMod = (b, a, m) => a.mods.push({ until: b.t + (m.dur ?? 0), ...m });
|
| 2769 |
var activeMods = (b, a, kind) => a.mods.filter((m) => m.kind === kind && m.until > b.t);
|
| 2770 |
var hasModKind = (b, a, kind) => a.mods.some((m) => m.kind === kind && m.until > b.t);
|
|
@@ -3006,12 +4194,13 @@ function applyEffect(b, src, tgt, e, delivery = "spell", s = null) {
|
|
| 3006 |
}
|
| 3007 |
}
|
| 3008 |
function shadowStep(b, a, tgt) {
|
|
|
|
| 3009 |
const dx = a.x - tgt.x, dy = a.y - tgt.y, len = Math.hypot(dx, dy) || 1;
|
| 3010 |
-
a.x = Math.max(0, Math.min(
|
| 3011 |
-
a.y = Math.max(0, Math.min(
|
| 3012 |
}
|
| 3013 |
function fireSpellProjectiles(b, src, tgt, amt, e, n) {
|
| 3014 |
-
const base =
|
| 3015 |
for (let i = 0; i < n; i++) {
|
| 3016 |
b.projectiles.push({
|
| 3017 |
srcId: src.id,
|
|
@@ -3088,7 +4277,7 @@ function strike(b, a, enemy, s) {
|
|
| 3088 |
}
|
| 3089 |
}
|
| 3090 |
if (a.role === "ranged") {
|
| 3091 |
-
const flight =
|
| 3092 |
b.projectiles.push({ srcId: a.id, tgtId: enemy.id, fromX: a.x, fromY: a.y, aimX: enemy.x, aimY: enemy.y, bornT: b.t, hitT: b.t + flight, weaponDmg, bonus, s, empEffect });
|
| 3093 |
log(b, "shoot", a, { name: s ? s.name : "shot", skillId: s?.id });
|
| 3094 |
} else {
|
|
@@ -3201,7 +4390,7 @@ function usable(b, a, s, tgt, foe, free = false) {
|
|
| 3201 |
if (s.cost?.energy && a.energy < s.cost.energy) return false;
|
| 3202 |
if (s.cost?.adrenaline && a.adrenaline < s.cost.adrenaline) return false;
|
| 3203 |
if (isAttack(s) && edgeGap(a, foe) > reachOf(a)) return false;
|
| 3204 |
-
if (!isAttack(s) && !isSupport(s) &&
|
| 3205 |
for (const r of s.requires || []) {
|
| 3206 |
if (r === "on_hit") continue;
|
| 3207 |
if (r.combo_follows && a.marks[foe.id]?.stage !== r.combo_follows) return false;
|
|
@@ -3235,7 +4424,7 @@ function chooseAction(b, a, foe) {
|
|
| 3235 |
return null;
|
| 3236 |
}
|
| 3237 |
function moveActor(b, a, enemy, dt) {
|
| 3238 |
-
const d =
|
| 3239 |
let toward = 0;
|
| 3240 |
if (a.role === "ranged") {
|
| 3241 |
if (d < (a.preferredRange ?? a.weapon.range * 0.7)) toward = -1;
|
|
@@ -3309,9 +4498,10 @@ function timeToHit(px, py, rvx, rvy, R) {
|
|
| 3309 |
}
|
| 3310 |
var clampField = (v, r, max) => Math.max(r, Math.min(max - r, v));
|
| 3311 |
function stepMove(b, a, vx, vy, dt) {
|
|
|
|
| 3312 |
const w = b.world && b.world.walkable;
|
| 3313 |
-
let nx = clampField(a.x + vx * dt, a.radius,
|
| 3314 |
-
let ny = clampField(a.y + vy * dt, a.radius,
|
| 3315 |
if (w) {
|
| 3316 |
if (!w(nx, a.y)) nx = a.x;
|
| 3317 |
if (!w(nx, ny)) ny = a.y;
|
|
@@ -3320,6 +4510,7 @@ function stepMove(b, a, vx, vy, dt) {
|
|
| 3320 |
a.y = ny;
|
| 3321 |
}
|
| 3322 |
function resolveOverlaps(b) {
|
|
|
|
| 3323 |
const live = b.actors.filter((a) => a.alive);
|
| 3324 |
for (let it = 0; it < DEOVERLAP_ITERS; it++) {
|
| 3325 |
for (let i = 0; i < live.length; i++) {
|
|
@@ -3335,10 +4526,10 @@ function resolveOverlaps(b) {
|
|
| 3335 |
const aShare = aFix ? 0 : oFix ? 1 : 0.5;
|
| 3336 |
const oShare = oFix ? 0 : aFix ? 1 : 0.5;
|
| 3337 |
const yPush = uy / COLLISION_Y_WEIGHT;
|
| 3338 |
-
a.x = clampField(a.x - ux * push * aShare, a.radius,
|
| 3339 |
-
a.y = clampField(a.y - yPush * push * aShare, a.radius,
|
| 3340 |
-
o.x = clampField(o.x + ux * push * oShare, o.radius,
|
| 3341 |
-
o.y = clampField(o.y + yPush * push * oShare, o.radius,
|
| 3342 |
}
|
| 3343 |
}
|
| 3344 |
}
|
|
@@ -3439,7 +4630,7 @@ function step(b, dt) {
|
|
| 3439 |
if (!a.alive || b.over) continue;
|
| 3440 |
const enemy = nearestFoe(b, a);
|
| 3441 |
if (!enemy && a.control !== "player") continue;
|
| 3442 |
-
if (a.aggroRadius != null && a.control !== "player" &&
|
| 3443 |
a.moving = false;
|
| 3444 |
continue;
|
| 3445 |
}
|
|
@@ -3498,14 +4689,26 @@ function step(b, dt) {
|
|
| 3498 |
}
|
| 3499 |
|
| 3500 |
// ../auto-battler/src/sim/walkable.js
|
| 3501 |
-
function
|
| 3502 |
-
return (wx, wy) =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3503 |
}
|
| 3504 |
|
| 3505 |
// ../auto-battler/src/render/comboBattler.js
|
| 3506 |
-
var
|
| 3507 |
var SPRITE_TILES = 1.9;
|
| 3508 |
var STEP = 0.05;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3509 |
async function contentHeight(url) {
|
| 3510 |
try {
|
| 3511 |
const img = await new Promise((res, rej) => {
|
|
@@ -3535,10 +4738,20 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 3535 |
const { Graphics, Container } = pixi;
|
| 3536 |
const seed = (opts.seed ?? 1) >>> 0;
|
| 3537 |
const G = opts.groundScale ?? 0.3;
|
| 3538 |
-
const
|
| 3539 |
-
const
|
| 3540 |
-
const
|
| 3541 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3542 |
const depthScale = { v: 2 };
|
| 3543 |
let offTick = null, keyHandlers = null, tapHandlers = null, controlsEl = null, alive = true;
|
| 3544 |
const keys = { x: 0, y: 0 };
|
|
@@ -3549,10 +4762,14 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 3549 |
const listeners = /* @__PURE__ */ new Set();
|
| 3550 |
const pa = () => battle?.actors.find((a) => a.id === "P0") || null;
|
| 3551 |
const ea = () => battle?.actors.filter((a) => a.team === "enemy") || [];
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3552 |
const snap = () => ({
|
| 3553 |
-
player: pa() ? { x: pa().x, y: pa().y, hp: pa().hp,
|
| 3554 |
-
enemies: ea().map((a) => ({ x: a.x, y: a.y, hp: Math.round(a.hp), alive: a.alive,
|
| 3555 |
-
over: !!battle?.over
|
| 3556 |
});
|
| 3557 |
const emit = () => listeners.forEach((fn) => fn(snap()));
|
| 3558 |
const KEYMAP = { w: [0, -1], s: [0, 1], a: [-1, 0], d: [1, 0], arrowup: [0, -1], arrowdown: [0, 1], arrowleft: [-1, 0], arrowright: [1, 0] };
|
|
@@ -3613,7 +4830,7 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 3613 |
downAt = null;
|
| 3614 |
if (moved > 8 || elapsed > 500) return;
|
| 3615 |
const r = canvas.getBoundingClientRect(), w = map.screenToWorld(e.clientX - r.left, e.clientY - r.top);
|
| 3616 |
-
moveTarget =
|
| 3617 |
};
|
| 3618 |
canvas.addEventListener("pointerdown", onDown);
|
| 3619 |
window.addEventListener("pointerup", onUp);
|
|
@@ -3646,12 +4863,12 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 3646 |
host.appendChild(bar);
|
| 3647 |
controlsEl = bar;
|
| 3648 |
}
|
| 3649 |
-
const
|
| 3650 |
const nearestFoeDist = () => {
|
| 3651 |
const p = pa();
|
| 3652 |
if (!p) return Infinity;
|
| 3653 |
let m = Infinity;
|
| 3654 |
-
for (const e of ea()) if (e.alive) m = Math.min(m,
|
| 3655 |
return m;
|
| 3656 |
};
|
| 3657 |
function tick(ticker) {
|
|
@@ -3682,11 +4899,16 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 3682 |
step(battle, STEP);
|
| 3683 |
acc.t -= STEP;
|
| 3684 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3685 |
R.syncActors(battle, dtMS, battle.t);
|
| 3686 |
R.updateFloats(dtMS);
|
| 3687 |
R.drawProjectiles(battle);
|
| 3688 |
R.processLog(battle, cursor);
|
| 3689 |
drawRings();
|
|
|
|
| 3690 |
emit();
|
| 3691 |
}
|
| 3692 |
function drawRings() {
|
|
@@ -3695,42 +4917,53 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 3695 |
if (!p) return;
|
| 3696 |
rings.ellipse(p.x, p.y, p.weapon.range, p.weapon.range * 0.6).stroke({ width: 1.5 / G, color: 16777215, alpha: 0.25 });
|
| 3697 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3698 |
const ready = (async () => {
|
| 3699 |
await map.ready;
|
| 3700 |
if (!alive) return;
|
| 3701 |
const cam = map.getCamera();
|
| 3702 |
-
const
|
| 3703 |
-
|
| 3704 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3705 |
};
|
| 3706 |
-
const p0 = snapTile(Math.floor(cam.x / TILE2), Math.floor(cam.y / TILE2));
|
| 3707 |
-
const toField = (tile) => ({ x: (tile.x + 0.5) * TILE2 / G, y: (tile.y + 1) * TILE2 / G });
|
| 3708 |
const pProf = opts.player?.unit?.profession;
|
| 3709 |
const pSkills = opts.player?.unit?.skills?.length ? opts.player.unit.skills : CB_SKILLS.filter((s) => s.profession === pProf).slice(0, 3).map((s) => s.id);
|
| 3710 |
const players = [{ ...opts.player?.unit || {}, name: opts.player?.name || "Hero", control: "player", skills: pSkills }];
|
| 3711 |
-
|
| 3712 |
-
|
| 3713 |
-
const
|
| 3714 |
const P = pa();
|
| 3715 |
if (P) {
|
| 3716 |
-
P.x =
|
| 3717 |
-
P.y =
|
| 3718 |
-
}
|
| 3719 |
-
ea().forEach((a, i) => {
|
| 3720 |
-
const ring = 7 + i % 3 * 3, ang = i / Math.max(1, ea().length) * Math.PI * 2;
|
| 3721 |
-
const t = snapTile(p0.x + Math.round(Math.cos(ang) * ring), p0.y + Math.round(Math.sin(ang) * ring));
|
| 3722 |
-
const f = toField(t);
|
| 3723 |
-
a.x = f.x;
|
| 3724 |
-
a.y = f.y;
|
| 3725 |
-
});
|
| 3726 |
const defsById = {};
|
| 3727 |
const sd = (o) => ({ idle: o?.idle, walk: o?.walk, attack: o?.attack, dmg: o?.dmg, die: o?.die });
|
| 3728 |
defsById.P0 = { name: players[0].name, profession: players[0].profession, ...sd(opts.player?.sheets) };
|
| 3729 |
-
ea().forEach((a, i) => {
|
| 3730 |
-
defsById[a.id] = { name: a.name, ...sd((opts.enemies || [])[i]?.sheets) };
|
| 3731 |
-
});
|
| 3732 |
combatRoot = new Container();
|
| 3733 |
combatRoot.scale.set(G);
|
|
|
|
| 3734 |
rings = new Graphics();
|
| 3735 |
combatRoot.addChild(rings);
|
| 3736 |
const units = new Container();
|
|
@@ -3739,11 +4972,59 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 3739 |
const proj = new Graphics();
|
| 3740 |
combatRoot.addChild(units, proj, fx);
|
| 3741 |
map.getEntityLayer().addChild(combatRoot);
|
|
|
|
|
|
|
| 3742 |
const ch = await contentHeight(opts.player?.sheets?.idle || opts.player?.sheets?.walk);
|
| 3743 |
-
depthScale.v = SPRITE_TILES *
|
| 3744 |
R = await createCombatRenderer({ pixi, defsById, layers: { units, fx, projLayer: proj }, coords: { mapX: (x) => x, mapY: (y) => y, depthOf: () => depthScale.v }, getBattle: () => battle });
|
| 3745 |
if (!alive) return;
|
| 3746 |
for (const a of battle.actors) a.attackTimer = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3747 |
bindKeys();
|
| 3748 |
bindTap();
|
| 3749 |
buildControls();
|
|
@@ -3781,6 +5062,10 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 3781 |
controlsEl?.remove();
|
| 3782 |
} catch {
|
| 3783 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3784 |
try {
|
| 3785 |
map.destroy();
|
| 3786 |
} catch {
|
|
@@ -3790,7 +5075,7 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 3790 |
combatRoot = null;
|
| 3791 |
listeners.clear();
|
| 3792 |
}
|
| 3793 |
-
const ctrl = { ready, getSnapshot, resize, onChange, destroy, map, walkable: (wx, wy) =>
|
| 3794 |
if (typeof window !== "undefined") {
|
| 3795 |
window.__comboSnap = () => ctrl.getSnapshot();
|
| 3796 |
window.__combo = ctrl;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
// ../auto-battler/src/render/chunkedMap.js
|
| 2 |
function createChunkedMap(pixi, host, config) {
|
| 3 |
const { Application, Assets, Sprite, Container, Texture, Rectangle, RenderTexture } = pixi;
|
| 4 |
+
const TILE7 = config.tile ?? 8;
|
| 5 |
+
const CHUNK5 = config.chunk ?? 32;
|
| 6 |
+
const CHUNKPX = CHUNK5 * TILE7;
|
| 7 |
const BG = config.background ?? "#69636f";
|
| 8 |
const Z_DEFAULT = config.zoomDefault ?? 1;
|
| 9 |
const Z_MIN = config.zoomMin ?? 1 / 32;
|
|
|
|
| 16 |
const MACRO_LEVEL_MAX = config.macroLevelMax ?? 6;
|
| 17 |
const DETAIL_BUDGET = config.detailBudget ?? 1;
|
| 18 |
const MACRO_BUDGET = config.macroBudget ?? 8;
|
| 19 |
+
const bounds = config.bounds ? {
|
| 20 |
+
x0: config.bounds.x0 * TILE7,
|
| 21 |
+
y0: config.bounds.y0 * TILE7,
|
| 22 |
+
x1: config.bounds.x1 * TILE7,
|
| 23 |
+
y1: config.bounds.y1 * TILE7,
|
| 24 |
+
tx0: config.bounds.x0,
|
| 25 |
+
ty0: config.bounds.y0,
|
| 26 |
+
tx1: config.bounds.x1,
|
| 27 |
+
ty1: config.bounds.y1
|
| 28 |
+
} : null;
|
| 29 |
let app = null, root = null, genRoot = null, macroRoot = null, shadowLayer = null, propLayer = null;
|
| 30 |
let ctx = null;
|
| 31 |
let alive = true;
|
|
|
|
| 33 |
let seed = config.seed ?? 1;
|
| 34 |
let zoom = Z_DEFAULT;
|
| 35 |
let cameraDirty = true, genPending = false, lastEmitTile = null;
|
| 36 |
+
const camera = { x: config.initialCamera?.x ?? CHUNKPX / 2, y: config.initialCamera?.y ?? CHUNKPX / 2 };
|
| 37 |
const chunks = /* @__PURE__ */ new Map();
|
| 38 |
const macroChunks = /* @__PURE__ */ new Map();
|
| 39 |
const fading = /* @__PURE__ */ new Set();
|
|
|
|
| 46 |
const pinch = { on: false, dist: 0 };
|
| 47 |
const handlers = {};
|
| 48 |
const emit = () => listeners.forEach((fn) => fn(getSnapshot()));
|
| 49 |
+
const getSnapshot = () => ({ seed, zoom, cx: Math.round(camera.x / TILE7), cy: Math.round(camera.y / TILE7), chunks: chunks.size });
|
| 50 |
function tex(source, c, r) {
|
| 51 |
const k = source.uid + ":" + c + "," + r;
|
| 52 |
let t = texCache.get(k);
|
| 53 |
if (!t) {
|
| 54 |
+
t = new Texture({ source, frame: new Rectangle(c * TILE7, r * TILE7, TILE7, TILE7) });
|
| 55 |
texCache.set(k, t);
|
| 56 |
}
|
| 57 |
return t;
|
|
|
|
| 66 |
return t;
|
| 67 |
}
|
| 68 |
function makeChunk(cx, cy) {
|
| 69 |
+
const x0 = cx * CHUNK5, y0 = cy * CHUNK5;
|
| 70 |
const tmp = new Container();
|
| 71 |
const add = (source, c, r, tx, ty) => {
|
| 72 |
const sp = new Sprite(tex(source, c, r));
|
| 73 |
+
sp.x = tx * TILE7;
|
| 74 |
+
sp.y = ty * TILE7;
|
| 75 |
tmp.addChild(sp);
|
| 76 |
return sp;
|
| 77 |
};
|
| 78 |
+
const res = config.bake({ cx, cy, x0, y0, seed, chunk: CHUNK5, tile: TILE7, ctx, tmp, app, Sprite, Texture, Rectangle, tex, texFrame, add }) || {};
|
| 79 |
const rt = RenderTexture.create({ width: CHUNKPX, height: CHUNKPX, autoGenerateMipmaps: true, scaleMode: "nearest" });
|
| 80 |
rt.source.minFilter = "linear";
|
| 81 |
rt.source.mipmapFilter = "linear";
|
|
|
|
| 83 |
rt.source.updateMipmaps();
|
| 84 |
tmp.destroy({ children: true });
|
| 85 |
const sprite = new Sprite(rt);
|
| 86 |
+
sprite.x = x0 * TILE7;
|
| 87 |
+
sprite.y = y0 * TILE7;
|
| 88 |
return { sprite, rt, meta: res.meta ?? null, live: res.live ?? null };
|
| 89 |
}
|
| 90 |
function chooseMacroLevel(z) {
|
| 91 |
+
return Math.max(0, Math.min(MACRO_LEVEL_MAX, Math.round(Math.log2(MACRO_TEXEL_PX / (TILE7 * z)))));
|
| 92 |
}
|
| 93 |
function makeMacroChunk(L, mcx, mcy) {
|
| 94 |
const step2 = 1 << L, t0x = mcx * MCHUNK * step2, t0y = mcy * MCHUNK * step2;
|
|
|
|
| 109 |
const t = Texture.from(cv);
|
| 110 |
t.source.scaleMode = "linear";
|
| 111 |
const sprite = new Sprite(t);
|
| 112 |
+
sprite.x = t0x * TILE7;
|
| 113 |
+
sprite.y = t0y * TILE7;
|
| 114 |
+
sprite.width = sprite.height = MCHUNK * step2 * TILE7;
|
| 115 |
return { sprite, tex: t };
|
| 116 |
}
|
| 117 |
+
function coverZoom() {
|
| 118 |
+
if (!bounds || !app) return Z_MIN;
|
| 119 |
+
return Math.max(app.screen.width / (bounds.x1 - bounds.x0), app.screen.height / (bounds.y1 - bounds.y0));
|
| 120 |
+
}
|
| 121 |
+
function clampCamera() {
|
| 122 |
+
if (!bounds || !app) return;
|
| 123 |
+
const hw = app.screen.width / 2 / zoom, hh = app.screen.height / 2 / zoom;
|
| 124 |
+
const loX = bounds.x0 + hw, hiX = bounds.x1 - hw, loY = bounds.y0 + hh, hiY = bounds.y1 - hh;
|
| 125 |
+
camera.x = loX <= hiX ? Math.min(hiX, Math.max(loX, camera.x)) : (bounds.x0 + bounds.x1) / 2;
|
| 126 |
+
camera.y = loY <= hiY ? Math.min(hiY, Math.max(loY, camera.y)) : (bounds.y0 + bounds.y1) / 2;
|
| 127 |
+
}
|
| 128 |
function reconcile() {
|
| 129 |
if (!genRoot || !ctx || !app) return;
|
| 130 |
+
if (bounds) zoom = Math.max(zoom, coverZoom());
|
| 131 |
+
clampCamera();
|
| 132 |
const sx = Math.round(app.screen.width / 2 - camera.x * zoom);
|
| 133 |
const sy = Math.round(app.screen.height / 2 - camera.y * zoom);
|
| 134 |
for (const L of [macroRoot, genRoot, shadowLayer, propLayer]) {
|
|
|
|
| 165 |
}
|
| 166 |
function reconcileDetail() {
|
| 167 |
const halfW = app.screen.width / 2 / zoom, halfH = app.screen.height / 2 / zoom;
|
| 168 |
+
let c0 = Math.floor((camera.x - halfW) / CHUNKPX) - 1, c1 = Math.floor((camera.x + halfW) / CHUNKPX) + 1;
|
| 169 |
+
let r0 = Math.floor((camera.y - halfH) / CHUNKPX) - 1, r1 = Math.floor((camera.y + halfH) / CHUNKPX) + 1;
|
| 170 |
+
if (bounds) {
|
| 171 |
+
c0 = Math.max(c0, Math.floor(bounds.tx0 / CHUNK5));
|
| 172 |
+
c1 = Math.min(c1, Math.floor((bounds.tx1 - 1) / CHUNK5));
|
| 173 |
+
r0 = Math.max(r0, Math.floor(bounds.ty0 / CHUNK5));
|
| 174 |
+
r1 = Math.min(r1, Math.floor((bounds.ty1 - 1) / CHUNK5));
|
| 175 |
+
}
|
| 176 |
for (const [key, ch] of chunks) {
|
| 177 |
const [cx, cy] = key.split(",").map(Number);
|
| 178 |
if (cx < c0 - 1 || cx > c1 + 1 || cy < r0 - 1 || cy > r1 + 1) evictChunk(key, ch);
|
|
|
|
| 198 |
return missing.length > DETAIL_BUDGET;
|
| 199 |
}
|
| 200 |
function reconcileMacro(L) {
|
| 201 |
+
const px = MCHUNK * (1 << L) * TILE7;
|
| 202 |
const halfW = app.screen.width / 2 / zoom, halfH = app.screen.height / 2 / zoom;
|
| 203 |
+
let c0 = Math.floor((camera.x - halfW) / px) - 1, c1 = Math.floor((camera.x + halfW) / px) + 1;
|
| 204 |
+
let r0 = Math.floor((camera.y - halfH) / px) - 1, r1 = Math.floor((camera.y + halfH) / px) + 1;
|
| 205 |
+
if (bounds) {
|
| 206 |
+
const mt = MCHUNK * (1 << L);
|
| 207 |
+
c0 = Math.max(c0, Math.floor(bounds.tx0 / mt));
|
| 208 |
+
c1 = Math.min(c1, Math.floor((bounds.tx1 - 1) / mt));
|
| 209 |
+
r0 = Math.max(r0, Math.floor(bounds.ty0 / mt));
|
| 210 |
+
r1 = Math.min(r1, Math.floor((bounds.ty1 - 1) / mt));
|
| 211 |
+
}
|
| 212 |
for (const [key, mc] of macroChunks) {
|
| 213 |
const [ml, mx, my] = key.split(",").map(Number);
|
| 214 |
if (ml !== L || mx < c0 - 1 || mx > c1 + 1 || my < r0 - 1 || my > r1 + 1) {
|
|
|
|
| 245 |
}
|
| 246 |
function zoomAt(factor, lx, ly) {
|
| 247 |
if (!app) return;
|
| 248 |
+
const nz = Math.min(Z_MAX, Math.max(bounds ? coverZoom() : Z_MIN, zoom * factor));
|
| 249 |
if (nz === zoom) return;
|
| 250 |
const sw = app.screen.width, sh = app.screen.height;
|
| 251 |
const wx = camera.x + (lx - sw / 2) / zoom, wy = camera.y + (ly - sh / 2) / zoom;
|
|
|
|
| 370 |
if (cameraDirty || genPending) {
|
| 371 |
reconcile();
|
| 372 |
cameraDirty = false;
|
| 373 |
+
const tk = Math.round(camera.x / TILE7) + "," + Math.round(camera.y / TILE7) + "," + zoom.toFixed(3);
|
| 374 |
if (tk !== lastEmitTile) {
|
| 375 |
lastEmitTile = tk;
|
| 376 |
emit();
|
|
|
|
| 431 |
}
|
| 432 |
function tileIndexAt(wx, wy) {
|
| 433 |
if (!config.tileIndexAt) return null;
|
| 434 |
+
const cx = Math.floor(wx / CHUNK5), cy = Math.floor(wy / CHUNK5);
|
| 435 |
const ch = chunks.get(cx + "," + cy);
|
| 436 |
if (!ch) return null;
|
| 437 |
return config.tileIndexAt(wx, wy, ch.meta);
|
| 438 |
}
|
| 439 |
+
function biomeAt(wx, wy) {
|
| 440 |
+
return config.biomeAt ? config.biomeAt(seed, wx, wy) : null;
|
| 441 |
+
}
|
| 442 |
+
function getBounds() {
|
| 443 |
+
return bounds ? { x0: bounds.x0, y0: bounds.y0, x1: bounds.x1, y1: bounds.y1 } : null;
|
| 444 |
+
}
|
| 445 |
function setEnabled(v) {
|
| 446 |
enabled = v;
|
| 447 |
if (v) cameraDirty = true;
|
|
|
|
| 490 |
ctx = null;
|
| 491 |
texCache.clear();
|
| 492 |
}
|
| 493 |
+
return { ready, regenerate, destroy, onChange, getSnapshot, getCamera, tileIndexAt, biomeAt, getBounds, zoomBy, setEnabled, onTick, getEntityLayer, getApp, screenToWorld, worldToScreen, tile: TILE7 };
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
// ../auto-battler/src/engine/rng.js
|
| 497 |
+
function makeRng(seed) {
|
| 498 |
+
let a = seed >>> 0;
|
| 499 |
+
return function rng() {
|
| 500 |
+
a |= 0;
|
| 501 |
+
a = a + 1831565813 | 0;
|
| 502 |
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
| 503 |
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
| 504 |
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
| 505 |
+
};
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
// ../auto-battler/src/engine/worldgen.js
|
| 509 |
+
function hash2(seed, x, y) {
|
| 510 |
+
let h = Math.imul(x | 0, 374761393) + Math.imul(y | 0, 668265263) + Math.imul(seed | 0, 2654435761);
|
| 511 |
+
h = Math.imul(h ^ h >>> 13, 1274126177);
|
| 512 |
+
h ^= h >>> 16;
|
| 513 |
+
return (h >>> 0) / 4294967296;
|
| 514 |
+
}
|
| 515 |
+
var smooth = (t) => t * t * (3 - 2 * t);
|
| 516 |
+
var lerp = (a, b, t) => a + (b - a) * t;
|
| 517 |
+
function valueNoise(seed, x, y) {
|
| 518 |
+
const xi = Math.floor(x), yi = Math.floor(y);
|
| 519 |
+
const xf = x - xi, yf = y - yi;
|
| 520 |
+
const v00 = hash2(seed, xi, yi), v10 = hash2(seed, xi + 1, yi);
|
| 521 |
+
const v01 = hash2(seed, xi, yi + 1), v11 = hash2(seed, xi + 1, yi + 1);
|
| 522 |
+
const u = smooth(xf), v = smooth(yf);
|
| 523 |
+
return lerp(lerp(v00, v10, u), lerp(v01, v11, u), v);
|
| 524 |
+
}
|
| 525 |
+
function fbm(seed, x, y, octaves = 5, lacunarity = 2, gain = 0.5) {
|
| 526 |
+
let amp = 1, freq = 1, sum = 0, norm = 0;
|
| 527 |
+
for (let o = 0; o < octaves; o++) {
|
| 528 |
+
sum += amp * valueNoise(seed + o * 1013, x * freq, y * freq);
|
| 529 |
+
norm += amp;
|
| 530 |
+
amp *= gain;
|
| 531 |
+
freq *= lacunarity;
|
| 532 |
+
}
|
| 533 |
+
return sum / norm;
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
// ../auto-battler/src/engine/biomeMap.js
|
| 537 |
+
var sub = (seed, salt) => (Math.imul(seed | 0, 2654435761) ^ (salt | 0)) >>> 0;
|
| 538 |
+
var SCALE = 5e-3;
|
| 539 |
+
var REGION_OCTAVES = 3;
|
| 540 |
+
var WARP_SCALE = 0.012;
|
| 541 |
+
var WARP_AMP = 40;
|
| 542 |
+
var BIOMES = [
|
| 543 |
+
{ id: "forgottenPlains", salt: 985505 },
|
| 544 |
+
// green meadow
|
| 545 |
+
{ id: "orc", salt: 11281 },
|
| 546 |
+
// dirt / grass plains
|
| 547 |
+
{ id: "necropolis", salt: 966869 }
|
| 548 |
+
// corrupted swamp
|
| 549 |
+
];
|
| 550 |
+
var OVERWORLD_BIOMES = BIOMES.map((b) => b.id);
|
| 551 |
+
function weights(seed, x, y, out) {
|
| 552 |
+
const wx = x + WARP_AMP * (fbm(sub(seed, 1441), x * WARP_SCALE, y * WARP_SCALE) - 0.5);
|
| 553 |
+
const wy = y + WARP_AMP * (fbm(sub(seed, 1442), x * WARP_SCALE, y * WARP_SCALE) - 0.5);
|
| 554 |
+
for (let i = 0; i < BIOMES.length; i++) out[i] = fbm(sub(seed, BIOMES[i].salt), wx * SCALE, wy * SCALE, REGION_OCTAVES);
|
| 555 |
+
return out;
|
| 556 |
+
}
|
| 557 |
+
var _w = new Array(BIOMES.length);
|
| 558 |
+
function biomeRegion(seed, x, y) {
|
| 559 |
+
const w = weights(seed, x, y, _w);
|
| 560 |
+
let i1 = 0;
|
| 561 |
+
for (let i = 1; i < w.length; i++) if (w[i] > w[i1]) i1 = i;
|
| 562 |
+
let i2 = -1;
|
| 563 |
+
for (let i = 0; i < w.length; i++) {
|
| 564 |
+
if (i === i1) continue;
|
| 565 |
+
if (i2 < 0 || w[i] > w[i2]) i2 = i;
|
| 566 |
+
}
|
| 567 |
+
return { id: BIOMES[i1].id, id2: BIOMES[i2]?.id ?? BIOMES[i1].id, edge: w[i1] - w[i2] };
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
// ../auto-battler/src/engine/transitionStencils.js
|
| 571 |
+
var TILE = 8;
|
| 572 |
+
var MID = 3.5;
|
| 573 |
+
var BAND = 2.6;
|
| 574 |
+
var WAVE_AMP = 1.35;
|
| 575 |
+
var WAVE_FREQ = 0.45;
|
| 576 |
+
var R_OUTER = 4.6;
|
| 577 |
+
var R_CONCAVE = 2.3;
|
| 578 |
+
var R_TIP = 2.6;
|
| 579 |
+
var BAYER4 = [
|
| 580 |
+
0.5 / 16,
|
| 581 |
+
8.5 / 16,
|
| 582 |
+
2.5 / 16,
|
| 583 |
+
10.5 / 16,
|
| 584 |
+
12.5 / 16,
|
| 585 |
+
4.5 / 16,
|
| 586 |
+
14.5 / 16,
|
| 587 |
+
6.5 / 16,
|
| 588 |
+
3.5 / 16,
|
| 589 |
+
11.5 / 16,
|
| 590 |
+
1.5 / 16,
|
| 591 |
+
9.5 / 16,
|
| 592 |
+
15.5 / 16,
|
| 593 |
+
7.5 / 16,
|
| 594 |
+
13.5 / 16,
|
| 595 |
+
5.5 / 16
|
| 596 |
+
];
|
| 597 |
+
var bayer = (x, y) => BAYER4[(y & 3) * 4 + (x & 3)];
|
| 598 |
+
function wave1d(t, salt) {
|
| 599 |
+
const i = Math.floor(t * WAVE_FREQ), f = t * WAVE_FREQ - i;
|
| 600 |
+
const a = hash2(salt, i, 0), b = hash2(salt, i + 1, 0);
|
| 601 |
+
const u = f * f * (3 - 2 * f);
|
| 602 |
+
return (a + (b - a) * u) * 2 - 1;
|
| 603 |
+
}
|
| 604 |
+
var CASES = {
|
| 605 |
+
// cardinal edges — wavy line; fg on the side away from the foreign neighbour.
|
| 606 |
+
N: (x, y, s) => y - (MID + WAVE_AMP * wave1d(x, s)),
|
| 607 |
+
S: (x, y, s) => MID + WAVE_AMP * wave1d(x, s) - y,
|
| 608 |
+
E: (x, y, s) => MID + WAVE_AMP * wave1d(y, s) - x,
|
| 609 |
+
W: (x, y, s) => x - (MID + WAVE_AMP * wave1d(y, s)),
|
| 610 |
+
// outer corners — bg is a quarter-disc in the corner (2 adjacent foreign cardinals).
|
| 611 |
+
oNE: (x, y, s) => dist(x, y, 7, 0) - (R_OUTER + WAVE_AMP * wave1d(x + y, s)),
|
| 612 |
+
oNW: (x, y, s) => dist(x, y, 0, 0) - (R_OUTER + WAVE_AMP * wave1d(x + y, s)),
|
| 613 |
+
oSE: (x, y, s) => dist(x, y, 7, 7) - (R_OUTER + WAVE_AMP * wave1d(x + y, s)),
|
| 614 |
+
oSW: (x, y, s) => dist(x, y, 0, 7) - (R_OUTER + WAVE_AMP * wave1d(x + y, s)),
|
| 615 |
+
// concave corners — small bg notch in the corner (1 foreign diagonal only).
|
| 616 |
+
cNE: (x, y, s) => dist(x, y, 7, 0) - (R_CONCAVE + 0.6 * wave1d(x + y, s)),
|
| 617 |
+
cNW: (x, y, s) => dist(x, y, 0, 0) - (R_CONCAVE + 0.6 * wave1d(x + y, s)),
|
| 618 |
+
cSE: (x, y, s) => dist(x, y, 7, 7) - (R_CONCAVE + 0.6 * wave1d(x + y, s)),
|
| 619 |
+
cSW: (x, y, s) => dist(x, y, 0, 7) - (R_CONCAVE + 0.6 * wave1d(x + y, s)),
|
| 620 |
+
// peninsula — fg is a small blob on the one open side (3 foreign cardinals).
|
| 621 |
+
tipN: (x, y, s) => R_TIP + 0.6 * wave1d(x, s) - dist(x, y, 3.5, 0),
|
| 622 |
+
tipS: (x, y, s) => R_TIP + 0.6 * wave1d(x, s) - dist(x, y, 3.5, 7),
|
| 623 |
+
tipE: (x, y, s) => R_TIP + 0.6 * wave1d(y, s) - dist(x, y, 7, 3.5),
|
| 624 |
+
tipW: (x, y, s) => R_TIP + 0.6 * wave1d(y, s) - dist(x, y, 0, 3.5),
|
| 625 |
+
// island — fg is a small blob in the centre (foreign on all 4 cardinals).
|
| 626 |
+
island: (x, y, s) => R_TIP + 0.6 * wave1d(x - y, s) - dist(x, y, 3.5, 3.5)
|
| 627 |
+
};
|
| 628 |
+
function dist(x, y, cx, cy) {
|
| 629 |
+
const dx = x - cx, dy = y - cy;
|
| 630 |
+
return Math.sqrt(dx * dx + dy * dy);
|
| 631 |
+
}
|
| 632 |
+
var STENCIL_CASES = Object.keys(CASES);
|
| 633 |
+
function boundaryCase(N, E, S, W, NW, NE, SW, SE) {
|
| 634 |
+
const card = N + E + S + W;
|
| 635 |
+
if (card === 0) {
|
| 636 |
+
const diag = NW + NE + SW + SE;
|
| 637 |
+
if (diag === 0) return null;
|
| 638 |
+
if (NW) return "cNW";
|
| 639 |
+
if (NE) return "cNE";
|
| 640 |
+
if (SW) return "cSW";
|
| 641 |
+
return "cSE";
|
| 642 |
+
}
|
| 643 |
+
if (card === 1) return N ? "N" : E ? "E" : S ? "S" : "W";
|
| 644 |
+
if (card === 2) {
|
| 645 |
+
if (N && E) return "oNE";
|
| 646 |
+
if (N && W) return "oNW";
|
| 647 |
+
if (S && E) return "oSE";
|
| 648 |
+
if (S && W) return "oSW";
|
| 649 |
+
return N ? "N" : "E";
|
| 650 |
+
}
|
| 651 |
+
if (card === 3) return !N ? "tipN" : !E ? "tipE" : !S ? "tipS" : "tipW";
|
| 652 |
+
return "island";
|
| 653 |
+
}
|
| 654 |
+
function makeStencil(caseKey, variant = 0) {
|
| 655 |
+
const sd = CASES[caseKey];
|
| 656 |
+
if (!sd) throw new Error(`unknown stencil case "${caseKey}"`);
|
| 657 |
+
const salt = (hashStr(caseKey) ^ variant * 40503) >>> 0;
|
| 658 |
+
const mask = new Uint8Array(TILE * TILE);
|
| 659 |
+
for (let y = 0; y < TILE; y++) for (let x = 0; x < TILE; x++) {
|
| 660 |
+
const d = sd(x, y, salt);
|
| 661 |
+
let fg;
|
| 662 |
+
if (d > BAND / 2) fg = 1;
|
| 663 |
+
else if (d < -BAND / 2) fg = 0;
|
| 664 |
+
else fg = (d + BAND / 2) / BAND > bayer(x, y) ? 1 : 0;
|
| 665 |
+
mask[y * TILE + x] = fg;
|
| 666 |
+
}
|
| 667 |
+
return mask;
|
| 668 |
+
}
|
| 669 |
+
function hashStr(s) {
|
| 670 |
+
let h = 2166136261;
|
| 671 |
+
for (let i = 0; i < s.length; i++) {
|
| 672 |
+
h ^= s.charCodeAt(i);
|
| 673 |
+
h = Math.imul(h, 16777619);
|
| 674 |
+
}
|
| 675 |
+
return h >>> 0;
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
// ../auto-battler/src/engine/orcGen.js
|
| 679 |
+
var sub2 = (seed, salt) => (Math.imul(seed | 0, 2654435761) ^ (salt | 0)) >>> 0;
|
| 680 |
+
var DDIRT_SCALE = 0.045;
|
| 681 |
+
var DDIRT_THRESHOLD = 0.55;
|
| 682 |
+
function isDarkerDirt(seed, x, y) {
|
| 683 |
+
return fbm(sub2(seed, 56599), x * DDIRT_SCALE, y * DDIRT_SCALE) > DDIRT_THRESHOLD;
|
| 684 |
+
}
|
| 685 |
+
var GRASS_SCALE = 0.04;
|
| 686 |
+
var GRASS_THRESHOLD = 0.58;
|
| 687 |
+
function isGrass(seed, x, y) {
|
| 688 |
+
return fbm(sub2(seed, 27221), x * GRASS_SCALE, y * GRASS_SCALE) > GRASS_THRESHOLD;
|
| 689 |
+
}
|
| 690 |
+
var ROCK_SCALE = 0.12;
|
| 691 |
+
var ROCK_THRESHOLD = 0.64;
|
| 692 |
+
function isRock(seed, x, y) {
|
| 693 |
+
return fbm(sub2(seed, 16588), x * ROCK_SCALE, y * ROCK_SCALE) > ROCK_THRESHOLD;
|
| 694 |
}
|
| 695 |
|
| 696 |
// ../auto-battler/src/render/tileAutotile.js
|
|
|
|
| 699 |
for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) a.push([c + i, r + j]);
|
| 700 |
return a;
|
| 701 |
};
|
| 702 |
+
var offset = (t, col) => [t[0] + col, t[1]];
|
| 703 |
var rhash = (x, y, salt, seed) => Math.imul(x * 73856093 ^ y * 19349663 ^ seed + (salt | 0), 2654435761) >>> 0;
|
| 704 |
function hashU32(a, b, c) {
|
| 705 |
let h = Math.imul((a | 0) ^ 2654435769, 2654435761);
|
|
|
|
| 714 |
return h % rate === 0 ? vars[(h >>> 5) % vars.length] : base;
|
| 715 |
};
|
| 716 |
var lerp3 = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
|
| 717 |
+
function autotile(set, N, E, S, W, NW, NE, SW, SE) {
|
| 718 |
+
const card = N + E + S + W;
|
| 719 |
+
if (card === 1) return N ? set.N : S ? set.S : E ? set.E : set.W;
|
| 720 |
+
if (card === 2) {
|
| 721 |
+
if (N && W) return set.vNW;
|
| 722 |
+
if (N && E) return set.vNE;
|
| 723 |
+
if (S && W) return set.vSW;
|
| 724 |
+
if (S && E) return set.vSE;
|
| 725 |
+
return N ? set.N : set.E;
|
| 726 |
+
}
|
| 727 |
+
if (card >= 3) return N ? set.N : S ? set.S : E ? set.E : set.W;
|
| 728 |
+
const diag = NW + NE + SW + SE;
|
| 729 |
+
if (diag === 0) return null;
|
| 730 |
+
if (diag === 1) return NW ? set.cNW : NE ? set.cNE : SW ? set.cSW : set.cSE;
|
| 731 |
+
if (NW && SE && !NE && !SW) return set.dNWSE;
|
| 732 |
+
if (NE && SW && !NW && !SE) return set.dSWNE;
|
| 733 |
+
return null;
|
| 734 |
+
}
|
| 735 |
+
function cleanField(raw, M, SZ) {
|
| 736 |
+
const idx = (i, j) => j * SZ + i;
|
| 737 |
+
let cur = new Uint8Array(SZ * SZ);
|
| 738 |
+
for (let j = 0; j < SZ; j++) for (let i = 0; i < SZ; i++) cur[idx(i, j)] = raw(i - M, j - M) ? 1 : 0;
|
| 739 |
+
let nxt = new Uint8Array(SZ * SZ);
|
| 740 |
+
for (let j = 1; j < SZ - 1; j++) for (let i = 1; i < SZ - 1; i++) {
|
| 741 |
+
let c = 0;
|
| 742 |
+
for (let dj = -1; dj <= 1; dj++) for (let di = -1; di <= 1; di++) c += cur[idx(i + di, j + dj)];
|
| 743 |
+
nxt[idx(i, j)] = c >= 5 ? 1 : 0;
|
| 744 |
+
}
|
| 745 |
+
cur = nxt;
|
| 746 |
+
for (let p = 0; p < 2; p++) {
|
| 747 |
+
nxt = new Uint8Array(SZ * SZ);
|
| 748 |
+
for (let j = 1; j < SZ - 1; j++) for (let i = 1; i < SZ - 1; i++) {
|
| 749 |
+
const card = cur[idx(i, j - 1)] + cur[idx(i + 1, j)] + cur[idx(i, j + 1)] + cur[idx(i - 1, j)];
|
| 750 |
+
nxt[idx(i, j)] = cur[idx(i, j)] ? card >= 2 ? 1 : 0 : card >= 3 ? 1 : 0;
|
| 751 |
+
}
|
| 752 |
+
cur = nxt;
|
| 753 |
+
}
|
| 754 |
+
return cur;
|
| 755 |
+
}
|
| 756 |
function blockTile([c0, r0], N, E, S, W) {
|
| 757 |
const cx = W && !E ? 0 : E && !W ? 2 : 1;
|
| 758 |
const cy = N && !S ? 0 : S && !N ? 2 : 1;
|
|
|
|
| 850 |
}
|
| 851 |
}
|
| 852 |
|
| 853 |
+
// ../auto-battler/src/render/orcKingdom.js
|
| 854 |
+
var ORC = "/assets/minifantasy/Minifantasy_Orc_Kingdom_v1.0/Minifantasy_Orc_Kingdom_Assets";
|
| 855 |
+
var TILES = `${ORC}/Tileset/Tiles.png`;
|
| 856 |
+
var TILE2 = 8;
|
| 857 |
+
var CHUNK = 32;
|
| 858 |
+
var DIRT_BASE = [7, 51];
|
| 859 |
+
var DIRT_VARS = rect(13, 51, 2, 6);
|
| 860 |
+
var DIRT_RATE = 4;
|
| 861 |
+
var GRASS = {
|
| 862 |
+
fill: [3, 51],
|
| 863 |
+
vars: [],
|
| 864 |
+
N: [3, 54],
|
| 865 |
+
S: [3, 52],
|
| 866 |
+
E: [2, 53],
|
| 867 |
+
W: [4, 53],
|
| 868 |
+
cNW: [4, 54],
|
| 869 |
+
cNE: [2, 54],
|
| 870 |
+
cSW: [4, 52],
|
| 871 |
+
cSE: [2, 52],
|
| 872 |
+
vNW: [3, 56],
|
| 873 |
+
vNE: [2, 56],
|
| 874 |
+
vSW: [3, 55],
|
| 875 |
+
vSE: [2, 55],
|
| 876 |
+
dNWSE: [4, 55],
|
| 877 |
+
dSWNE: [4, 56]
|
| 878 |
+
};
|
| 879 |
+
var DDIRT = {
|
| 880 |
+
fill: [7, 53],
|
| 881 |
+
vars: rect(10, 51, 2, 6),
|
| 882 |
+
N: [7, 52],
|
| 883 |
+
S: [7, 54],
|
| 884 |
+
E: [8, 53],
|
| 885 |
+
W: [6, 53],
|
| 886 |
+
vNW: [6, 52],
|
| 887 |
+
vNE: [8, 52],
|
| 888 |
+
vSW: [6, 54],
|
| 889 |
+
vSE: [8, 54],
|
| 890 |
+
cNW: [6, 55],
|
| 891 |
+
cNE: [7, 55],
|
| 892 |
+
cSW: [6, 56],
|
| 893 |
+
cSE: [7, 56],
|
| 894 |
+
dNWSE: [8, 55],
|
| 895 |
+
dSWNE: [8, 56]
|
| 896 |
+
};
|
| 897 |
+
var OVERLAY_RATE = 5;
|
| 898 |
+
var ROCK_COLOR_OFF = [0, 6, 12];
|
| 899 |
+
var STONE_SIZES = [[16, 51], [16, 52], [16, 53], [16, 54], [16, 55], [16, 56]];
|
| 900 |
+
var ROCK_FOLIAGE = [[17, 51], [18, 51], [19, 51], [20, 51]];
|
| 901 |
+
var ROCK_DENSE = [[18, 53], [19, 53], [18, 54], [19, 54], [18, 55], [19, 55]];
|
| 902 |
+
var ROCK_SPARSE = [
|
| 903 |
+
[17, 52],
|
| 904 |
+
[18, 52],
|
| 905 |
+
[19, 52],
|
| 906 |
+
[20, 52],
|
| 907 |
+
[17, 53],
|
| 908 |
+
[20, 53],
|
| 909 |
+
[17, 54],
|
| 910 |
+
[20, 54],
|
| 911 |
+
[17, 55],
|
| 912 |
+
[20, 55],
|
| 913 |
+
[17, 56],
|
| 914 |
+
[18, 56],
|
| 915 |
+
[19, 56],
|
| 916 |
+
[20, 56]
|
| 917 |
+
];
|
| 918 |
+
var ROCK_STONE_RATE = 28;
|
| 919 |
+
var ROCK_FOLIAGE_RATE = 12;
|
| 920 |
+
var COL_DIRT = [150, 128, 95];
|
| 921 |
+
var COL_DDIRT = [106, 90, 79];
|
| 922 |
+
var COL_GRASS = [96, 132, 58];
|
| 923 |
+
function chunkFields(seed, x0, y0) {
|
| 924 |
+
const M = 6, SZ = CHUNK + 2 * M;
|
| 925 |
+
const dd = cleanField((lx, ly) => isDarkerDirt(seed, x0 + lx, y0 + ly), M, SZ);
|
| 926 |
+
const grRaw = cleanField((lx, ly) => isGrass(seed, x0 + lx, y0 + ly), M, SZ);
|
| 927 |
+
const gr = new Uint8Array(grRaw.length);
|
| 928 |
+
for (let k = 0; k < gr.length; k++) gr[k] = grRaw[k] && !dd[k] ? 1 : 0;
|
| 929 |
+
const rockRaw = cleanField((lx, ly) => isRock(seed, x0 + lx, y0 + ly), M, SZ);
|
| 930 |
+
const rock = new Uint8Array(rockRaw.length);
|
| 931 |
+
for (let k = 0; k < rock.length; k++) rock[k] = rockRaw[k] && !dd[k] && !gr[k] ? 1 : 0;
|
| 932 |
+
return { dd, gr, rock, M, SZ, x0, y0 };
|
| 933 |
+
}
|
| 934 |
+
var ORC_GROUND_FILLS = [
|
| 935 |
+
{ key: "dirt", url: TILES, tile: DIRT_BASE },
|
| 936 |
+
{ key: "ddirt", url: TILES, tile: DDIRT.fill },
|
| 937 |
+
{ key: "grass", url: TILES, tile: GRASS.fill }
|
| 938 |
+
];
|
| 939 |
+
function orcGroundFillKey(seed, x, y) {
|
| 940 |
+
if (isDarkerDirt(seed, x, y)) return "ddirt";
|
| 941 |
+
if (isGrass(seed, x, y)) return "grass";
|
| 942 |
+
return "dirt";
|
| 943 |
+
}
|
| 944 |
+
var orcConfig = (seed) => ({
|
| 945 |
+
seed,
|
| 946 |
+
tile: TILE2,
|
| 947 |
+
chunk: CHUNK,
|
| 948 |
+
background: "#69636f",
|
| 949 |
+
async load({ Assets }) {
|
| 950 |
+
const t = await Assets.load(TILES);
|
| 951 |
+
t.source.scaleMode = "nearest";
|
| 952 |
+
return { tiles: t.source };
|
| 953 |
+
},
|
| 954 |
+
// `accept(tx,ty)` (chunk-local) gates which tiles this biome paints — defaults to all, so the
|
| 955 |
+
// standalone map is unchanged; the multi-biome overworld passes a per-biome dither mask.
|
| 956 |
+
bake({ x0, y0, seed: seed2, ctx, add, accept = () => true }) {
|
| 957 |
+
const f = chunkFields(seed2, x0, y0);
|
| 958 |
+
const { dd, gr, rock, M, SZ } = f;
|
| 959 |
+
const idx = (i, j) => j * SZ + i;
|
| 960 |
+
const at = (fld, lx, ly) => fld[idx(lx + M, ly + M)] === 1;
|
| 961 |
+
const atRock = (lx, ly) => rock[idx(lx + M, ly + M)] === 1;
|
| 962 |
+
const place = (coord, tx, ty) => add(ctx.tiles, coord[0], coord[1], tx, ty);
|
| 963 |
+
const drawLayer = (field, set, tx, ty) => {
|
| 964 |
+
if (!at(field, tx, ty)) return;
|
| 965 |
+
const N = !at(field, tx, ty - 1), E = !at(field, tx + 1, ty), S = !at(field, tx, ty + 1), W = !at(field, tx - 1, ty);
|
| 966 |
+
const NW = !at(field, tx - 1, ty - 1), NE = !at(field, tx + 1, ty - 1), SW = !at(field, tx - 1, ty + 1), SE = !at(field, tx + 1, ty + 1);
|
| 967 |
+
const t = autotile(set, N, E, S, W, NW, NE, SW, SE);
|
| 968 |
+
place(t || sparse(set.fill, set.vars, x0 + tx, y0 + ty, set === GRASS ? 1 : 2, OVERLAY_RATE, seed2), tx, ty);
|
| 969 |
+
};
|
| 970 |
+
for (let ty = 0; ty < CHUNK; ty++) {
|
| 971 |
+
for (let tx = 0; tx < CHUNK; tx++) {
|
| 972 |
+
if (!accept(tx, ty)) continue;
|
| 973 |
+
place(sparse(DIRT_BASE, DIRT_VARS, x0 + tx, y0 + ty, 0, DIRT_RATE, seed2), tx, ty);
|
| 974 |
+
drawLayer(dd, DDIRT, tx, ty);
|
| 975 |
+
drawLayer(gr, GRASS, tx, ty);
|
| 976 |
+
if (atRock(tx, ty)) {
|
| 977 |
+
const h = rhash(x0 + tx, y0 + ty, 3, seed2);
|
| 978 |
+
const interior = atRock(tx, ty - 1) && atRock(tx + 1, ty) && atRock(tx, ty + 1) && atRock(tx - 1, ty);
|
| 979 |
+
const pool = interior ? ROCK_DENSE : ROCK_SPARSE;
|
| 980 |
+
place(offset(pool[(h >>> 2) % pool.length], ROCK_COLOR_OFF[h % 3]), tx, ty);
|
| 981 |
+
} else if (!at(dd, tx, ty) && !at(gr, tx, ty)) {
|
| 982 |
+
const h = rhash(x0 + tx, y0 + ty, 7, seed2);
|
| 983 |
+
if (h % ROCK_STONE_RATE === 0) place(offset(STONE_SIZES[(h >>> 3) % STONE_SIZES.length], ROCK_COLOR_OFF[(h >>> 6) % 3]), tx, ty);
|
| 984 |
+
else if ((h >>> 8) % ROCK_FOLIAGE_RATE === 0) place(offset(ROCK_FOLIAGE[(h >>> 11) % ROCK_FOLIAGE.length], ROCK_COLOR_OFF[(h >>> 14) % 3]), tx, ty);
|
| 985 |
+
}
|
| 986 |
+
}
|
| 987 |
+
}
|
| 988 |
+
return { meta: f };
|
| 989 |
+
},
|
| 990 |
+
macroColor(seed2, tx, ty) {
|
| 991 |
+
if (isDarkerDirt(seed2, tx, ty)) return COL_DDIRT;
|
| 992 |
+
if (isGrass(seed2, tx, ty)) return COL_GRASS;
|
| 993 |
+
return COL_DIRT;
|
| 994 |
+
},
|
| 995 |
+
// Top-most ground tile [c,r] at world (wx,wy) for the grid overlay (rocks omitted).
|
| 996 |
+
tileIndexAt(wx, wy, meta) {
|
| 997 |
+
if (!meta) return null;
|
| 998 |
+
const { dd, gr, M, SZ, x0, y0 } = meta;
|
| 999 |
+
const lx = wx - x0, ly = wy - y0;
|
| 1000 |
+
if (lx < 0 || ly < 0 || lx >= CHUNK || ly >= CHUNK) return null;
|
| 1001 |
+
const at = (f, x, y) => f[(y + M) * SZ + (x + M)] === 1;
|
| 1002 |
+
for (const [field, set] of [[gr, GRASS], [dd, DDIRT]]) {
|
| 1003 |
+
if (!at(field, lx, ly)) continue;
|
| 1004 |
+
const N = !at(field, lx, ly - 1), E = !at(field, lx + 1, ly), S = !at(field, lx, ly + 1), W = !at(field, lx - 1, ly);
|
| 1005 |
+
const NW = !at(field, lx - 1, ly - 1), NE = !at(field, lx + 1, ly - 1), SW = !at(field, lx - 1, ly + 1), SE = !at(field, lx + 1, ly + 1);
|
| 1006 |
+
return autotile(set, N, E, S, W, NW, NE, SW, SE) || set.fill;
|
| 1007 |
+
}
|
| 1008 |
+
return DIRT_BASE;
|
| 1009 |
+
}
|
| 1010 |
+
});
|
| 1011 |
+
|
| 1012 |
+
// ../auto-battler/src/engine/fpGen.js
|
| 1013 |
+
var sub3 = (seed, salt) => (Math.imul(seed | 0, 2654435761) ^ (salt | 0)) >>> 0;
|
| 1014 |
+
var DIRT_SCALE = 0.04;
|
| 1015 |
+
var DIRT_THRESHOLD = 0.56;
|
| 1016 |
+
function isDirt(seed, x, y) {
|
| 1017 |
+
return fbm(sub3(seed, 53543), x * DIRT_SCALE, y * DIRT_SCALE) > DIRT_THRESHOLD;
|
| 1018 |
+
}
|
| 1019 |
+
var STONE_SCALE = 0.085;
|
| 1020 |
+
var STONE_THRESHOLD = 0.66;
|
| 1021 |
+
function isStone(seed, x, y) {
|
| 1022 |
+
return fbm(sub3(seed, 22286), x * STONE_SCALE, y * STONE_SCALE) > STONE_THRESHOLD;
|
| 1023 |
+
}
|
| 1024 |
+
var FOREST_SCALE = 0.05;
|
| 1025 |
+
function forestField(seed, x, y) {
|
| 1026 |
+
return fbm(sub3(seed, 15740503), x * FOREST_SCALE, y * FOREST_SCALE);
|
| 1027 |
+
}
|
| 1028 |
+
var STONE_CLUMP_SCALE = 0.11;
|
| 1029 |
+
function stoneClumpField(seed, x, y) {
|
| 1030 |
+
return fbm(sub3(seed, 5702849), x * STONE_CLUMP_SCALE, y * STONE_CLUMP_SCALE);
|
| 1031 |
+
}
|
| 1032 |
+
var ELEV_SCALE = 0.025;
|
| 1033 |
+
var ELEV_THRESHOLD = 0.6;
|
| 1034 |
+
function isRaised(seed, x, y) {
|
| 1035 |
+
return fbm(sub3(seed, 57836), x * ELEV_SCALE, y * ELEV_SCALE) > ELEV_THRESHOLD;
|
| 1036 |
+
}
|
| 1037 |
+
var RIVER_SCALE = 0.022;
|
| 1038 |
+
var RIVER_WIDTH = 0.035;
|
| 1039 |
+
var WARP_SCALE2 = 0.03;
|
| 1040 |
+
var WARP_AMP2 = 22;
|
| 1041 |
+
function isRiver(seed, x, y) {
|
| 1042 |
+
const wx = x + WARP_AMP2 * (fbm(sub3(seed, 1297), x * WARP_SCALE2, y * WARP_SCALE2) - 0.5);
|
| 1043 |
+
const wy = y + WARP_AMP2 * (fbm(sub3(seed, 1298), x * WARP_SCALE2, y * WARP_SCALE2) - 0.5);
|
| 1044 |
+
return Math.abs(fbm(sub3(seed, 8654), wx * RIVER_SCALE, wy * RIVER_SCALE) - 0.5) < RIVER_WIDTH;
|
| 1045 |
+
}
|
| 1046 |
+
|
| 1047 |
// ../auto-battler/src/render/forgottenPlains.js
|
| 1048 |
var FP = "/assets/minifantasy/Minifantasy_ForgottenPlains_v3.6_Commercial_Version/Minifantasy_ForgottenPlains_Assets";
|
| 1049 |
+
var TILES2 = `${FP}/Tileset/Minifantasy_ForgottenPlainsTiles.png`;
|
| 1050 |
var SHADOW = `${FP}/Tileset/Minifantasy_ForgottenPlainsTilesShadows.png`;
|
| 1051 |
var PROPS = `${FP}/props/Minifantasy_ForgottenPlainsProps.png`;
|
| 1052 |
var PROP_SHADOW = `${FP}/props/Minifantasy_ForgottenPlainsPropsShadows.png`;
|
| 1053 |
var FP_MOCKUP_URL = `${FP}/Minifantasy_ForgottenPlainsMockup.png`;
|
| 1054 |
+
var TILE3 = 8;
|
| 1055 |
+
var CHUNK2 = 32;
|
| 1056 |
var GRASS_BASE = [37, 11];
|
| 1057 |
var GRASS_VARS = [...rect(1, 1, 4, 1), ...rect(2, 3, 3, 5)];
|
| 1058 |
var GRASS_RATE = 9;
|
| 1059 |
var DIRT_BLOCK = [7, 3];
|
| 1060 |
var DIRT_FILL = [8, 4];
|
| 1061 |
+
var DIRT_VARS2 = [[7, 1], [8, 1], [9, 1]];
|
| 1062 |
var STONE_BLOCK = [12, 3];
|
| 1063 |
var STONE_FILL = [13, 4];
|
| 1064 |
var STONE_VARS = [[12, 1], [13, 1], [14, 1]];
|
|
|
|
| 1196 |
var STONE_CLUMP_THRESHOLD = 0.7;
|
| 1197 |
var STONE_CLUMP_MIN_NB = 3;
|
| 1198 |
var STONE_CLUMP_FILL = 0.7;
|
| 1199 |
+
var COL_GRASS2 = [97, 150, 55];
|
| 1200 |
+
var COL_DIRT2 = [118, 80, 38];
|
| 1201 |
var COL_STONE = [120, 120, 122];
|
| 1202 |
var COL_WATER = [74, 116, 196];
|
| 1203 |
function loadImg(url) {
|
|
|
|
| 1215 |
cv.height = img.naturalHeight;
|
| 1216 |
const g = cv.getContext("2d", { willReadFrequently: true });
|
| 1217 |
g.drawImage(img, 0, 0);
|
| 1218 |
+
const cols = img.naturalWidth / TILE3 | 0, rows = img.naturalHeight / TILE3 | 0, set = /* @__PURE__ */ new Set();
|
| 1219 |
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
|
| 1220 |
+
const d = g.getImageData(c * TILE3, r * TILE3, TILE3, TILE3).data;
|
| 1221 |
for (let i = 3; i < d.length; i += 4) {
|
| 1222 |
if (d[i] > 8) {
|
| 1223 |
set.add(c + "," + r);
|
|
|
|
| 1227 |
}
|
| 1228 |
return set;
|
| 1229 |
}
|
| 1230 |
+
function chunkFields2(seed, x0, y0) {
|
| 1231 |
const WATER_BUFFER = 2;
|
| 1232 |
+
const M = WATER_BUFFER + 4, SZ = CHUNK2 + 2 * M;
|
| 1233 |
const idx = (i, j) => j * SZ + i;
|
| 1234 |
const clean = (pred) => {
|
| 1235 |
let cur = new Uint8Array(SZ * SZ);
|
|
|
|
| 1287 |
}
|
| 1288 |
return { waterF, dirt, stone, raisedField, forestRegion, stoneClumpRegion, M, SZ, x0, y0 };
|
| 1289 |
}
|
| 1290 |
+
var FP_GROUND_FILLS = [
|
| 1291 |
+
{ key: "grass", url: TILES2, tile: GRASS_BASE },
|
| 1292 |
+
{ key: "dirt", url: TILES2, tile: DIRT_FILL },
|
| 1293 |
+
{ key: "stone", url: TILES2, tile: STONE_FILL }
|
| 1294 |
+
];
|
| 1295 |
+
function fpGroundFillKey(seed, x, y) {
|
| 1296 |
+
if (isStone(seed, x, y) && !isDirt(seed, x, y)) return "stone";
|
| 1297 |
+
if (isDirt(seed, x, y)) return "dirt";
|
| 1298 |
+
return "grass";
|
| 1299 |
+
}
|
| 1300 |
var fpConfig = (seed) => ({
|
| 1301 |
seed,
|
| 1302 |
+
tile: TILE3,
|
| 1303 |
+
chunk: CHUNK2,
|
| 1304 |
background: "#5a7b3a",
|
| 1305 |
async load({ Assets }) {
|
| 1306 |
const ctx = { tiles: null, shadow: null, shadowSet: null, props: null, propShadow: null };
|
| 1307 |
+
const t = await Assets.load(TILES2);
|
| 1308 |
t.source.scaleMode = "nearest";
|
| 1309 |
ctx.tiles = t.source;
|
| 1310 |
try {
|
|
|
|
| 1335 |
// `accept(tx,ty)` (chunk-local) gates which tiles this biome paints — defaults to all, so the
|
| 1336 |
// standalone map is unchanged; the multi-biome overworld passes a per-biome dither mask.
|
| 1337 |
bake({ x0, y0, seed: seed2, ctx, tmp, Sprite, tex, texFrame, add, accept = () => true }) {
|
| 1338 |
+
const f = chunkFields2(seed2, x0, y0);
|
| 1339 |
const { waterF, dirt, stone, raisedField, forestRegion, stoneClumpRegion, M, SZ } = f;
|
| 1340 |
const idx = (i, j) => j * SZ + i;
|
| 1341 |
const atW = (wx, wy) => waterF[idx(wx - x0 + M, wy - y0 + M)] === 1;
|
|
|
|
| 1346 |
add(ctx.tiles, coord[0], coord[1], tx, ty);
|
| 1347 |
if (ctx.shadow && (!ctx.shadowSet || ctx.shadowSet.has(coord[0] + "," + coord[1]))) {
|
| 1348 |
const sh = new Sprite(tex(ctx.shadow, coord[0], coord[1]));
|
| 1349 |
+
sh.x = tx * TILE3;
|
| 1350 |
+
sh.y = ty * TILE3;
|
| 1351 |
tmp.addChild(sh);
|
| 1352 |
}
|
| 1353 |
};
|
|
|
|
| 1368 |
} else cr = blockTile(BLOCK, N, E, S, W);
|
| 1369 |
place(cr, tx, ty);
|
| 1370 |
};
|
| 1371 |
+
for (let ty = 0; ty < CHUNK2; ty++) for (let tx = 0; tx < CHUNK2; tx++) {
|
| 1372 |
if (!accept(tx, ty)) continue;
|
| 1373 |
const wx = x0 + tx, wy = y0 + ty;
|
| 1374 |
place(sparse(GRASS_BASE, GRASS_VARS, wx, wy, 0, GRASS_RATE, seed2), tx, ty);
|
| 1375 |
+
if (atD(wx, wy)) blob(atD, DIRT_BLOCK, DIRT_FILL, DIRT_VARS2, 1, wx, wy, tx, ty);
|
| 1376 |
if (atS(wx, wy)) blob(atS, STONE_BLOCK, STONE_FILL, STONE_VARS, 2, wx, wy, tx, ty);
|
| 1377 |
if (atW(wx, wy)) blob(atW, WATER_BLOCK, WATER_FILL, WATER_VARS, 3, wx, wy, tx, ty);
|
| 1378 |
}
|
| 1379 |
const live = [];
|
| 1380 |
const propSprite = (spec, wx, wy) => {
|
| 1381 |
+
const sp = new Sprite(texFrame(ctx.props, spec.c * TILE3, (spec.r - spec.h + 1) * TILE3, spec.w * TILE3, spec.h * TILE3));
|
| 1382 |
sp.anchor.set(0.5, 1);
|
| 1383 |
+
sp.x = wx * TILE3 + TILE3 / 2;
|
| 1384 |
+
sp.y = (wy + 1) * TILE3;
|
| 1385 |
+
sp.zIndex = (wy + 1) * TILE3;
|
| 1386 |
return sp;
|
| 1387 |
};
|
| 1388 |
+
if (ctx.props) for (let ty = 0; ty < CHUNK2; ty++) for (let tx = 0; tx < CHUNK2; tx++) {
|
| 1389 |
if (!accept(tx, ty)) continue;
|
| 1390 |
const wx = x0 + tx, wy = y0 + ty;
|
| 1391 |
if (atW(wx, wy)) {
|
|
|
|
| 1408 |
continue;
|
| 1409 |
}
|
| 1410 |
const [c, r] = g.tiles[(h >>> 16) % g.tiles.length];
|
| 1411 |
+
const sp = new Sprite(texFrame(ctx.props, c * TILE3, r * TILE3, TILE3, TILE3));
|
| 1412 |
+
sp.x = tx * TILE3;
|
| 1413 |
+
sp.y = ty * TILE3;
|
| 1414 |
tmp.addChild(sp);
|
| 1415 |
}
|
| 1416 |
const stairAt = (wx, wy) => {
|
|
|
|
| 1421 |
const off = hashU32(seed2 ^ 358940, Math.floor(wx / STAIR_SPACING), 0) % STAIR_SPACING;
|
| 1422 |
return ((wy - off) % STAIR_SPACING + STAIR_SPACING) % STAIR_SPACING === 0;
|
| 1423 |
};
|
| 1424 |
+
bakeCliffs(FP_CLIFF, { chunk: CHUNK2, x0, y0, seed: seed2, raised: isRaisedAt, place, accept, stairAt, stairAtV });
|
| 1425 |
if (ctx.props) {
|
| 1426 |
const inGrove = (wx, wy) => forestRegion[idx(wx - x0 + M, wy - y0 + M)] === 1;
|
| 1427 |
+
const bx0 = Math.floor(x0 / FOREST_BLOCK_X), bx1 = Math.floor((x0 + CHUNK2 - 1) / FOREST_BLOCK_X);
|
| 1428 |
+
const by0 = Math.floor(y0 / FOREST_BLOCK_Y), by1 = Math.floor((y0 + CHUNK2 - 1) / FOREST_BLOCK_Y);
|
| 1429 |
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
|
| 1430 |
const h = hashU32(seed2 ^ 15748695, bx, by);
|
| 1431 |
const wx = bx * FOREST_BLOCK_X + h % FOREST_BLOCK_X, wy = by * FOREST_BLOCK_Y + (h >>> 4) % FOREST_BLOCK_Y;
|
| 1432 |
+
if (wx < x0 || wx >= x0 + CHUNK2 || wy < y0 || wy >= y0 + CHUNK2) continue;
|
| 1433 |
if (!accept(wx - x0, wy - y0)) continue;
|
| 1434 |
if (!inGrove(wx, wy)) continue;
|
| 1435 |
if ((h >>> 8 & 65535) / 65536 >= FOREST_FILL) continue;
|
|
|
|
| 1445 |
const T = TREES[(h >>> 24) % TREES.length];
|
| 1446 |
const flip = (h >>> 20 & 255) / 256 < TREE_FLIP_RATE;
|
| 1447 |
const sc = TREE_SCALE_BASE * (1 + ((h >>> 12 & 255) / 256 - 0.5) * 2 * TREE_SCALE_JITTER);
|
| 1448 |
+
const px = wx * TILE3 + TILE3 / 2 + ((h >>> 16 & 15) / 16 - 0.5) * 2 * TREE_JITTER;
|
| 1449 |
+
const py = wy * TILE3 + TILE3 / 2 + ((h >>> 28 & 15) / 16 - 0.5) * 2 * TREE_JITTER;
|
| 1450 |
const tr = new Sprite(texFrame(ctx.props, ...T.frame));
|
| 1451 |
tr.anchor.set(T.ax, T.ay);
|
| 1452 |
tr.scale.set(flip ? -sc : sc, sc);
|
|
|
|
| 1466 |
}
|
| 1467 |
if (ctx.props) {
|
| 1468 |
const inClump = (wx, wy) => stoneClumpRegion[idx(wx - x0 + M, wy - y0 + M)] === 1;
|
| 1469 |
+
const sbx0 = Math.floor(x0 / STONE_BLOCK_X), sbx1 = Math.floor((x0 + CHUNK2 - 1) / STONE_BLOCK_X);
|
| 1470 |
+
const sby0 = Math.floor(y0 / STONE_BLOCK_Y), sby1 = Math.floor((y0 + CHUNK2 - 1) / STONE_BLOCK_Y);
|
| 1471 |
for (let by = sby0; by <= sby1; by++) for (let bx = sbx0; bx <= sbx1; bx++) {
|
| 1472 |
const h = hashU32(seed2 ^ 5702885, bx, by);
|
| 1473 |
const wx = bx * STONE_BLOCK_X + h % STONE_BLOCK_X, wy = by * STONE_BLOCK_Y + (h >>> 4) % STONE_BLOCK_Y;
|
| 1474 |
+
if (wx < x0 || wx >= x0 + CHUNK2 || wy < y0 || wy >= y0 + CHUNK2) continue;
|
| 1475 |
if (!accept(wx - x0, wy - y0)) continue;
|
| 1476 |
if (!inClump(wx, wy)) continue;
|
| 1477 |
if ((h >>> 8 & 65535) / 65536 >= STONE_CLUMP_FILL) continue;
|
|
|
|
| 1500 |
macroColor(seed2, tx, ty) {
|
| 1501 |
const raised = isRaised(seed2, tx, ty);
|
| 1502 |
if (!raised && isRiver(seed2, tx, ty)) return COL_WATER;
|
| 1503 |
+
let c = isStone(seed2, tx, ty) && !isDirt(seed2, tx, ty) ? COL_STONE : isDirt(seed2, tx, ty) ? COL_DIRT2 : COL_GRASS2;
|
| 1504 |
if (raised) c = lerp3(c, COL_CLIFF, 0.25);
|
| 1505 |
return c;
|
| 1506 |
},
|
| 1507 |
tileIndexAt(wx, wy, meta) {
|
| 1508 |
if (!meta) return null;
|
| 1509 |
const { waterF, dirt, stone, M, SZ, x0, y0 } = meta;
|
| 1510 |
+
if (wx - x0 < 0 || wy - y0 < 0 || wx - x0 >= CHUNK2 || wy - y0 >= CHUNK2) return null;
|
| 1511 |
const idx = (i, j) => j * SZ + i;
|
| 1512 |
const pick = (fld, BLOCK, fill) => {
|
| 1513 |
const at = (x, y) => fld[idx(x - x0 + M, y - y0 + M)] === 1;
|
|
|
|
| 1526 |
return GRASS_BASE;
|
| 1527 |
}
|
| 1528 |
});
|
| 1529 |
+
|
| 1530 |
+
// ../auto-battler/src/engine/necropolisGen.js
|
| 1531 |
+
var sub4 = (seed, salt) => (Math.imul(seed | 0, 2654435761) ^ (salt | 0)) >>> 0;
|
| 1532 |
+
var RIVER_SCALE2 = 0.045;
|
| 1533 |
+
var RIVER_WIDTH2 = 0.05;
|
| 1534 |
+
var WARP_SCALE3 = 0.03;
|
| 1535 |
+
var WARP_AMP3 = 16;
|
| 1536 |
+
var DARK_SCALE = 0.06;
|
| 1537 |
+
var DARK_THRESHOLD = 0.6;
|
| 1538 |
+
function isDark(seed, x, y) {
|
| 1539 |
+
return fbm(sub4(seed, 55852), x * DARK_SCALE, y * DARK_SCALE) > DARK_THRESHOLD;
|
| 1540 |
+
}
|
| 1541 |
+
var ELEV_SCALE2 = 0.025;
|
| 1542 |
+
var ELEV_THRESHOLD2 = 0.6;
|
| 1543 |
+
function isRaised2(seed, x, y) {
|
| 1544 |
+
return fbm(sub4(seed, 57836), x * ELEV_SCALE2, y * ELEV_SCALE2) > ELEV_THRESHOLD2;
|
| 1545 |
+
}
|
| 1546 |
+
var BONE_SCALE = 0.055;
|
| 1547 |
+
var BONE_THRESHOLD = 0.62;
|
| 1548 |
+
function isBone(seed, x, y) {
|
| 1549 |
+
return fbm(sub4(seed, 45134), x * BONE_SCALE, y * BONE_SCALE) > BONE_THRESHOLD;
|
| 1550 |
+
}
|
| 1551 |
+
var FOREST_SCALE2 = 0.05;
|
| 1552 |
+
function forestField2(seed, x, y) {
|
| 1553 |
+
return fbm(sub4(seed, 15740503), x * FOREST_SCALE2, y * FOREST_SCALE2);
|
| 1554 |
+
}
|
| 1555 |
+
function isRiver2(seed, x, y) {
|
| 1556 |
+
const wx = x + WARP_AMP3 * (fbm(sub4(seed, 1297), x * WARP_SCALE3, y * WARP_SCALE3) - 0.5);
|
| 1557 |
+
const wy = y + WARP_AMP3 * (fbm(sub4(seed, 1298), x * WARP_SCALE3, y * WARP_SCALE3) - 0.5);
|
| 1558 |
+
return Math.abs(fbm(sub4(seed, 8654), wx * RIVER_SCALE2, wy * RIVER_SCALE2) - 0.5) < RIVER_WIDTH2;
|
| 1559 |
+
}
|
| 1560 |
+
|
| 1561 |
+
// ../auto-battler/src/render/necropolis.js
|
| 1562 |
+
var NECRO = "/assets/minifantasy/Minifantasy_Necropolis_v1.0/Minifantasy_Necropolis_Assets";
|
| 1563 |
+
var EXT_DIR = `${NECRO}/PremadeScenes/Exterior/SeparateLayers`;
|
| 1564 |
+
var BIOME = `${NECRO}/Tileset/Biome/CorruptedBiome.png`;
|
| 1565 |
+
var SHADOW2 = `${NECRO}/Tileset/Biome/CorruptedBiomeShadows.png`;
|
| 1566 |
+
var PROPS2 = `${NECRO}/Props/Props.png`;
|
| 1567 |
+
var PROP_SHADOW2 = `${NECRO}/Props/PropShadows.png`;
|
| 1568 |
+
var TILE4 = 8;
|
| 1569 |
+
var CHUNK3 = 32;
|
| 1570 |
+
var TREES2 = [
|
| 1571 |
+
{ frame: [9, 15, 14, 14], shadow: [8, 24, 3, 5], ax: 0.04, ay: 0.82 },
|
| 1572 |
+
// 1,1 (2×3)
|
| 1573 |
+
{ frame: [28, 11, 17, 19], shadow: [25, 24, 6, 6], ax: 0, ay: 0.84 },
|
| 1574 |
+
// 3,1 (3×3)
|
| 1575 |
+
{ frame: [51, 13, 18, 16], shadow: [55, 24, 7, 5], ax: 0.42, ay: 0.84 },
|
| 1576 |
+
// 6,1 (3×3)
|
| 1577 |
+
{ frame: [75, 14, 10, 15], shadow: [72, 25, 7, 4], ax: 0.05, ay: 0.87 }
|
| 1578 |
+
// 9,1 (2×3)
|
| 1579 |
+
];
|
| 1580 |
+
var TREE_SCALE_BASE2 = 1;
|
| 1581 |
+
var TREE_SCALE_JITTER2 = 0.15;
|
| 1582 |
+
var TREE_FLIP_RATE2 = 0.5;
|
| 1583 |
+
var TREE_JITTER2 = 2;
|
| 1584 |
+
var FOREST_BLOCK_X2 = 2;
|
| 1585 |
+
var FOREST_BLOCK_Y2 = 1;
|
| 1586 |
+
var FOREST_THRESHOLD2 = 0.62;
|
| 1587 |
+
var FOREST_MIN_NB2 = 2;
|
| 1588 |
+
var FOREST_FILL2 = 0.55;
|
| 1589 |
+
var TREE_WATER_BUFFER = 1;
|
| 1590 |
+
var FOLIAGE2 = [
|
| 1591 |
+
{ tiles: [[1, 5], [2, 5], [1, 6], [2, 6]], shadow: false, weight: 3 },
|
| 1592 |
+
// grass tufts (most common)
|
| 1593 |
+
{ tiles: [[6, 5], [7, 5], [6, 6], [7, 6]], shadow: true, weight: 1 },
|
| 1594 |
+
// species A
|
| 1595 |
+
{ tiles: [[9, 5], [10, 5], [9, 6], [10, 6]], shadow: true, weight: 1 }
|
| 1596 |
+
// species B
|
| 1597 |
+
];
|
| 1598 |
+
var FOLIAGE_WEIGHT2 = FOLIAGE2.reduce((s, g) => s + g.weight, 0);
|
| 1599 |
+
var FOLIAGE_RATE2 = 12;
|
| 1600 |
+
var LIP_T = [2, 10];
|
| 1601 |
+
var LIP_T_VAR = [[3, 10], [4, 10], [5, 10]];
|
| 1602 |
+
var LIP_TL = [1, 10];
|
| 1603 |
+
var LIP_TR = [6, 10];
|
| 1604 |
+
var WALL_L = [1, 14];
|
| 1605 |
+
var WALL_L_VAR = [[1, 11], [1, 12], [1, 13]];
|
| 1606 |
+
var WALL_R = [6, 14];
|
| 1607 |
+
var WALL_R_VAR = [[6, 11], [6, 12], [6, 13]];
|
| 1608 |
+
var LIP_S = [2, 15];
|
| 1609 |
+
var LIP_S_VAR = [[3, 15], [4, 15], [5, 15]];
|
| 1610 |
+
var LIP_SW = [1, 15];
|
| 1611 |
+
var LIP_SE = [6, 15];
|
| 1612 |
+
var FACE_H = 2;
|
| 1613 |
+
var FACE = [[3, 16], [3, 17]];
|
| 1614 |
+
var FACE_VAR = [[[2, 16], [4, 16], [5, 16]], [[2, 17], [4, 17], [5, 17]]];
|
| 1615 |
+
var FACE_L = [[1, 16], [1, 17]];
|
| 1616 |
+
var FACE_R = [[6, 16], [6, 17]];
|
| 1617 |
+
var CLIFF_SPARSE_RATE = 2;
|
| 1618 |
+
var NECRO_CLIFF = {
|
| 1619 |
+
LIP_T,
|
| 1620 |
+
LIP_T_VAR,
|
| 1621 |
+
LIP_TL,
|
| 1622 |
+
LIP_TR,
|
| 1623 |
+
WALL_L,
|
| 1624 |
+
WALL_L_VAR,
|
| 1625 |
+
WALL_R,
|
| 1626 |
+
WALL_R_VAR,
|
| 1627 |
+
LIP_S,
|
| 1628 |
+
LIP_S_VAR,
|
| 1629 |
+
LIP_SW,
|
| 1630 |
+
LIP_SE,
|
| 1631 |
+
FACE_H,
|
| 1632 |
+
FACE,
|
| 1633 |
+
FACE_VAR,
|
| 1634 |
+
FACE_L,
|
| 1635 |
+
FACE_R,
|
| 1636 |
+
RATE: CLIFF_SPARSE_RATE
|
| 1637 |
+
};
|
| 1638 |
+
var CORRUPT_LIGHT = [1, 1];
|
| 1639 |
+
var CORRUPT_DARK = [2, 1];
|
| 1640 |
+
var NECRO_GROUND_FILLS = [
|
| 1641 |
+
{ key: "light", url: BIOME, tile: CORRUPT_LIGHT },
|
| 1642 |
+
{ key: "dark", url: BIOME, tile: CORRUPT_DARK }
|
| 1643 |
+
];
|
| 1644 |
+
function necroGroundFillKey(seed, x, y) {
|
| 1645 |
+
return isDark(seed, x, y) ? "dark" : "light";
|
| 1646 |
+
}
|
| 1647 |
+
var LIGHT_VARIANTS = [[1, 2], [2, 2], [1, 3], [2, 3], [1, 4], [2, 4], [1, 5], [2, 5]];
|
| 1648 |
+
var LIGHT_SPARSE_RATE = 10;
|
| 1649 |
+
var WATER_BLOCK2 = [4, 2];
|
| 1650 |
+
var BONE_BLOCK = [13, 2];
|
| 1651 |
+
var BONE_FILL = [[12, 1], [13, 1], [14, 1], [15, 1]];
|
| 1652 |
+
var EDGE_N = [18, 4];
|
| 1653 |
+
var EDGE_S = [18, 2];
|
| 1654 |
+
var EDGE_E = [17, 3];
|
| 1655 |
+
var EDGE_W = [19, 3];
|
| 1656 |
+
var CONV_NW = [18, 6];
|
| 1657 |
+
var CONV_NE = [17, 6];
|
| 1658 |
+
var CONV_SW = [18, 5];
|
| 1659 |
+
var CONV_SE = [17, 5];
|
| 1660 |
+
var CONC_NW = [19, 4];
|
| 1661 |
+
var CONC_NE = [17, 4];
|
| 1662 |
+
var CONC_SW = [19, 2];
|
| 1663 |
+
var CONC_SE = [17, 2];
|
| 1664 |
+
var CONC_NWSE = [19, 6];
|
| 1665 |
+
var CONC_NESW = [19, 5];
|
| 1666 |
+
var COL_LIGHT = [130, 119, 136];
|
| 1667 |
+
var COL_DARK = [105, 99, 113];
|
| 1668 |
+
var COL_WATER2 = [74, 142, 48];
|
| 1669 |
+
var COL_BONE = [180, 156, 126];
|
| 1670 |
+
var COL_CLIFF2 = [175, 146, 109];
|
| 1671 |
+
var COL_FOREST = [96, 101, 70];
|
| 1672 |
+
function corruptTile(N, E, S, W, NW, NE, SW, SE) {
|
| 1673 |
+
const card = (N ? 1 : 0) + (E ? 1 : 0) + (S ? 1 : 0) + (W ? 1 : 0);
|
| 1674 |
+
if (card === 1) return N ? EDGE_N : S ? EDGE_S : E ? EDGE_E : EDGE_W;
|
| 1675 |
+
if (card === 2) {
|
| 1676 |
+
if (N && W) return CONV_NW;
|
| 1677 |
+
if (N && E) return CONV_NE;
|
| 1678 |
+
if (S && W) return CONV_SW;
|
| 1679 |
+
if (S && E) return CONV_SE;
|
| 1680 |
+
return N ? EDGE_N : EDGE_E;
|
| 1681 |
+
}
|
| 1682 |
+
if (card >= 3) return N ? EDGE_N : S ? EDGE_S : E ? EDGE_E : EDGE_W;
|
| 1683 |
+
const diag = (NW ? 1 : 0) + (NE ? 1 : 0) + (SW ? 1 : 0) + (SE ? 1 : 0);
|
| 1684 |
+
if (diag === 0) return null;
|
| 1685 |
+
if (diag === 1) return NW ? CONC_NW : NE ? CONC_NE : SW ? CONC_SW : CONC_SE;
|
| 1686 |
+
if (NW && SE && !NE && !SW) return CONC_NWSE;
|
| 1687 |
+
if (NE && SW && !NW && !SE) return CONC_NESW;
|
| 1688 |
+
return null;
|
| 1689 |
+
}
|
| 1690 |
+
function boneCenterTile([c0, r0], dNW, dNE, dSW, dSE) {
|
| 1691 |
+
const n = dNW + dNE + dSW + dSE;
|
| 1692 |
+
if (n === 1) {
|
| 1693 |
+
if (dNW) return [c0, r0 + 3];
|
| 1694 |
+
if (dNE) return [c0 + 1, r0 + 3];
|
| 1695 |
+
if (dSW) return [c0, r0 + 4];
|
| 1696 |
+
return [c0 + 1, r0 + 4];
|
| 1697 |
+
}
|
| 1698 |
+
if (dNE && dSW && !dNW && !dSE) return [c0 + 2, r0 + 4];
|
| 1699 |
+
if (dNW && dSE && !dNE && !dSW) return [c0 + 2, r0 + 3];
|
| 1700 |
+
return [c0 + 1, r0 + 1];
|
| 1701 |
+
}
|
| 1702 |
+
function biomeColor(seed, tx, ty) {
|
| 1703 |
+
const raised = isRaised2(seed, tx, ty);
|
| 1704 |
+
if (!raised && isRiver2(seed, tx, ty)) return COL_WATER2;
|
| 1705 |
+
if (isBone(seed, tx, ty)) return COL_BONE;
|
| 1706 |
+
let c = isDark(seed, tx, ty) ? COL_DARK : COL_LIGHT;
|
| 1707 |
+
if (forestField2(seed, tx, ty) > FOREST_THRESHOLD2) c = lerp3(c, COL_FOREST, 0.5);
|
| 1708 |
+
if (raised) c = lerp3(c, COL_CLIFF2, 0.22);
|
| 1709 |
+
return c;
|
| 1710 |
+
}
|
| 1711 |
+
function loadImg2(url) {
|
| 1712 |
+
return new Promise((resolve, reject) => {
|
| 1713 |
+
const img = new Image();
|
| 1714 |
+
img.onload = () => resolve(img);
|
| 1715 |
+
img.onerror = reject;
|
| 1716 |
+
img.src = url;
|
| 1717 |
+
});
|
| 1718 |
+
}
|
| 1719 |
+
async function buildShadowMask2(url) {
|
| 1720 |
+
const img = await loadImg2(url);
|
| 1721 |
+
const cv = document.createElement("canvas");
|
| 1722 |
+
cv.width = img.naturalWidth;
|
| 1723 |
+
cv.height = img.naturalHeight;
|
| 1724 |
+
const g = cv.getContext("2d", { willReadFrequently: true });
|
| 1725 |
+
g.drawImage(img, 0, 0);
|
| 1726 |
+
const cols = img.naturalWidth / TILE4 | 0, rows = img.naturalHeight / TILE4 | 0, set = /* @__PURE__ */ new Set();
|
| 1727 |
+
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
|
| 1728 |
+
const d = g.getImageData(c * TILE4, r * TILE4, TILE4, TILE4).data;
|
| 1729 |
+
for (let i = 3; i < d.length; i += 4) {
|
| 1730 |
+
if (d[i] > 8) {
|
| 1731 |
+
set.add(c + "," + r);
|
| 1732 |
+
break;
|
| 1733 |
+
}
|
| 1734 |
+
}
|
| 1735 |
+
}
|
| 1736 |
+
return set;
|
| 1737 |
+
}
|
| 1738 |
+
function chunkFields3(seed, x0, y0) {
|
| 1739 |
+
const WATER_BUFFER = 2;
|
| 1740 |
+
const ELEV_BUFFER = 3;
|
| 1741 |
+
const M = Math.max(WATER_BUFFER, ELEV_BUFFER) + 4, SZ = CHUNK3 + 2 * M;
|
| 1742 |
+
const idx = (i, j) => j * SZ + i;
|
| 1743 |
+
const clean = (pred) => {
|
| 1744 |
+
let cur = new Uint8Array(SZ * SZ);
|
| 1745 |
+
for (let j = 0; j < SZ; j++) for (let i = 0; i < SZ; i++) cur[idx(i, j)] = pred(x0 + i - M, y0 + j - M) ? 1 : 0;
|
| 1746 |
+
let nxt = new Uint8Array(SZ * SZ);
|
| 1747 |
+
for (let j = 1; j < SZ - 1; j++) for (let i = 1; i < SZ - 1; i++) {
|
| 1748 |
+
let c = 0;
|
| 1749 |
+
for (let dj = -1; dj <= 1; dj++) for (let di = -1; di <= 1; di++) c += cur[idx(i + di, j + dj)];
|
| 1750 |
+
nxt[idx(i, j)] = c >= 5 ? 1 : 0;
|
| 1751 |
+
}
|
| 1752 |
+
cur = nxt;
|
| 1753 |
+
for (let p = 0; p < 2; p++) {
|
| 1754 |
+
nxt = new Uint8Array(SZ * SZ);
|
| 1755 |
+
for (let j = 1; j < SZ - 1; j++) for (let i = 1; i < SZ - 1; i++) {
|
| 1756 |
+
const card = cur[idx(i, j - 1)] + cur[idx(i + 1, j)] + cur[idx(i, j + 1)] + cur[idx(i - 1, j)];
|
| 1757 |
+
nxt[idx(i, j)] = cur[idx(i, j)] ? card >= 2 ? 1 : 0 : card >= 3 ? 1 : 0;
|
| 1758 |
+
}
|
| 1759 |
+
cur = nxt;
|
| 1760 |
+
}
|
| 1761 |
+
return cur;
|
| 1762 |
+
};
|
| 1763 |
+
const darkRaw = clean((x, y) => isDark(seed, x, y));
|
| 1764 |
+
const waterField = clean((x, y) => isRiver2(seed, x, y));
|
| 1765 |
+
const raisedField = clean((x, y) => isRaised2(seed, x, y));
|
| 1766 |
+
const boneField = clean((x, y) => isBone(seed, x, y));
|
| 1767 |
+
const fraw = new Uint8Array(SZ * SZ);
|
| 1768 |
+
for (let j = 0; j < SZ; j++) for (let i = 0; i < SZ; i++) fraw[idx(i, j)] = forestField2(seed, x0 + i - M, y0 + j - M) > FOREST_THRESHOLD2 ? 1 : 0;
|
| 1769 |
+
const forestRegion = new Uint8Array(SZ * SZ);
|
| 1770 |
+
for (let j = 1; j < SZ - 1; j++) for (let i = 1; i < SZ - 1; i++) {
|
| 1771 |
+
if (!fraw[idx(i, j)]) continue;
|
| 1772 |
+
let c = 0;
|
| 1773 |
+
for (let dj = -1; dj <= 1; dj++) for (let di = -1; di <= 1; di++) if ((di || dj) && fraw[idx(i + di, j + dj)]) c++;
|
| 1774 |
+
forestRegion[idx(i, j)] = c >= FOREST_MIN_NB2 ? 1 : 0;
|
| 1775 |
+
}
|
| 1776 |
+
const field = new Uint8Array(SZ * SZ);
|
| 1777 |
+
const boneMask = new Uint8Array(SZ * SZ);
|
| 1778 |
+
const B = Math.max(WATER_BUFFER, ELEV_BUFFER);
|
| 1779 |
+
for (let j = B; j < SZ - B; j++) for (let i = B; i < SZ - B; i++) {
|
| 1780 |
+
let nearW = 0;
|
| 1781 |
+
for (let dj = -WATER_BUFFER; dj <= WATER_BUFFER && !nearW; dj++) for (let di = -WATER_BUFFER; di <= WATER_BUFFER; di++) if (waterField[idx(i + di, j + dj)]) {
|
| 1782 |
+
nearW = 1;
|
| 1783 |
+
break;
|
| 1784 |
+
}
|
| 1785 |
+
let nearCliff = 0;
|
| 1786 |
+
{
|
| 1787 |
+
const me = raisedField[idx(i, j)];
|
| 1788 |
+
for (let dj = -ELEV_BUFFER; dj <= ELEV_BUFFER && !nearCliff; dj++) for (let di = -ELEV_BUFFER; di <= ELEV_BUFFER; di++) if (raisedField[idx(i + di, j + dj)] !== me) {
|
| 1789 |
+
nearCliff = 1;
|
| 1790 |
+
break;
|
| 1791 |
+
}
|
| 1792 |
+
}
|
| 1793 |
+
const water = waterField[idx(i, j)];
|
| 1794 |
+
field[idx(i, j)] = darkRaw[idx(i, j)] && !nearW && !water && !nearCliff ? 0 : 1;
|
| 1795 |
+
boneMask[idx(i, j)] = boneField[idx(i, j)] && !water && !nearW && !nearCliff ? 1 : 0;
|
| 1796 |
+
}
|
| 1797 |
+
for (let pass = 0; pass < 3; pass++) {
|
| 1798 |
+
const prev = boneMask.slice();
|
| 1799 |
+
let changed = false;
|
| 1800 |
+
for (let j = B + 1; j < SZ - B - 1; j++) for (let i = B + 1; i < SZ - B - 1; i++) {
|
| 1801 |
+
if (!prev[idx(i, j)]) continue;
|
| 1802 |
+
const up = prev[idx(i, j - 1)], down = prev[idx(i, j + 1)], left = prev[idx(i - 1, j)], right = prev[idx(i + 1, j)];
|
| 1803 |
+
if (!up && !down || !left && !right) {
|
| 1804 |
+
boneMask[idx(i, j)] = 0;
|
| 1805 |
+
changed = true;
|
| 1806 |
+
}
|
| 1807 |
+
}
|
| 1808 |
+
if (!changed) break;
|
| 1809 |
+
}
|
| 1810 |
+
return { field, boneMask, waterField, raisedField, forestRegion, M, SZ, x0, y0 };
|
| 1811 |
+
}
|
| 1812 |
+
var necropolisConfig = (seed) => ({
|
| 1813 |
+
seed,
|
| 1814 |
+
tile: TILE4,
|
| 1815 |
+
chunk: CHUNK3,
|
| 1816 |
+
background: "#69636f",
|
| 1817 |
+
async load({ Assets }) {
|
| 1818 |
+
const ctx = { biome: null, shadow: null, shadowSet: null, props: null, propShadow: null };
|
| 1819 |
+
const b = await Assets.load(BIOME);
|
| 1820 |
+
b.source.scaleMode = "nearest";
|
| 1821 |
+
ctx.biome = b.source;
|
| 1822 |
+
try {
|
| 1823 |
+
const s = await Assets.load(SHADOW2);
|
| 1824 |
+
s.source.scaleMode = "nearest";
|
| 1825 |
+
ctx.shadow = s.source;
|
| 1826 |
+
} catch {
|
| 1827 |
+
}
|
| 1828 |
+
try {
|
| 1829 |
+
ctx.shadowSet = await buildShadowMask2(SHADOW2);
|
| 1830 |
+
} catch {
|
| 1831 |
+
ctx.shadowSet = null;
|
| 1832 |
+
}
|
| 1833 |
+
try {
|
| 1834 |
+
const p = await Assets.load(PROPS2);
|
| 1835 |
+
p.source.scaleMode = "nearest";
|
| 1836 |
+
ctx.props = p.source;
|
| 1837 |
+
} catch {
|
| 1838 |
+
}
|
| 1839 |
+
try {
|
| 1840 |
+
const p = await Assets.load(PROP_SHADOW2);
|
| 1841 |
+
p.source.scaleMode = "nearest";
|
| 1842 |
+
ctx.propShadow = p.source;
|
| 1843 |
+
} catch {
|
| 1844 |
+
}
|
| 1845 |
+
return ctx;
|
| 1846 |
+
},
|
| 1847 |
+
// `accept(tx,ty)` (chunk-local) gates which tiles this biome paints — defaults to all, so the
|
| 1848 |
+
// standalone map is unchanged; the multi-biome overworld passes a per-biome dither mask.
|
| 1849 |
+
bake({ x0, y0, seed: seed2, ctx, tmp, Sprite, tex, texFrame, add, accept = () => true }) {
|
| 1850 |
+
const f = chunkFields3(seed2, x0, y0);
|
| 1851 |
+
const { field, boneMask, waterField, raisedField, forestRegion, M, SZ } = f;
|
| 1852 |
+
const idx = (i, j) => j * SZ + i;
|
| 1853 |
+
const isLight = (wx, wy) => field[idx(wx - x0 + M, wy - y0 + M)] === 1;
|
| 1854 |
+
const isRaisedAt = (wx, wy) => raisedField[idx(wx - x0 + M, wy - y0 + M)] === 1;
|
| 1855 |
+
const isWater = (wx, wy) => waterField[idx(wx - x0 + M, wy - y0 + M)] === 1 && !isRaisedAt(wx, wy);
|
| 1856 |
+
const boneAt = (wx, wy) => boneMask[idx(wx - x0 + M, wy - y0 + M)] === 1;
|
| 1857 |
+
const place = (coord, tx, ty) => {
|
| 1858 |
+
add(ctx.biome, coord[0], coord[1], tx, ty);
|
| 1859 |
+
if (ctx.shadow && (!ctx.shadowSet || ctx.shadowSet.has(coord[0] + "," + coord[1]))) {
|
| 1860 |
+
const sh = new Sprite(tex(ctx.shadow, coord[0], coord[1]));
|
| 1861 |
+
sh.x = tx * TILE4;
|
| 1862 |
+
sh.y = ty * TILE4;
|
| 1863 |
+
tmp.addChild(sh);
|
| 1864 |
+
}
|
| 1865 |
+
};
|
| 1866 |
+
for (let ty = 0; ty < CHUNK3; ty++) for (let tx = 0; tx < CHUNK3; tx++) {
|
| 1867 |
+
if (!accept(tx, ty)) continue;
|
| 1868 |
+
const wx = x0 + tx, wy = y0 + ty;
|
| 1869 |
+
if (!isLight(wx, wy)) {
|
| 1870 |
+
place(CORRUPT_DARK, tx, ty);
|
| 1871 |
+
continue;
|
| 1872 |
+
}
|
| 1873 |
+
const N = !isLight(wx, wy - 1), E = !isLight(wx + 1, wy), S = !isLight(wx, wy + 1), Wf = !isLight(wx - 1, wy);
|
| 1874 |
+
const NW = !isLight(wx - 1, wy - 1), NE = !isLight(wx + 1, wy - 1), SW = !isLight(wx - 1, wy + 1), SE = !isLight(wx + 1, wy + 1);
|
| 1875 |
+
const t = corruptTile(N, E, S, Wf, NW, NE, SW, SE);
|
| 1876 |
+
if (!t) {
|
| 1877 |
+
place(sparse(CORRUPT_LIGHT, LIGHT_VARIANTS, wx, wy, 0, LIGHT_SPARSE_RATE, seed2), tx, ty);
|
| 1878 |
+
continue;
|
| 1879 |
+
}
|
| 1880 |
+
place(CORRUPT_DARK, tx, ty);
|
| 1881 |
+
place(t, tx, ty);
|
| 1882 |
+
}
|
| 1883 |
+
for (let ty = 0; ty < CHUNK3; ty++) for (let tx = 0; tx < CHUNK3; tx++) {
|
| 1884 |
+
if (!accept(tx, ty)) continue;
|
| 1885 |
+
const wx = x0 + tx, wy = y0 + ty;
|
| 1886 |
+
if (!isWater(wx, wy)) continue;
|
| 1887 |
+
const N = !isWater(wx, wy - 1), E = !isWater(wx + 1, wy), S = !isWater(wx, wy + 1), Wf = !isWater(wx - 1, wy);
|
| 1888 |
+
if (N && E && S && Wf) continue;
|
| 1889 |
+
let cr;
|
| 1890 |
+
if (!N && !E && !S && !Wf) cr = centerTile(WATER_BLOCK2, !isWater(wx - 1, wy - 1), !isWater(wx + 1, wy - 1), !isWater(wx - 1, wy + 1), !isWater(wx + 1, wy + 1));
|
| 1891 |
+
else cr = blockTile(WATER_BLOCK2, N, E, S, Wf);
|
| 1892 |
+
place(cr, tx, ty);
|
| 1893 |
+
}
|
| 1894 |
+
for (let ty = 0; ty < CHUNK3; ty++) for (let tx = 0; tx < CHUNK3; tx++) {
|
| 1895 |
+
if (!accept(tx, ty)) continue;
|
| 1896 |
+
const wx = x0 + tx, wy = y0 + ty;
|
| 1897 |
+
if (!boneAt(wx, wy)) continue;
|
| 1898 |
+
const N = !boneAt(wx, wy - 1), E = !boneAt(wx + 1, wy), S = !boneAt(wx, wy + 1), Wf = !boneAt(wx - 1, wy);
|
| 1899 |
+
if (N && E && S && Wf) {
|
| 1900 |
+
place(sparse(BONE_FILL[0], BONE_FILL, wx, wy, 11, 1, seed2), tx, ty);
|
| 1901 |
+
continue;
|
| 1902 |
+
}
|
| 1903 |
+
let cr;
|
| 1904 |
+
if (!N && !E && !S && !Wf) {
|
| 1905 |
+
const dNW = !boneAt(wx - 1, wy - 1), dNE = !boneAt(wx + 1, wy - 1), dSW = !boneAt(wx - 1, wy + 1), dSE = !boneAt(wx + 1, wy + 1);
|
| 1906 |
+
if (dNW || dNE || dSW || dSE) cr = boneCenterTile(BONE_BLOCK, dNW, dNE, dSW, dSE);
|
| 1907 |
+
else {
|
| 1908 |
+
place(sparse(BONE_FILL[0], BONE_FILL, wx, wy, 11, 1, seed2), tx, ty);
|
| 1909 |
+
continue;
|
| 1910 |
+
}
|
| 1911 |
+
} else cr = blockTile(BONE_BLOCK, N, E, S, Wf);
|
| 1912 |
+
place(cr, tx, ty);
|
| 1913 |
+
}
|
| 1914 |
+
if (ctx.props) for (let ty = 0; ty < CHUNK3; ty++) for (let tx = 0; tx < CHUNK3; tx++) {
|
| 1915 |
+
if (!accept(tx, ty)) continue;
|
| 1916 |
+
const wx = x0 + tx, wy = y0 + ty;
|
| 1917 |
+
if (isWater(wx, wy) || boneAt(wx, wy)) continue;
|
| 1918 |
+
const h = hashU32(seed2 ^ 15733114, wx, wy);
|
| 1919 |
+
if (h % FOLIAGE_RATE2 !== 0) continue;
|
| 1920 |
+
let pick = (h >>> 8) % FOLIAGE_WEIGHT2, g = FOLIAGE2[0];
|
| 1921 |
+
for (const grp of FOLIAGE2) {
|
| 1922 |
+
if (pick < grp.weight) {
|
| 1923 |
+
g = grp;
|
| 1924 |
+
break;
|
| 1925 |
+
}
|
| 1926 |
+
pick -= grp.weight;
|
| 1927 |
+
}
|
| 1928 |
+
const [c, r] = g.tiles[(h >>> 16) % g.tiles.length];
|
| 1929 |
+
if (g.shadow && ctx.propShadow) {
|
| 1930 |
+
const sh = new Sprite(texFrame(ctx.propShadow, c * TILE4, r * TILE4, TILE4, TILE4));
|
| 1931 |
+
sh.x = tx * TILE4;
|
| 1932 |
+
sh.y = ty * TILE4;
|
| 1933 |
+
tmp.addChild(sh);
|
| 1934 |
+
}
|
| 1935 |
+
const sp = new Sprite(texFrame(ctx.props, c * TILE4, r * TILE4, TILE4, TILE4));
|
| 1936 |
+
sp.x = tx * TILE4;
|
| 1937 |
+
sp.y = ty * TILE4;
|
| 1938 |
+
tmp.addChild(sp);
|
| 1939 |
+
}
|
| 1940 |
+
bakeCliffs(NECRO_CLIFF, { chunk: CHUNK3, x0, y0, seed: seed2, raised: isRaisedAt, place, accept });
|
| 1941 |
+
const live = [];
|
| 1942 |
+
if (ctx.props) {
|
| 1943 |
+
const nearWater = (wx, wy) => {
|
| 1944 |
+
for (let dj = -TREE_WATER_BUFFER; dj <= TREE_WATER_BUFFER; dj++) for (let di = -TREE_WATER_BUFFER; di <= TREE_WATER_BUFFER; di++) if (waterField[idx(wx + di - x0 + M, wy + dj - y0 + M)]) return true;
|
| 1945 |
+
return false;
|
| 1946 |
+
};
|
| 1947 |
+
const nearCliff = (wx, wy) => {
|
| 1948 |
+
const me = isRaisedAt(wx, wy);
|
| 1949 |
+
for (let dj = -3; dj <= 3; dj++) for (let di = -3; di <= 3; di++) if (isRaisedAt(wx + di, wy + dj) !== me) return true;
|
| 1950 |
+
return false;
|
| 1951 |
+
};
|
| 1952 |
+
const bx0 = Math.floor(x0 / FOREST_BLOCK_X2), bx1 = Math.floor((x0 + CHUNK3 - 1) / FOREST_BLOCK_X2);
|
| 1953 |
+
const by0 = Math.floor(y0 / FOREST_BLOCK_Y2), by1 = Math.floor((y0 + CHUNK3 - 1) / FOREST_BLOCK_Y2);
|
| 1954 |
+
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
|
| 1955 |
+
const h = hashU32(seed2 ^ 15748695, bx, by);
|
| 1956 |
+
const wx = bx * FOREST_BLOCK_X2 + h % FOREST_BLOCK_X2, wy = by * FOREST_BLOCK_Y2 + (h >>> 4) % FOREST_BLOCK_Y2;
|
| 1957 |
+
if (wx < x0 || wx >= x0 + CHUNK3 || wy < y0 || wy >= y0 + CHUNK3) continue;
|
| 1958 |
+
if (!accept(wx - x0, wy - y0)) continue;
|
| 1959 |
+
if (forestRegion[idx(wx - x0 + M, wy - y0 + M)] !== 1) continue;
|
| 1960 |
+
if ((h >>> 8 & 65535) / 65536 >= FOREST_FILL2) continue;
|
| 1961 |
+
if (nearWater(wx, wy) || boneAt(wx, wy) || nearCliff(wx, wy)) continue;
|
| 1962 |
+
const T = TREES2[(h >>> 24) % TREES2.length];
|
| 1963 |
+
const flip = (h >>> 20 & 255) / 256 < TREE_FLIP_RATE2;
|
| 1964 |
+
const sc = TREE_SCALE_BASE2 * (1 + ((h >>> 12 & 255) / 256 - 0.5) * 2 * TREE_SCALE_JITTER2);
|
| 1965 |
+
const px = wx * TILE4 + TILE4 / 2 + ((h >>> 16 & 15) / 16 - 0.5) * 2 * TREE_JITTER2;
|
| 1966 |
+
const py = wy * TILE4 + TILE4 / 2 + ((h >>> 28 & 15) / 16 - 0.5) * 2 * TREE_JITTER2;
|
| 1967 |
+
const tr = new Sprite(texFrame(ctx.props, ...T.frame));
|
| 1968 |
+
tr.anchor.set(T.ax, T.ay);
|
| 1969 |
+
tr.scale.set(flip ? -sc : sc, sc);
|
| 1970 |
+
tr.x = px;
|
| 1971 |
+
tr.y = py;
|
| 1972 |
+
tr.zIndex = py;
|
| 1973 |
+
let shadow = null;
|
| 1974 |
+
if (ctx.propShadow) {
|
| 1975 |
+
shadow = new Sprite(texFrame(ctx.propShadow, ...T.shadow));
|
| 1976 |
+
shadow.anchor.set(0.5, 0.5);
|
| 1977 |
+
shadow.scale.set(sc, sc);
|
| 1978 |
+
shadow.x = px;
|
| 1979 |
+
shadow.y = py;
|
| 1980 |
+
}
|
| 1981 |
+
live.push({ sprite: tr, shadow });
|
| 1982 |
+
}
|
| 1983 |
+
}
|
| 1984 |
+
return { meta: f, live };
|
| 1985 |
+
},
|
| 1986 |
+
macroColor: biomeColor,
|
| 1987 |
+
// The ground tile [c,r] the renderer placed at world (wx,wy) — for the debug grid overlay.
|
| 1988 |
+
tileIndexAt(wx, wy, meta) {
|
| 1989 |
+
if (!meta) return null;
|
| 1990 |
+
const { field, M, SZ, x0, y0 } = meta;
|
| 1991 |
+
const fl = (x, y) => field[(y - y0 + M) * SZ + (x - x0 + M)] === 1;
|
| 1992 |
+
if (wx - x0 < 0 || wy - y0 < 0 || wx - x0 >= CHUNK3 || wy - y0 >= CHUNK3) return null;
|
| 1993 |
+
if (!fl(wx, wy)) return CORRUPT_DARK;
|
| 1994 |
+
const t = corruptTile(
|
| 1995 |
+
!fl(wx, wy - 1),
|
| 1996 |
+
!fl(wx + 1, wy),
|
| 1997 |
+
!fl(wx, wy + 1),
|
| 1998 |
+
!fl(wx - 1, wy),
|
| 1999 |
+
!fl(wx - 1, wy - 1),
|
| 2000 |
+
!fl(wx + 1, wy - 1),
|
| 2001 |
+
!fl(wx - 1, wy + 1),
|
| 2002 |
+
!fl(wx + 1, wy + 1)
|
| 2003 |
+
);
|
| 2004 |
+
return t || CORRUPT_LIGHT;
|
| 2005 |
+
}
|
| 2006 |
+
});
|
| 2007 |
+
|
| 2008 |
+
// ../auto-battler/src/render/biomeRegistry.js
|
| 2009 |
+
var BIOME_FACTORIES = {
|
| 2010 |
+
forgottenPlains: fpConfig,
|
| 2011 |
+
orc: orcConfig,
|
| 2012 |
+
necropolis: necropolisConfig
|
| 2013 |
+
};
|
| 2014 |
+
var BIOME_GROUNDS = {
|
| 2015 |
+
forgottenPlains: { fills: FP_GROUND_FILLS, fillKey: fpGroundFillKey },
|
| 2016 |
+
orc: { fills: ORC_GROUND_FILLS, fillKey: orcGroundFillKey },
|
| 2017 |
+
necropolis: { fills: NECRO_GROUND_FILLS, fillKey: necroGroundFillKey }
|
| 2018 |
+
};
|
| 2019 |
+
|
| 2020 |
+
// ../auto-battler/src/render/transitionAtlas.js
|
| 2021 |
+
var VARIANTS = 3;
|
| 2022 |
+
function loadImg3(url) {
|
| 2023 |
+
return new Promise((resolve, reject) => {
|
| 2024 |
+
const img = new Image();
|
| 2025 |
+
img.onload = () => resolve(img);
|
| 2026 |
+
img.onerror = reject;
|
| 2027 |
+
img.src = url;
|
| 2028 |
+
});
|
| 2029 |
+
}
|
| 2030 |
+
function tilePixels(img, c, r) {
|
| 2031 |
+
const cv = document.createElement("canvas");
|
| 2032 |
+
cv.width = TILE;
|
| 2033 |
+
cv.height = TILE;
|
| 2034 |
+
const g = cv.getContext("2d", { willReadFrequently: true });
|
| 2035 |
+
g.imageSmoothingEnabled = false;
|
| 2036 |
+
g.drawImage(img, c * TILE, r * TILE, TILE, TILE, 0, 0, TILE, TILE);
|
| 2037 |
+
return g.getImageData(0, 0, TILE, TILE).data;
|
| 2038 |
+
}
|
| 2039 |
+
async function buildTransitionAtlas({ grounds, ids }) {
|
| 2040 |
+
const imgCache = /* @__PURE__ */ new Map(), pxCache = /* @__PURE__ */ new Map();
|
| 2041 |
+
const getPx = async (url, tile) => {
|
| 2042 |
+
const k = url + "|" + tile[0] + "," + tile[1];
|
| 2043 |
+
if (pxCache.has(k)) return pxCache.get(k);
|
| 2044 |
+
let img = imgCache.get(url);
|
| 2045 |
+
if (!img) {
|
| 2046 |
+
img = await loadImg3(url);
|
| 2047 |
+
imgCache.set(url, img);
|
| 2048 |
+
}
|
| 2049 |
+
const p = tilePixels(img, tile[0], tile[1]);
|
| 2050 |
+
pxCache.set(k, p);
|
| 2051 |
+
return p;
|
| 2052 |
+
};
|
| 2053 |
+
const combos = [];
|
| 2054 |
+
for (const A of ids) for (const B of ids) {
|
| 2055 |
+
if (A === B) continue;
|
| 2056 |
+
for (const fa of grounds[A].fills) for (const fb of grounds[B].fills) combos.push({ A, B, fa, fb });
|
| 2057 |
+
}
|
| 2058 |
+
for (const c of combos) {
|
| 2059 |
+
c.fp = await getPx(c.fa.url, c.fa.tile);
|
| 2060 |
+
c.bp = await getPx(c.fb.url, c.fb.tile);
|
| 2061 |
+
}
|
| 2062 |
+
const rowOf = /* @__PURE__ */ new Map();
|
| 2063 |
+
combos.forEach((c, i) => rowOf.set(c.A + "|" + c.fa.key + "|" + c.B + "|" + c.fb.key, i));
|
| 2064 |
+
const caseCol = /* @__PURE__ */ new Map();
|
| 2065 |
+
STENCIL_CASES.forEach((k, i) => caseCol.set(k, i));
|
| 2066 |
+
const canvas = document.createElement("canvas");
|
| 2067 |
+
canvas.width = STENCIL_CASES.length * VARIANTS * TILE;
|
| 2068 |
+
canvas.height = combos.length * TILE;
|
| 2069 |
+
const ctx = canvas.getContext("2d");
|
| 2070 |
+
ctx.imageSmoothingEnabled = false;
|
| 2071 |
+
for (let r = 0; r < combos.length; r++) {
|
| 2072 |
+
const { fp, bp } = combos[r];
|
| 2073 |
+
for (let ci = 0; ci < STENCIL_CASES.length; ci++) {
|
| 2074 |
+
for (let v = 0; v < VARIANTS; v++) {
|
| 2075 |
+
const mask = makeStencil(STENCIL_CASES[ci], v);
|
| 2076 |
+
const img = ctx.createImageData(TILE, TILE);
|
| 2077 |
+
for (let p = 0; p < TILE * TILE; p++) {
|
| 2078 |
+
const src = mask[p] ? fp : bp, o = p * 4;
|
| 2079 |
+
img.data[o] = src[o];
|
| 2080 |
+
img.data[o + 1] = src[o + 1];
|
| 2081 |
+
img.data[o + 2] = src[o + 2];
|
| 2082 |
+
img.data[o + 3] = src[o + 3];
|
| 2083 |
+
}
|
| 2084 |
+
ctx.putImageData(img, (ci * VARIANTS + v) * TILE, r * TILE);
|
| 2085 |
+
}
|
| 2086 |
+
}
|
| 2087 |
+
}
|
| 2088 |
+
const tileAt = (fgId, fgKey, bgId, bgKey, caseKey, variant) => {
|
| 2089 |
+
const row = rowOf.get(fgId + "|" + fgKey + "|" + bgId + "|" + bgKey);
|
| 2090 |
+
const ci = caseCol.get(caseKey);
|
| 2091 |
+
if (row === void 0 || ci === void 0) return null;
|
| 2092 |
+
return [ci * VARIANTS + (variant % VARIANTS + VARIANTS) % VARIANTS, row];
|
| 2093 |
+
};
|
| 2094 |
+
return { canvas, tileAt, variants: VARIANTS };
|
| 2095 |
+
}
|
| 2096 |
+
|
| 2097 |
+
// ../auto-battler/src/render/overworld.js
|
| 2098 |
+
var TILE5 = 8;
|
| 2099 |
+
var CHUNK4 = 32;
|
| 2100 |
+
var MACRO_BLEND = 0.06;
|
| 2101 |
+
var REGION_TINT = {
|
| 2102 |
+
forgottenPlains: [97, 150, 55],
|
| 2103 |
+
// meadow green
|
| 2104 |
+
orc: [150, 128, 95],
|
| 2105 |
+
// dirt tan
|
| 2106 |
+
necropolis: [120, 110, 128]
|
| 2107 |
+
// corrupted grey-purple
|
| 2108 |
+
};
|
| 2109 |
+
var TINT_MIX = 0.4;
|
| 2110 |
+
var RANK = {};
|
| 2111 |
+
OVERWORLD_BIOMES.forEach((id, i) => {
|
| 2112 |
+
RANK[id] = i;
|
| 2113 |
+
});
|
| 2114 |
+
var overworldConfig = (seed, opts = {}) => {
|
| 2115 |
+
const region = opts.regionFn ?? biomeRegion;
|
| 2116 |
+
const biomes = {};
|
| 2117 |
+
for (const id of OVERWORLD_BIOMES) {
|
| 2118 |
+
const factory = BIOME_FACTORIES[id];
|
| 2119 |
+
if (!factory) throw new Error(`overworld: no biome config registered for "${id}"`);
|
| 2120 |
+
biomes[id] = factory(seed);
|
| 2121 |
+
}
|
| 2122 |
+
return {
|
| 2123 |
+
seed,
|
| 2124 |
+
tile: TILE5,
|
| 2125 |
+
chunk: CHUNK4,
|
| 2126 |
+
background: "#2b2f3a",
|
| 2127 |
+
bounds: opts.bounds,
|
| 2128 |
+
initialCamera: opts.initialCamera,
|
| 2129 |
+
// Each biome loads its own sheets into its own ctx; then build the cross-biome transition atlas
|
| 2130 |
+
// from their base grounds. Key everything for bake to route to.
|
| 2131 |
+
async load(api) {
|
| 2132 |
+
const ctxById = {};
|
| 2133 |
+
for (const id of OVERWORLD_BIOMES) ctxById[id] = await biomes[id].load(api);
|
| 2134 |
+
const atlas = await buildTransitionAtlas({ grounds: BIOME_GROUNDS, ids: OVERWORLD_BIOMES });
|
| 2135 |
+
const texture = api.Texture.from(atlas.canvas);
|
| 2136 |
+
texture.source.scaleMode = "nearest";
|
| 2137 |
+
return { ctxById, trans: { source: texture.source, tileAt: atlas.tileAt, variants: atlas.variants } };
|
| 2138 |
+
},
|
| 2139 |
+
// Bake a chunk: assign each tile to its (crisp) region winner, let each present biome paint its
|
| 2140 |
+
// region, then overpaint the 1-tile border ring with generated transition tiles so neighbouring
|
| 2141 |
+
// biomes blend cleanly. Fast path: a single-biome chunk = one bake call, no mask, no seams.
|
| 2142 |
+
bake(api) {
|
| 2143 |
+
const { x0, y0, seed: seed2, ctx, add } = api;
|
| 2144 |
+
const GW = CHUNK4 + 2;
|
| 2145 |
+
const winner = new Array(GW * GW);
|
| 2146 |
+
const widx = (lx, ly) => (ly + 1) * GW + (lx + 1);
|
| 2147 |
+
for (let ly = -1; ly <= CHUNK4; ly++) for (let lx = -1; lx <= CHUNK4; lx++) {
|
| 2148 |
+
winner[widx(lx, ly)] = region(seed2, x0 + lx, y0 + ly).id;
|
| 2149 |
+
}
|
| 2150 |
+
const at = (lx, ly) => winner[widx(lx, ly)];
|
| 2151 |
+
const present = /* @__PURE__ */ new Set();
|
| 2152 |
+
for (let ty = 0; ty < CHUNK4; ty++) for (let tx = 0; tx < CHUNK4; tx++) present.add(at(tx, ty));
|
| 2153 |
+
const single = present.size === 1;
|
| 2154 |
+
const live = [];
|
| 2155 |
+
const metaById = {};
|
| 2156 |
+
for (const id of OVERWORLD_BIOMES) {
|
| 2157 |
+
if (!present.has(id)) continue;
|
| 2158 |
+
const accept = single ? void 0 : (tx, ty) => at(tx, ty) === id;
|
| 2159 |
+
const res = biomes[id].bake({ ...api, ctx: ctx.ctxById[id], accept }) || {};
|
| 2160 |
+
if (res.live) for (const l of res.live) live.push(l);
|
| 2161 |
+
if (res.meta) metaById[id] = res.meta;
|
| 2162 |
+
}
|
| 2163 |
+
const trans = ctx.trans;
|
| 2164 |
+
if (trans && !single) {
|
| 2165 |
+
for (let ty = 0; ty < CHUNK4; ty++) for (let tx = 0; tx < CHUNK4; tx++) {
|
| 2166 |
+
const A = at(tx, ty), rankA = RANK[A];
|
| 2167 |
+
const below = (lx, ly) => {
|
| 2168 |
+
const b = at(lx, ly);
|
| 2169 |
+
return b !== A && RANK[b] < rankA;
|
| 2170 |
+
};
|
| 2171 |
+
const key = boundaryCase(
|
| 2172 |
+
below(tx, ty - 1),
|
| 2173 |
+
below(tx + 1, ty),
|
| 2174 |
+
below(tx, ty + 1),
|
| 2175 |
+
below(tx - 1, ty),
|
| 2176 |
+
below(tx - 1, ty - 1),
|
| 2177 |
+
below(tx + 1, ty - 1),
|
| 2178 |
+
below(tx - 1, ty + 1),
|
| 2179 |
+
below(tx + 1, ty + 1)
|
| 2180 |
+
);
|
| 2181 |
+
if (!key) continue;
|
| 2182 |
+
const B = dominantForeign(at, tx, ty, A, rankA);
|
| 2183 |
+
if (!B) continue;
|
| 2184 |
+
const wx = x0 + tx, wy = y0 + ty;
|
| 2185 |
+
const fgKey = BIOME_GROUNDS[A].fillKey(seed2, wx, wy);
|
| 2186 |
+
const bgKey = BIOME_GROUNDS[B].fillKey(seed2, wx, wy);
|
| 2187 |
+
const variant = hashU32(seed2 ^ 1957, wx, wy) % trans.variants;
|
| 2188 |
+
const cr = trans.tileAt(A, fgKey, B, bgKey, key, variant);
|
| 2189 |
+
if (cr) add(trans.source, cr[0], cr[1], tx, ty);
|
| 2190 |
+
}
|
| 2191 |
+
}
|
| 2192 |
+
return { live, meta: { winner, GW, metaById } };
|
| 2193 |
+
},
|
| 2194 |
+
// Zoomed-out world map: a flat region tint (so lands stay legible at any zoom) modulated by a
|
| 2195 |
+
// little of the biome's internal terrain colour, cross-fading into the runner-up region across
|
| 2196 |
+
// a thin border band so regions read as soft-edged lands, not hard cells.
|
| 2197 |
+
macroColor(seed2, tx, ty) {
|
| 2198 |
+
const { id, id2, edge } = region(seed2, tx, ty);
|
| 2199 |
+
let tint = REGION_TINT[id];
|
| 2200 |
+
if (edge < MACRO_BLEND && id !== id2) tint = lerp3(tint, REGION_TINT[id2], 0.5 * (1 - edge / MACRO_BLEND));
|
| 2201 |
+
return lerp3(tint, biomes[id].macroColor(seed2, tx, ty), TINT_MIX);
|
| 2202 |
+
},
|
| 2203 |
+
// Which biome owns a world tile (no chunk meta needed) — for callers that key behaviour to the
|
| 2204 |
+
// land the player is on (free-roam enemy rosters, per-biome walkability).
|
| 2205 |
+
biomeAt(seed2, wx, wy) {
|
| 2206 |
+
return region(seed2, wx, wy).id;
|
| 2207 |
+
},
|
| 2208 |
+
// Debug grid overlay: dispatch to whichever biome owns the tile, with that biome's chunk meta.
|
| 2209 |
+
tileIndexAt(wx, wy, meta) {
|
| 2210 |
+
if (!meta) return null;
|
| 2211 |
+
const tx = (wx % CHUNK4 + CHUNK4) % CHUNK4, ty = (wy % CHUNK4 + CHUNK4) % CHUNK4;
|
| 2212 |
+
const id = meta.winner[(ty + 1) * meta.GW + (tx + 1)];
|
| 2213 |
+
const bm = meta.metaById[id];
|
| 2214 |
+
return bm ? biomes[id].tileIndexAt(wx, wy, bm) : null;
|
| 2215 |
+
}
|
| 2216 |
+
};
|
| 2217 |
+
};
|
| 2218 |
+
function dominantForeign(at, tx, ty, A, rankA) {
|
| 2219 |
+
const tally = /* @__PURE__ */ new Map();
|
| 2220 |
+
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
|
| 2221 |
+
if (!dx && !dy) continue;
|
| 2222 |
+
const b = at(tx + dx, ty + dy);
|
| 2223 |
+
if (b !== A && RANK[b] < rankA) tally.set(b, (tally.get(b) || 0) + 1);
|
| 2224 |
+
}
|
| 2225 |
+
let best = null, bestN = 0;
|
| 2226 |
+
for (const [b, n] of tally) if (n > bestN) {
|
| 2227 |
+
bestN = n;
|
| 2228 |
+
best = b;
|
| 2229 |
+
}
|
| 2230 |
+
return best;
|
| 2231 |
+
}
|
| 2232 |
+
var GAME_W = 510;
|
| 2233 |
+
var GAME_H = 340;
|
| 2234 |
+
var BAND_IDS = ["forgottenPlains", "orc", "necropolis"];
|
| 2235 |
+
var BAND_WARP_SCALE = 0.014;
|
| 2236 |
+
var BAND_WARP_AMP = 26;
|
| 2237 |
+
var BAND_EDGE_TILES = 50;
|
| 2238 |
+
var bandSub = (seed, salt) => (Math.imul(seed | 0, 2654435761) ^ (salt | 0)) >>> 0;
|
| 2239 |
+
function bandedRegion(W) {
|
| 2240 |
+
const b1 = W / 3, b2 = 2 * W / 3;
|
| 2241 |
+
return (seed, x, y) => {
|
| 2242 |
+
const wx = x + BAND_WARP_AMP * (fbm(bandSub(seed, 1441), x * BAND_WARP_SCALE, y * BAND_WARP_SCALE) - 0.5);
|
| 2243 |
+
let i, j, d;
|
| 2244 |
+
if (wx < b1) {
|
| 2245 |
+
i = 0;
|
| 2246 |
+
j = 1;
|
| 2247 |
+
d = b1 - wx;
|
| 2248 |
+
} else if (wx >= b2) {
|
| 2249 |
+
i = 2;
|
| 2250 |
+
j = 1;
|
| 2251 |
+
d = wx - b2;
|
| 2252 |
+
} else {
|
| 2253 |
+
i = 1;
|
| 2254 |
+
const dl = wx - b1, dr = b2 - wx;
|
| 2255 |
+
if (dl < dr) {
|
| 2256 |
+
j = 0;
|
| 2257 |
+
d = dl;
|
| 2258 |
+
} else {
|
| 2259 |
+
j = 2;
|
| 2260 |
+
d = dr;
|
| 2261 |
+
}
|
| 2262 |
+
}
|
| 2263 |
+
const edge = Math.max(0, Math.min(1, d / BAND_EDGE_TILES));
|
| 2264 |
+
return { id: BAND_IDS[i], id2: BAND_IDS[j], edge };
|
| 2265 |
+
};
|
| 2266 |
+
}
|
| 2267 |
+
function createGameOverworldMap(pixi, host, opts = {}) {
|
| 2268 |
+
const config = overworldConfig(opts.seed ?? 1, {
|
| 2269 |
+
regionFn: bandedRegion(GAME_W),
|
| 2270 |
+
bounds: { x0: 0, y0: 0, x1: GAME_W, y1: GAME_H },
|
| 2271 |
+
// Start the view near the left (Forgotten Plains) edge, vertically centred.
|
| 2272 |
+
initialCamera: { x: 64 * TILE5, y: GAME_H / 2 * TILE5 }
|
| 2273 |
+
});
|
| 2274 |
+
return createChunkedMap(pixi, host, { ...config, keyboardPan: opts.keyboardPan });
|
| 2275 |
}
|
| 2276 |
|
| 2277 |
// ../auto-battler/src/engine/skills.js
|
|
|
|
| 3216 |
const actorOf = (id) => getBattle()?.actors.find((x) => x.id === id);
|
| 3217 |
const floats = [];
|
| 3218 |
const sheetsById = {};
|
| 3219 |
+
async function loadSheets(id, def) {
|
| 3220 |
if (!def?.idle) {
|
| 3221 |
sheetsById[id] = null;
|
| 3222 |
+
return;
|
| 3223 |
}
|
| 3224 |
try {
|
| 3225 |
const load = (url) => url ? recoloredTexture(Texture, url, def.idle, def.recolor) : null;
|
|
|
|
| 3245 |
sheetsById[id] = null;
|
| 3246 |
}
|
| 3247 |
}
|
| 3248 |
+
for (const [id, def] of Object.entries(defsById)) await loadSheets(id, def);
|
| 3249 |
const skillIcons = {};
|
| 3250 |
for (const s of CB_SKILLS) {
|
| 3251 |
try {
|
|
|
|
| 3281 |
} catch {
|
| 3282 |
}
|
| 3283 |
const skillPlay = {};
|
| 3284 |
+
async function buildSkillPlay(id, def) {
|
| 3285 |
const cell = sheetsById[id]?.cell;
|
| 3286 |
const map = {};
|
| 3287 |
for (const [skillId, fxCfg] of Object.entries(def.skillFx || {})) {
|
|
|
|
| 3306 |
}
|
| 3307 |
skillPlay[id] = map;
|
| 3308 |
}
|
| 3309 |
+
for (const [id, def] of Object.entries(defsById)) await buildSkillPlay(id, def);
|
| 3310 |
const view = {};
|
| 3311 |
+
function buildView(id, def) {
|
| 3312 |
const sh = sheetsById[id];
|
| 3313 |
const c = new Container();
|
| 3314 |
let sp;
|
|
|
|
| 3357 |
units.addChild(c);
|
| 3358 |
view[id] = { def, sheets: sh, container: c, sprite: sp, overlay, overlayKey: null, bars, status, statusSprites: {}, outline, castOutline, mode: null, facing: null, flash: 0, dead: false };
|
| 3359 |
}
|
| 3360 |
+
for (const [id, def] of Object.entries(defsById)) buildView(id, def);
|
| 3361 |
+
async function addActor(id, def) {
|
| 3362 |
+
if (view[id]) return view[id];
|
| 3363 |
+
defsById[id] = def;
|
| 3364 |
+
await loadSheets(id, def);
|
| 3365 |
+
await buildSkillPlay(id, def);
|
| 3366 |
+
buildView(id, def);
|
| 3367 |
+
return view[id];
|
| 3368 |
+
}
|
| 3369 |
+
function removeActor2(id) {
|
| 3370 |
+
const v = view[id];
|
| 3371 |
+
if (!v) return;
|
| 3372 |
+
units.removeChild(v.container);
|
| 3373 |
+
v.container.destroy({ children: true });
|
| 3374 |
+
delete view[id];
|
| 3375 |
+
delete sheetsById[id];
|
| 3376 |
+
delete skillPlay[id];
|
| 3377 |
+
delete defsById[id];
|
| 3378 |
+
}
|
| 3379 |
function setLoop(v, mode, facing) {
|
| 3380 |
if (v.mode === mode && v.facing === facing) return;
|
| 3381 |
v.mode = mode;
|
|
|
|
| 3751 |
floats,
|
| 3752 |
actorOf,
|
| 3753 |
skillPlay,
|
| 3754 |
+
addActor,
|
| 3755 |
+
removeActor: removeActor2,
|
| 3756 |
setLoop,
|
| 3757 |
playOnce,
|
| 3758 |
resume,
|
|
|
|
| 3887 |
// optional: idle until a foe is within this distance (else always engage)
|
| 3888 |
};
|
| 3889 |
}
|
| 3890 |
+
function makeTeamBattle({ seed = 1, players = [], enemies = [], sandbox = false, respawnDummies = 0, freeCast = false, world = null, field = null } = {}) {
|
| 3891 |
const actors = [];
|
| 3892 |
players.filter(Boolean).slice(0, 5).forEach((u, i) => actors.push(makeActor(u, "player", `P${i}`, i)));
|
| 3893 |
enemies.filter(Boolean).slice(0, 5).forEach((u, i) => actors.push(makeActor(u, "enemy", `E${i}`, i)));
|
| 3894 |
+
return { t: 0, rng: makeRng(seed), actors, projectiles: [], log: [], over: false, winner: null, sandbox, respawnDummies, freeCast, input: {}, world, field: field || FIELD };
|
| 3895 |
+
}
|
| 3896 |
+
function spawnActor(b, unit, team, id) {
|
| 3897 |
+
const a = makeActor(unit, team, id, 0);
|
| 3898 |
+
a.attackTimer = a.weapon.interval;
|
| 3899 |
+
b.actors.push(a);
|
| 3900 |
+
return a;
|
| 3901 |
+
}
|
| 3902 |
+
function removeActor(b, id) {
|
| 3903 |
+
const i = b.actors.findIndex((a) => a.id === id);
|
| 3904 |
+
if (i >= 0) b.actors.splice(i, 1);
|
| 3905 |
}
|
| 3906 |
function setInput(b, id, cmd) {
|
| 3907 |
if (!b.input) b.input = {};
|
|
|
|
| 3916 |
var MAX_BATTLE_T = 90;
|
| 3917 |
var COLLISION_Y_WEIGHT = 3.2;
|
| 3918 |
var radiusOf = (unit, tpl) => unit.template?.radius ?? unit.stats?.radius ?? tpl.radius ?? BODY_RADIUS[tpl.role] ?? DEFAULT_RADIUS;
|
| 3919 |
+
var edgeGap = (a, t) => dist2(a, t) - (a.radius || 0) - (t.radius || 0);
|
| 3920 |
var MELEE_REACH = 2;
|
| 3921 |
var reachOf = (a) => a.role === "ranged" ? a.weapon.range : MELEE_REACH;
|
| 3922 |
var SPELL_RANGE = 900;
|
| 3923 |
var ARMOR_IGNORING = /* @__PURE__ */ new Set(["shadow", "holy", "dark", "chaos"]);
|
| 3924 |
+
var dist2 = (a, e) => Math.hypot(a.x - e.x, a.y - e.y);
|
| 3925 |
var hasCond = (a, type) => a.conds.some((c) => c.type === type);
|
| 3926 |
var isKd = (b, a) => a.kd > b.t;
|
| 3927 |
var gainAdr = (a, n) => {
|
|
|
|
| 3932 |
function nearestFoe(b, a) {
|
| 3933 |
let best = null, bd = Infinity;
|
| 3934 |
for (const x of livingFoes(b, a)) {
|
| 3935 |
+
const d = dist2(a, x);
|
| 3936 |
if (d < bd) {
|
| 3937 |
bd = d;
|
| 3938 |
best = x;
|
|
|
|
| 3952 |
}
|
| 3953 |
return best;
|
| 3954 |
}
|
| 3955 |
+
var adjacentTo = (b, tgt) => b.actors.filter((x) => x.alive && x.team === tgt.team && x !== tgt && dist2(x, tgt) <= ADJACENT_GW);
|
| 3956 |
var addMod = (b, a, m) => a.mods.push({ until: b.t + (m.dur ?? 0), ...m });
|
| 3957 |
var activeMods = (b, a, kind) => a.mods.filter((m) => m.kind === kind && m.until > b.t);
|
| 3958 |
var hasModKind = (b, a, kind) => a.mods.some((m) => m.kind === kind && m.until > b.t);
|
|
|
|
| 4194 |
}
|
| 4195 |
}
|
| 4196 |
function shadowStep(b, a, tgt) {
|
| 4197 |
+
const f = b.field || FIELD;
|
| 4198 |
const dx = a.x - tgt.x, dy = a.y - tgt.y, len = Math.hypot(dx, dy) || 1;
|
| 4199 |
+
a.x = Math.max(0, Math.min(f.w, tgt.x + dx / len * (BASIC_MELEE_GW * 0.8)));
|
| 4200 |
+
a.y = Math.max(0, Math.min(f.h, tgt.y + dy / len * (BASIC_MELEE_GW * 0.8)));
|
| 4201 |
}
|
| 4202 |
function fireSpellProjectiles(b, src, tgt, amt, e, n) {
|
| 4203 |
+
const base = dist2(src, tgt) / 900;
|
| 4204 |
for (let i = 0; i < n; i++) {
|
| 4205 |
b.projectiles.push({
|
| 4206 |
srcId: src.id,
|
|
|
|
| 4277 |
}
|
| 4278 |
}
|
| 4279 |
if (a.role === "ranged") {
|
| 4280 |
+
const flight = dist2(a, enemy) / (a.weapon.projSpeed || 800);
|
| 4281 |
b.projectiles.push({ srcId: a.id, tgtId: enemy.id, fromX: a.x, fromY: a.y, aimX: enemy.x, aimY: enemy.y, bornT: b.t, hitT: b.t + flight, weaponDmg, bonus, s, empEffect });
|
| 4282 |
log(b, "shoot", a, { name: s ? s.name : "shot", skillId: s?.id });
|
| 4283 |
} else {
|
|
|
|
| 4390 |
if (s.cost?.energy && a.energy < s.cost.energy) return false;
|
| 4391 |
if (s.cost?.adrenaline && a.adrenaline < s.cost.adrenaline) return false;
|
| 4392 |
if (isAttack(s) && edgeGap(a, foe) > reachOf(a)) return false;
|
| 4393 |
+
if (!isAttack(s) && !isSupport(s) && dist2(a, foe) > SPELL_RANGE) return false;
|
| 4394 |
for (const r of s.requires || []) {
|
| 4395 |
if (r === "on_hit") continue;
|
| 4396 |
if (r.combo_follows && a.marks[foe.id]?.stage !== r.combo_follows) return false;
|
|
|
|
| 4424 |
return null;
|
| 4425 |
}
|
| 4426 |
function moveActor(b, a, enemy, dt) {
|
| 4427 |
+
const d = dist2(a, enemy);
|
| 4428 |
let toward = 0;
|
| 4429 |
if (a.role === "ranged") {
|
| 4430 |
if (d < (a.preferredRange ?? a.weapon.range * 0.7)) toward = -1;
|
|
|
|
| 4498 |
}
|
| 4499 |
var clampField = (v, r, max) => Math.max(r, Math.min(max - r, v));
|
| 4500 |
function stepMove(b, a, vx, vy, dt) {
|
| 4501 |
+
const f = b.field || FIELD;
|
| 4502 |
const w = b.world && b.world.walkable;
|
| 4503 |
+
let nx = clampField(a.x + vx * dt, a.radius, f.w);
|
| 4504 |
+
let ny = clampField(a.y + vy * dt, a.radius, f.h);
|
| 4505 |
if (w) {
|
| 4506 |
if (!w(nx, a.y)) nx = a.x;
|
| 4507 |
if (!w(nx, ny)) ny = a.y;
|
|
|
|
| 4510 |
a.y = ny;
|
| 4511 |
}
|
| 4512 |
function resolveOverlaps(b) {
|
| 4513 |
+
const f = b.field || FIELD;
|
| 4514 |
const live = b.actors.filter((a) => a.alive);
|
| 4515 |
for (let it = 0; it < DEOVERLAP_ITERS; it++) {
|
| 4516 |
for (let i = 0; i < live.length; i++) {
|
|
|
|
| 4526 |
const aShare = aFix ? 0 : oFix ? 1 : 0.5;
|
| 4527 |
const oShare = oFix ? 0 : aFix ? 1 : 0.5;
|
| 4528 |
const yPush = uy / COLLISION_Y_WEIGHT;
|
| 4529 |
+
a.x = clampField(a.x - ux * push * aShare, a.radius, f.w);
|
| 4530 |
+
a.y = clampField(a.y - yPush * push * aShare, a.radius, f.h);
|
| 4531 |
+
o.x = clampField(o.x + ux * push * oShare, o.radius, f.w);
|
| 4532 |
+
o.y = clampField(o.y + yPush * push * oShare, o.radius, f.h);
|
| 4533 |
}
|
| 4534 |
}
|
| 4535 |
}
|
|
|
|
| 4630 |
if (!a.alive || b.over) continue;
|
| 4631 |
const enemy = nearestFoe(b, a);
|
| 4632 |
if (!enemy && a.control !== "player") continue;
|
| 4633 |
+
if (a.aggroRadius != null && a.control !== "player" && dist2(a, enemy) > a.aggroRadius) {
|
| 4634 |
a.moving = false;
|
| 4635 |
continue;
|
| 4636 |
}
|
|
|
|
| 4689 |
}
|
| 4690 |
|
| 4691 |
// ../auto-battler/src/sim/walkable.js
|
| 4692 |
+
function makeRoamWalkable(seed, biomeAt) {
|
| 4693 |
+
return (wx, wy) => {
|
| 4694 |
+
const b = biomeAt ? biomeAt(wx, wy) : "forgottenPlains";
|
| 4695 |
+
if (b === "necropolis") return !isRiver2(seed, wx, wy) && !isRaised2(seed, wx, wy);
|
| 4696 |
+
if (b === "orc") return true;
|
| 4697 |
+
return !isRiver(seed, wx, wy) && !isRaised(seed, wx, wy);
|
| 4698 |
+
};
|
| 4699 |
}
|
| 4700 |
|
| 4701 |
// ../auto-battler/src/render/comboBattler.js
|
| 4702 |
+
var TILE6 = 8;
|
| 4703 |
var SPRITE_TILES = 1.9;
|
| 4704 |
var STEP = 0.05;
|
| 4705 |
+
var SPAWN_TILES = 22;
|
| 4706 |
+
var SPAWN_JITTER = 6;
|
| 4707 |
+
var DESPAWN_TILES = 52;
|
| 4708 |
+
var TARGET_FOES = 8;
|
| 4709 |
+
var MAX_FOES = 16;
|
| 4710 |
+
var SPAWN_INTERVAL = 0.4;
|
| 4711 |
+
var DEATH_LINGER = 1.6;
|
| 4712 |
async function contentHeight(url) {
|
| 4713 |
try {
|
| 4714 |
const img = await new Promise((res, rej) => {
|
|
|
|
| 4738 |
const { Graphics, Container } = pixi;
|
| 4739 |
const seed = (opts.seed ?? 1) >>> 0;
|
| 4740 |
const G = opts.groundScale ?? 0.3;
|
| 4741 |
+
const ARENA = { ox: 0, oy: 0 };
|
| 4742 |
+
const fieldToWorld = (fx, fy) => ({ x: ARENA.ox + fx * G, y: ARENA.oy + fy * G });
|
| 4743 |
+
const worldToField = (wx, wy) => ({ x: (wx - ARENA.ox) / G, y: (wy - ARENA.oy) / G });
|
| 4744 |
+
const map = createGameOverworldMap(pixi, host, { seed, keyboardPan: false });
|
| 4745 |
+
const roamWalkable = makeRoamWalkable(seed, (wx, wy) => map.biomeAt(wx, wy));
|
| 4746 |
+
const world = { walkable: (fx, fy) => {
|
| 4747 |
+
const w = fieldToWorld(fx, fy);
|
| 4748 |
+
return roamWalkable(Math.round(w.x / TILE6), Math.round(w.y / TILE6));
|
| 4749 |
+
} };
|
| 4750 |
+
const rosters = opts.rosters || (() => {
|
| 4751 |
+
const e = opts.enemies || [];
|
| 4752 |
+
return { forgottenPlains: e, orc: e, necropolis: e };
|
| 4753 |
+
})();
|
| 4754 |
+
let battle = null, R = null, combatRoot = null, rings = null, markers = null, spawnFn = null, dead = false;
|
| 4755 |
const depthScale = { v: 2 };
|
| 4756 |
let offTick = null, keyHandlers = null, tapHandlers = null, controlsEl = null, alive = true;
|
| 4757 |
const keys = { x: 0, y: 0 };
|
|
|
|
| 4762 |
const listeners = /* @__PURE__ */ new Set();
|
| 4763 |
const pa = () => battle?.actors.find((a) => a.id === "P0") || null;
|
| 4764 |
const ea = () => battle?.actors.filter((a) => a.team === "enemy") || [];
|
| 4765 |
+
const wtile = (fx, fy) => {
|
| 4766 |
+
const w = fieldToWorld(fx, fy);
|
| 4767 |
+
return { wx: Math.round(w.x / TILE6), wy: Math.round(w.y / TILE6) };
|
| 4768 |
+
};
|
| 4769 |
const snap = () => ({
|
| 4770 |
+
player: pa() ? { x: pa().x, y: pa().y, hp: pa().hp, ...wtile(pa().x, pa().y) } : null,
|
| 4771 |
+
enemies: ea().map((a) => ({ x: a.x, y: a.y, hp: Math.round(a.hp), alive: a.alive, ...wtile(a.x, a.y) })),
|
| 4772 |
+
over: dead || !!battle?.over
|
| 4773 |
});
|
| 4774 |
const emit = () => listeners.forEach((fn) => fn(snap()));
|
| 4775 |
const KEYMAP = { w: [0, -1], s: [0, 1], a: [-1, 0], d: [1, 0], arrowup: [0, -1], arrowdown: [0, 1], arrowleft: [-1, 0], arrowright: [1, 0] };
|
|
|
|
| 4830 |
downAt = null;
|
| 4831 |
if (moved > 8 || elapsed > 500) return;
|
| 4832 |
const r = canvas.getBoundingClientRect(), w = map.screenToWorld(e.clientX - r.left, e.clientY - r.top);
|
| 4833 |
+
moveTarget = worldToField(w.x, w.y);
|
| 4834 |
};
|
| 4835 |
canvas.addEventListener("pointerdown", onDown);
|
| 4836 |
window.addEventListener("pointerup", onUp);
|
|
|
|
| 4863 |
host.appendChild(bar);
|
| 4864 |
controlsEl = bar;
|
| 4865 |
}
|
| 4866 |
+
const dist3 = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
| 4867 |
const nearestFoeDist = () => {
|
| 4868 |
const p = pa();
|
| 4869 |
if (!p) return Infinity;
|
| 4870 |
let m = Infinity;
|
| 4871 |
+
for (const e of ea()) if (e.alive) m = Math.min(m, dist3(p, e));
|
| 4872 |
return m;
|
| 4873 |
};
|
| 4874 |
function tick(ticker) {
|
|
|
|
| 4899 |
step(battle, STEP);
|
| 4900 |
acc.t -= STEP;
|
| 4901 |
}
|
| 4902 |
+
if (!dead && p && !p.alive) {
|
| 4903 |
+
dead = true;
|
| 4904 |
+
}
|
| 4905 |
+
spawnFn && spawnFn(dt);
|
| 4906 |
R.syncActors(battle, dtMS, battle.t);
|
| 4907 |
R.updateFloats(dtMS);
|
| 4908 |
R.drawProjectiles(battle);
|
| 4909 |
R.processLog(battle, cursor);
|
| 4910 |
drawRings();
|
| 4911 |
+
drawMarkers();
|
| 4912 |
emit();
|
| 4913 |
}
|
| 4914 |
function drawRings() {
|
|
|
|
| 4917 |
if (!p) return;
|
| 4918 |
rings.ellipse(p.x, p.y, p.weapon.range, p.weapon.range * 0.6).stroke({ width: 1.5 / G, color: 16777215, alpha: 0.25 });
|
| 4919 |
}
|
| 4920 |
+
function drawMarkers() {
|
| 4921 |
+
if (!markers) return;
|
| 4922 |
+
markers.clear();
|
| 4923 |
+
const a = 1 - Math.max(0, Math.min(1, (map.getCamera().zoom - 0.5) / (0.9 - 0.5)));
|
| 4924 |
+
if (a <= 0) return;
|
| 4925 |
+
const dot = (fx, fy, color) => {
|
| 4926 |
+
const w = fieldToWorld(fx, fy), s = map.worldToScreen(w.x, w.y);
|
| 4927 |
+
markers.circle(s.x, s.y, 4.5).fill({ color, alpha: a }).stroke({ width: 1.5, color: 1053723, alpha: a });
|
| 4928 |
+
};
|
| 4929 |
+
for (const e of ea()) if (e.alive) dot(e.x, e.y, 16726832);
|
| 4930 |
+
const p = pa();
|
| 4931 |
+
if (p) dot(p.x, p.y, 16766474);
|
| 4932 |
+
}
|
| 4933 |
const ready = (async () => {
|
| 4934 |
await map.ready;
|
| 4935 |
if (!alive) return;
|
| 4936 |
const cam = map.getCamera();
|
| 4937 |
+
const bnds = map.getBounds();
|
| 4938 |
+
ARENA.ox = bnds.x0;
|
| 4939 |
+
ARENA.oy = bnds.y0;
|
| 4940 |
+
const field = { w: (bnds.x1 - bnds.x0) / G, h: (bnds.y1 - bnds.y0) / G };
|
| 4941 |
+
const FSTEP = TILE6 / G;
|
| 4942 |
+
const snapField = (fx2, fy) => {
|
| 4943 |
+
for (let r = 0; r <= 24; r++) for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {
|
| 4944 |
+
if (Math.max(Math.abs(dx), Math.abs(dy)) !== r) continue;
|
| 4945 |
+
const x = fx2 + dx * FSTEP, y = fy + dy * FSTEP;
|
| 4946 |
+
if (x >= 0 && x <= field.w && y >= 0 && y <= field.h && world.walkable(x, y)) return { x, y };
|
| 4947 |
+
}
|
| 4948 |
+
return { x: Math.max(0, Math.min(field.w, fx2)), y: Math.max(0, Math.min(field.h, fy)) };
|
| 4949 |
};
|
|
|
|
|
|
|
| 4950 |
const pProf = opts.player?.unit?.profession;
|
| 4951 |
const pSkills = opts.player?.unit?.skills?.length ? opts.player.unit.skills : CB_SKILLS.filter((s) => s.profession === pProf).slice(0, 3).map((s) => s.id);
|
| 4952 |
const players = [{ ...opts.player?.unit || {}, name: opts.player?.name || "Hero", control: "player", skills: pSkills }];
|
| 4953 |
+
battle = makeTeamBattle({ seed, players, enemies: [], sandbox: true, freeCast: true, world, field });
|
| 4954 |
+
const start = worldToField(cam.x, cam.y);
|
| 4955 |
+
const p0 = snapField(start.x, start.y);
|
| 4956 |
const P = pa();
|
| 4957 |
if (P) {
|
| 4958 |
+
P.x = p0.x;
|
| 4959 |
+
P.y = p0.y;
|
| 4960 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4961 |
const defsById = {};
|
| 4962 |
const sd = (o) => ({ idle: o?.idle, walk: o?.walk, attack: o?.attack, dmg: o?.dmg, die: o?.die });
|
| 4963 |
defsById.P0 = { name: players[0].name, profession: players[0].profession, ...sd(opts.player?.sheets) };
|
|
|
|
|
|
|
|
|
|
| 4964 |
combatRoot = new Container();
|
| 4965 |
combatRoot.scale.set(G);
|
| 4966 |
+
combatRoot.position.set(ARENA.ox, ARENA.oy);
|
| 4967 |
rings = new Graphics();
|
| 4968 |
combatRoot.addChild(rings);
|
| 4969 |
const units = new Container();
|
|
|
|
| 4972 |
const proj = new Graphics();
|
| 4973 |
combatRoot.addChild(units, proj, fx);
|
| 4974 |
map.getEntityLayer().addChild(combatRoot);
|
| 4975 |
+
markers = new Graphics();
|
| 4976 |
+
map.getApp().stage.addChild(markers);
|
| 4977 |
const ch = await contentHeight(opts.player?.sheets?.idle || opts.player?.sheets?.walk);
|
| 4978 |
+
depthScale.v = SPRITE_TILES * TILE6 / (ch * G);
|
| 4979 |
R = await createCombatRenderer({ pixi, defsById, layers: { units, fx, projLayer: proj }, coords: { mapX: (x) => x, mapY: (y) => y, depthOf: () => depthScale.v }, getBattle: () => battle });
|
| 4980 |
if (!alive) return;
|
| 4981 |
for (const a of battle.actors) a.attackTimer = 0;
|
| 4982 |
+
let foeN = 0;
|
| 4983 |
+
const deadAt = /* @__PURE__ */ new Map();
|
| 4984 |
+
const spawnAcc = { t: SPAWN_INTERVAL };
|
| 4985 |
+
const rosterAt = (fx2, fy) => {
|
| 4986 |
+
const w = fieldToWorld(fx2, fy);
|
| 4987 |
+
const b = map.biomeAt(Math.round(w.x / TILE6), Math.round(w.y / TILE6)) || "forgottenPlains";
|
| 4988 |
+
const r = rosters[b];
|
| 4989 |
+
return r && r.length ? r : rosters.forgottenPlains || [];
|
| 4990 |
+
};
|
| 4991 |
+
const addFoe = async (entry, pos) => {
|
| 4992 |
+
const id = "R" + foeN++;
|
| 4993 |
+
const a = spawnActor(battle, { ...entry.unit || {}, name: entry.name }, "enemy", id);
|
| 4994 |
+
a.x = pos.x;
|
| 4995 |
+
a.y = pos.y;
|
| 4996 |
+
if (alive) await R.addActor(id, { name: entry.name, ...sd(entry.sheets) });
|
| 4997 |
+
};
|
| 4998 |
+
spawnFn = (dt) => {
|
| 4999 |
+
if (dead || !battle) return;
|
| 5000 |
+
const p = pa();
|
| 5001 |
+
if (!p) return;
|
| 5002 |
+
for (const a of ea()) {
|
| 5003 |
+
if (!a.alive) {
|
| 5004 |
+
if (!deadAt.has(a.id)) deadAt.set(a.id, battle.t);
|
| 5005 |
+
else if (battle.t - deadAt.get(a.id) > DEATH_LINGER) {
|
| 5006 |
+
R.removeActor(a.id);
|
| 5007 |
+
removeActor(battle, a.id);
|
| 5008 |
+
deadAt.delete(a.id);
|
| 5009 |
+
}
|
| 5010 |
+
} else if (dist3(a, p) > DESPAWN_TILES * FSTEP) {
|
| 5011 |
+
R.removeActor(a.id);
|
| 5012 |
+
removeActor(battle, a.id);
|
| 5013 |
+
}
|
| 5014 |
+
}
|
| 5015 |
+
spawnAcc.t += dt;
|
| 5016 |
+
if (spawnAcc.t < SPAWN_INTERVAL) return;
|
| 5017 |
+
spawnAcc.t = 0;
|
| 5018 |
+
const need = Math.min(TARGET_FOES, MAX_FOES) - ea().filter((a) => a.alive).length;
|
| 5019 |
+
for (let i = 0; i < need; i++) {
|
| 5020 |
+
const ang = Math.random() * Math.PI * 2;
|
| 5021 |
+
const rr = (SPAWN_TILES + (Math.random() * 2 - 1) * SPAWN_JITTER) * FSTEP;
|
| 5022 |
+
const pos = snapField(p.x + Math.cos(ang) * rr, p.y + Math.sin(ang) * rr);
|
| 5023 |
+
const roster = rosterAt(pos.x, pos.y);
|
| 5024 |
+
if (!roster.length) continue;
|
| 5025 |
+
addFoe(roster[Math.random() * roster.length | 0], pos);
|
| 5026 |
+
}
|
| 5027 |
+
};
|
| 5028 |
bindKeys();
|
| 5029 |
bindTap();
|
| 5030 |
buildControls();
|
|
|
|
| 5062 |
controlsEl?.remove();
|
| 5063 |
} catch {
|
| 5064 |
}
|
| 5065 |
+
try {
|
| 5066 |
+
markers?.destroy();
|
| 5067 |
+
} catch {
|
| 5068 |
+
}
|
| 5069 |
try {
|
| 5070 |
map.destroy();
|
| 5071 |
} catch {
|
|
|
|
| 5075 |
combatRoot = null;
|
| 5076 |
listeners.clear();
|
| 5077 |
}
|
| 5078 |
+
const ctrl = { ready, getSnapshot, resize, onChange, destroy, map, walkable: (wx, wy) => roamWalkable(wx, wy) };
|
| 5079 |
if (typeof window !== "undefined") {
|
| 5080 |
window.__comboSnap = () => ctrl.getSnapshot();
|
| 5081 |
window.__combo = ctrl;
|
web/enemiesSandbox.js
CHANGED
|
@@ -855,11 +855,11 @@ function makeActor(unit, team, id, slot) {
|
|
| 855 |
// optional: idle until a foe is within this distance (else always engage)
|
| 856 |
};
|
| 857 |
}
|
| 858 |
-
function makeTeamBattle({ seed = 1, players = [], enemies = [], sandbox = false, respawnDummies = 0, freeCast = false, world = null } = {}) {
|
| 859 |
const actors = [];
|
| 860 |
players.filter(Boolean).slice(0, 5).forEach((u, i) => actors.push(makeActor(u, "player", `P${i}`, i)));
|
| 861 |
enemies.filter(Boolean).slice(0, 5).forEach((u, i) => actors.push(makeActor(u, "enemy", `E${i}`, i)));
|
| 862 |
-
return { t: 0, rng: makeRng(seed), actors, projectiles: [], log: [], over: false, winner: null, sandbox, respawnDummies, freeCast, input: {}, world };
|
| 863 |
}
|
| 864 |
function setInput(b, id, cmd) {
|
| 865 |
if (!b.input) b.input = {};
|
|
@@ -1152,9 +1152,10 @@ function applyEffect(b, src, tgt, e, delivery = "spell", s = null) {
|
|
| 1152 |
}
|
| 1153 |
}
|
| 1154 |
function shadowStep(b, a, tgt) {
|
|
|
|
| 1155 |
const dx = a.x - tgt.x, dy = a.y - tgt.y, len = Math.hypot(dx, dy) || 1;
|
| 1156 |
-
a.x = Math.max(0, Math.min(
|
| 1157 |
-
a.y = Math.max(0, Math.min(
|
| 1158 |
}
|
| 1159 |
function fireSpellProjectiles(b, src, tgt, amt, e, n) {
|
| 1160 |
const base = dist(src, tgt) / 900;
|
|
@@ -1455,9 +1456,10 @@ function timeToHit(px, py, rvx, rvy, R) {
|
|
| 1455 |
}
|
| 1456 |
var clampField = (v, r, max) => Math.max(r, Math.min(max - r, v));
|
| 1457 |
function stepMove(b, a, vx, vy, dt) {
|
|
|
|
| 1458 |
const w = b.world && b.world.walkable;
|
| 1459 |
-
let nx = clampField(a.x + vx * dt, a.radius,
|
| 1460 |
-
let ny = clampField(a.y + vy * dt, a.radius,
|
| 1461 |
if (w) {
|
| 1462 |
if (!w(nx, a.y)) nx = a.x;
|
| 1463 |
if (!w(nx, ny)) ny = a.y;
|
|
@@ -1466,6 +1468,7 @@ function stepMove(b, a, vx, vy, dt) {
|
|
| 1466 |
a.y = ny;
|
| 1467 |
}
|
| 1468 |
function resolveOverlaps(b) {
|
|
|
|
| 1469 |
const live = b.actors.filter((a) => a.alive);
|
| 1470 |
for (let it = 0; it < DEOVERLAP_ITERS; it++) {
|
| 1471 |
for (let i = 0; i < live.length; i++) {
|
|
@@ -1481,10 +1484,10 @@ function resolveOverlaps(b) {
|
|
| 1481 |
const aShare = aFix ? 0 : oFix ? 1 : 0.5;
|
| 1482 |
const oShare = oFix ? 0 : aFix ? 1 : 0.5;
|
| 1483 |
const yPush = uy / COLLISION_Y_WEIGHT;
|
| 1484 |
-
a.x = clampField(a.x - ux * push * aShare, a.radius,
|
| 1485 |
-
a.y = clampField(a.y - yPush * push * aShare, a.radius,
|
| 1486 |
-
o.x = clampField(o.x + ux * push * oShare, o.radius,
|
| 1487 |
-
o.y = clampField(o.y + yPush * push * oShare, o.radius,
|
| 1488 |
}
|
| 1489 |
}
|
| 1490 |
}
|
|
@@ -2026,10 +2029,10 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 2026 |
const actorOf = (id) => getBattle()?.actors.find((x) => x.id === id);
|
| 2027 |
const floats = [];
|
| 2028 |
const sheetsById = {};
|
| 2029 |
-
|
| 2030 |
if (!def?.idle) {
|
| 2031 |
sheetsById[id] = null;
|
| 2032 |
-
|
| 2033 |
}
|
| 2034 |
try {
|
| 2035 |
const load = (url) => url ? recoloredTexture(Texture, url, def.idle, def.recolor) : null;
|
|
@@ -2055,6 +2058,7 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 2055 |
sheetsById[id] = null;
|
| 2056 |
}
|
| 2057 |
}
|
|
|
|
| 2058 |
const skillIcons = {};
|
| 2059 |
for (const s of CB_SKILLS) {
|
| 2060 |
try {
|
|
@@ -2090,7 +2094,7 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 2090 |
} catch {
|
| 2091 |
}
|
| 2092 |
const skillPlay = {};
|
| 2093 |
-
|
| 2094 |
const cell = sheetsById[id]?.cell;
|
| 2095 |
const map = {};
|
| 2096 |
for (const [skillId, fxCfg] of Object.entries(def.skillFx || {})) {
|
|
@@ -2115,8 +2119,9 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 2115 |
}
|
| 2116 |
skillPlay[id] = map;
|
| 2117 |
}
|
|
|
|
| 2118 |
const view = {};
|
| 2119 |
-
|
| 2120 |
const sh = sheetsById[id];
|
| 2121 |
const c = new Container();
|
| 2122 |
let sp;
|
|
@@ -2165,6 +2170,25 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 2165 |
units.addChild(c);
|
| 2166 |
view[id] = { def, sheets: sh, container: c, sprite: sp, overlay, overlayKey: null, bars, status, statusSprites: {}, outline, castOutline, mode: null, facing: null, flash: 0, dead: false };
|
| 2167 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2168 |
function setLoop(v, mode, facing) {
|
| 2169 |
if (v.mode === mode && v.facing === facing) return;
|
| 2170 |
v.mode = mode;
|
|
@@ -2540,6 +2564,8 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 2540 |
floats,
|
| 2541 |
actorOf,
|
| 2542 |
skillPlay,
|
|
|
|
|
|
|
| 2543 |
setLoop,
|
| 2544 |
playOnce,
|
| 2545 |
resume,
|
|
|
|
| 855 |
// optional: idle until a foe is within this distance (else always engage)
|
| 856 |
};
|
| 857 |
}
|
| 858 |
+
function makeTeamBattle({ seed = 1, players = [], enemies = [], sandbox = false, respawnDummies = 0, freeCast = false, world = null, field = null } = {}) {
|
| 859 |
const actors = [];
|
| 860 |
players.filter(Boolean).slice(0, 5).forEach((u, i) => actors.push(makeActor(u, "player", `P${i}`, i)));
|
| 861 |
enemies.filter(Boolean).slice(0, 5).forEach((u, i) => actors.push(makeActor(u, "enemy", `E${i}`, i)));
|
| 862 |
+
return { t: 0, rng: makeRng(seed), actors, projectiles: [], log: [], over: false, winner: null, sandbox, respawnDummies, freeCast, input: {}, world, field: field || FIELD };
|
| 863 |
}
|
| 864 |
function setInput(b, id, cmd) {
|
| 865 |
if (!b.input) b.input = {};
|
|
|
|
| 1152 |
}
|
| 1153 |
}
|
| 1154 |
function shadowStep(b, a, tgt) {
|
| 1155 |
+
const f = b.field || FIELD;
|
| 1156 |
const dx = a.x - tgt.x, dy = a.y - tgt.y, len = Math.hypot(dx, dy) || 1;
|
| 1157 |
+
a.x = Math.max(0, Math.min(f.w, tgt.x + dx / len * (BASIC_MELEE_GW * 0.8)));
|
| 1158 |
+
a.y = Math.max(0, Math.min(f.h, tgt.y + dy / len * (BASIC_MELEE_GW * 0.8)));
|
| 1159 |
}
|
| 1160 |
function fireSpellProjectiles(b, src, tgt, amt, e, n) {
|
| 1161 |
const base = dist(src, tgt) / 900;
|
|
|
|
| 1456 |
}
|
| 1457 |
var clampField = (v, r, max) => Math.max(r, Math.min(max - r, v));
|
| 1458 |
function stepMove(b, a, vx, vy, dt) {
|
| 1459 |
+
const f = b.field || FIELD;
|
| 1460 |
const w = b.world && b.world.walkable;
|
| 1461 |
+
let nx = clampField(a.x + vx * dt, a.radius, f.w);
|
| 1462 |
+
let ny = clampField(a.y + vy * dt, a.radius, f.h);
|
| 1463 |
if (w) {
|
| 1464 |
if (!w(nx, a.y)) nx = a.x;
|
| 1465 |
if (!w(nx, ny)) ny = a.y;
|
|
|
|
| 1468 |
a.y = ny;
|
| 1469 |
}
|
| 1470 |
function resolveOverlaps(b) {
|
| 1471 |
+
const f = b.field || FIELD;
|
| 1472 |
const live = b.actors.filter((a) => a.alive);
|
| 1473 |
for (let it = 0; it < DEOVERLAP_ITERS; it++) {
|
| 1474 |
for (let i = 0; i < live.length; i++) {
|
|
|
|
| 1484 |
const aShare = aFix ? 0 : oFix ? 1 : 0.5;
|
| 1485 |
const oShare = oFix ? 0 : aFix ? 1 : 0.5;
|
| 1486 |
const yPush = uy / COLLISION_Y_WEIGHT;
|
| 1487 |
+
a.x = clampField(a.x - ux * push * aShare, a.radius, f.w);
|
| 1488 |
+
a.y = clampField(a.y - yPush * push * aShare, a.radius, f.h);
|
| 1489 |
+
o.x = clampField(o.x + ux * push * oShare, o.radius, f.w);
|
| 1490 |
+
o.y = clampField(o.y + yPush * push * oShare, o.radius, f.h);
|
| 1491 |
}
|
| 1492 |
}
|
| 1493 |
}
|
|
|
|
| 2029 |
const actorOf = (id) => getBattle()?.actors.find((x) => x.id === id);
|
| 2030 |
const floats = [];
|
| 2031 |
const sheetsById = {};
|
| 2032 |
+
async function loadSheets(id, def) {
|
| 2033 |
if (!def?.idle) {
|
| 2034 |
sheetsById[id] = null;
|
| 2035 |
+
return;
|
| 2036 |
}
|
| 2037 |
try {
|
| 2038 |
const load = (url) => url ? recoloredTexture(Texture, url, def.idle, def.recolor) : null;
|
|
|
|
| 2058 |
sheetsById[id] = null;
|
| 2059 |
}
|
| 2060 |
}
|
| 2061 |
+
for (const [id, def] of Object.entries(defsById)) await loadSheets(id, def);
|
| 2062 |
const skillIcons = {};
|
| 2063 |
for (const s of CB_SKILLS) {
|
| 2064 |
try {
|
|
|
|
| 2094 |
} catch {
|
| 2095 |
}
|
| 2096 |
const skillPlay = {};
|
| 2097 |
+
async function buildSkillPlay(id, def) {
|
| 2098 |
const cell = sheetsById[id]?.cell;
|
| 2099 |
const map = {};
|
| 2100 |
for (const [skillId, fxCfg] of Object.entries(def.skillFx || {})) {
|
|
|
|
| 2119 |
}
|
| 2120 |
skillPlay[id] = map;
|
| 2121 |
}
|
| 2122 |
+
for (const [id, def] of Object.entries(defsById)) await buildSkillPlay(id, def);
|
| 2123 |
const view = {};
|
| 2124 |
+
function buildView(id, def) {
|
| 2125 |
const sh = sheetsById[id];
|
| 2126 |
const c = new Container();
|
| 2127 |
let sp;
|
|
|
|
| 2170 |
units.addChild(c);
|
| 2171 |
view[id] = { def, sheets: sh, container: c, sprite: sp, overlay, overlayKey: null, bars, status, statusSprites: {}, outline, castOutline, mode: null, facing: null, flash: 0, dead: false };
|
| 2172 |
}
|
| 2173 |
+
for (const [id, def] of Object.entries(defsById)) buildView(id, def);
|
| 2174 |
+
async function addActor(id, def) {
|
| 2175 |
+
if (view[id]) return view[id];
|
| 2176 |
+
defsById[id] = def;
|
| 2177 |
+
await loadSheets(id, def);
|
| 2178 |
+
await buildSkillPlay(id, def);
|
| 2179 |
+
buildView(id, def);
|
| 2180 |
+
return view[id];
|
| 2181 |
+
}
|
| 2182 |
+
function removeActor(id) {
|
| 2183 |
+
const v = view[id];
|
| 2184 |
+
if (!v) return;
|
| 2185 |
+
units.removeChild(v.container);
|
| 2186 |
+
v.container.destroy({ children: true });
|
| 2187 |
+
delete view[id];
|
| 2188 |
+
delete sheetsById[id];
|
| 2189 |
+
delete skillPlay[id];
|
| 2190 |
+
delete defsById[id];
|
| 2191 |
+
}
|
| 2192 |
function setLoop(v, mode, facing) {
|
| 2193 |
if (v.mode === mode && v.facing === facing) return;
|
| 2194 |
v.mode = mode;
|
|
|
|
| 2564 |
floats,
|
| 2565 |
actorOf,
|
| 2566 |
skillPlay,
|
| 2567 |
+
addActor,
|
| 2568 |
+
removeActor,
|
| 2569 |
setLoop,
|
| 2570 |
playOnce,
|
| 2571 |
resume,
|
web/mapSandbox.js
CHANGED
|
@@ -16,6 +16,16 @@ function createChunkedMap(pixi, host, config) {
|
|
| 16 |
const MACRO_LEVEL_MAX = config.macroLevelMax ?? 6;
|
| 17 |
const DETAIL_BUDGET = config.detailBudget ?? 1;
|
| 18 |
const MACRO_BUDGET = config.macroBudget ?? 8;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
let app = null, root = null, genRoot = null, macroRoot = null, shadowLayer = null, propLayer = null;
|
| 20 |
let ctx = null;
|
| 21 |
let alive = true;
|
|
@@ -23,7 +33,7 @@ function createChunkedMap(pixi, host, config) {
|
|
| 23 |
let seed = config.seed ?? 1;
|
| 24 |
let zoom = Z_DEFAULT;
|
| 25 |
let cameraDirty = true, genPending = false, lastEmitTile = null;
|
| 26 |
-
const camera = { x: CHUNKPX / 2, y: CHUNKPX / 2 };
|
| 27 |
const chunks = /* @__PURE__ */ new Map();
|
| 28 |
const macroChunks = /* @__PURE__ */ new Map();
|
| 29 |
const fading = /* @__PURE__ */ new Set();
|
|
@@ -104,8 +114,21 @@ function createChunkedMap(pixi, host, config) {
|
|
| 104 |
sprite.width = sprite.height = MCHUNK * step * TILE6;
|
| 105 |
return { sprite, tex: t };
|
| 106 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
function reconcile() {
|
| 108 |
if (!genRoot || !ctx || !app) return;
|
|
|
|
|
|
|
| 109 |
const sx = Math.round(app.screen.width / 2 - camera.x * zoom);
|
| 110 |
const sy = Math.round(app.screen.height / 2 - camera.y * zoom);
|
| 111 |
for (const L of [macroRoot, genRoot, shadowLayer, propLayer]) {
|
|
@@ -142,8 +165,14 @@ function createChunkedMap(pixi, host, config) {
|
|
| 142 |
}
|
| 143 |
function reconcileDetail() {
|
| 144 |
const halfW = app.screen.width / 2 / zoom, halfH = app.screen.height / 2 / zoom;
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
for (const [key, ch] of chunks) {
|
| 148 |
const [cx, cy] = key.split(",").map(Number);
|
| 149 |
if (cx < c0 - 1 || cx > c1 + 1 || cy < r0 - 1 || cy > r1 + 1) evictChunk(key, ch);
|
|
@@ -171,8 +200,15 @@ function createChunkedMap(pixi, host, config) {
|
|
| 171 |
function reconcileMacro(L) {
|
| 172 |
const px = MCHUNK * (1 << L) * TILE6;
|
| 173 |
const halfW = app.screen.width / 2 / zoom, halfH = app.screen.height / 2 / zoom;
|
| 174 |
-
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
for (const [key, mc] of macroChunks) {
|
| 177 |
const [ml, mx, my] = key.split(",").map(Number);
|
| 178 |
if (ml !== L || mx < c0 - 1 || mx > c1 + 1 || my < r0 - 1 || my > r1 + 1) {
|
|
@@ -209,7 +245,7 @@ function createChunkedMap(pixi, host, config) {
|
|
| 209 |
}
|
| 210 |
function zoomAt(factor, lx, ly) {
|
| 211 |
if (!app) return;
|
| 212 |
-
const nz = Math.min(Z_MAX, Math.max(Z_MIN, zoom * factor));
|
| 213 |
if (nz === zoom) return;
|
| 214 |
const sw = app.screen.width, sh = app.screen.height;
|
| 215 |
const wx = camera.x + (lx - sw / 2) / zoom, wy = camera.y + (ly - sh / 2) / zoom;
|
|
@@ -400,6 +436,12 @@ function createChunkedMap(pixi, host, config) {
|
|
| 400 |
if (!ch) return null;
|
| 401 |
return config.tileIndexAt(wx, wy, ch.meta);
|
| 402 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
function setEnabled(v) {
|
| 404 |
enabled = v;
|
| 405 |
if (v) cameraDirty = true;
|
|
@@ -448,7 +490,7 @@ function createChunkedMap(pixi, host, config) {
|
|
| 448 |
ctx = null;
|
| 449 |
texCache.clear();
|
| 450 |
}
|
| 451 |
-
return { ready, regenerate, destroy, onChange, getSnapshot, getCamera, tileIndexAt, zoomBy, setEnabled, onTick, getEntityLayer, getApp, screenToWorld, worldToScreen, tile: TILE6 };
|
| 452 |
}
|
| 453 |
|
| 454 |
// ../auto-battler/src/engine/rng.js
|
|
@@ -2107,7 +2149,8 @@ var RANK = {};
|
|
| 2107 |
OVERWORLD_BIOMES.forEach((id, i) => {
|
| 2108 |
RANK[id] = i;
|
| 2109 |
});
|
| 2110 |
-
var overworldConfig = (seed) => {
|
|
|
|
| 2111 |
const biomes = {};
|
| 2112 |
for (const id of OVERWORLD_BIOMES) {
|
| 2113 |
const factory = BIOME_FACTORIES[id];
|
|
@@ -2119,6 +2162,8 @@ var overworldConfig = (seed) => {
|
|
| 2119 |
tile: TILE5,
|
| 2120 |
chunk: CHUNK4,
|
| 2121 |
background: "#2b2f3a",
|
|
|
|
|
|
|
| 2122 |
// Each biome loads its own sheets into its own ctx; then build the cross-biome transition atlas
|
| 2123 |
// from their base grounds. Key everything for bake to route to.
|
| 2124 |
async load(api) {
|
|
@@ -2138,7 +2183,7 @@ var overworldConfig = (seed) => {
|
|
| 2138 |
const winner = new Array(GW * GW);
|
| 2139 |
const widx = (lx, ly) => (ly + 1) * GW + (lx + 1);
|
| 2140 |
for (let ly = -1; ly <= CHUNK4; ly++) for (let lx = -1; lx <= CHUNK4; lx++) {
|
| 2141 |
-
winner[widx(lx, ly)] =
|
| 2142 |
}
|
| 2143 |
const at = (lx, ly) => winner[widx(lx, ly)];
|
| 2144 |
const present = /* @__PURE__ */ new Set();
|
|
@@ -2188,11 +2233,16 @@ var overworldConfig = (seed) => {
|
|
| 2188 |
// little of the biome's internal terrain colour, cross-fading into the runner-up region across
|
| 2189 |
// a thin border band so regions read as soft-edged lands, not hard cells.
|
| 2190 |
macroColor(seed2, tx, ty) {
|
| 2191 |
-
const { id, id2, edge } =
|
| 2192 |
let tint = REGION_TINT[id];
|
| 2193 |
if (edge < MACRO_BLEND && id !== id2) tint = lerp3(tint, REGION_TINT[id2], 0.5 * (1 - edge / MACRO_BLEND));
|
| 2194 |
return lerp3(tint, biomes[id].macroColor(seed2, tx, ty), TINT_MIX);
|
| 2195 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2196 |
// Debug grid overlay: dispatch to whichever biome owns the tile, with that biome's chunk meta.
|
| 2197 |
tileIndexAt(wx, wy, meta) {
|
| 2198 |
if (!meta) return null;
|
|
|
|
| 16 |
const MACRO_LEVEL_MAX = config.macroLevelMax ?? 6;
|
| 17 |
const DETAIL_BUDGET = config.detailBudget ?? 1;
|
| 18 |
const MACRO_BUDGET = config.macroBudget ?? 8;
|
| 19 |
+
const bounds = config.bounds ? {
|
| 20 |
+
x0: config.bounds.x0 * TILE6,
|
| 21 |
+
y0: config.bounds.y0 * TILE6,
|
| 22 |
+
x1: config.bounds.x1 * TILE6,
|
| 23 |
+
y1: config.bounds.y1 * TILE6,
|
| 24 |
+
tx0: config.bounds.x0,
|
| 25 |
+
ty0: config.bounds.y0,
|
| 26 |
+
tx1: config.bounds.x1,
|
| 27 |
+
ty1: config.bounds.y1
|
| 28 |
+
} : null;
|
| 29 |
let app = null, root = null, genRoot = null, macroRoot = null, shadowLayer = null, propLayer = null;
|
| 30 |
let ctx = null;
|
| 31 |
let alive = true;
|
|
|
|
| 33 |
let seed = config.seed ?? 1;
|
| 34 |
let zoom = Z_DEFAULT;
|
| 35 |
let cameraDirty = true, genPending = false, lastEmitTile = null;
|
| 36 |
+
const camera = { x: config.initialCamera?.x ?? CHUNKPX / 2, y: config.initialCamera?.y ?? CHUNKPX / 2 };
|
| 37 |
const chunks = /* @__PURE__ */ new Map();
|
| 38 |
const macroChunks = /* @__PURE__ */ new Map();
|
| 39 |
const fading = /* @__PURE__ */ new Set();
|
|
|
|
| 114 |
sprite.width = sprite.height = MCHUNK * step * TILE6;
|
| 115 |
return { sprite, tex: t };
|
| 116 |
}
|
| 117 |
+
function coverZoom() {
|
| 118 |
+
if (!bounds || !app) return Z_MIN;
|
| 119 |
+
return Math.max(app.screen.width / (bounds.x1 - bounds.x0), app.screen.height / (bounds.y1 - bounds.y0));
|
| 120 |
+
}
|
| 121 |
+
function clampCamera() {
|
| 122 |
+
if (!bounds || !app) return;
|
| 123 |
+
const hw = app.screen.width / 2 / zoom, hh = app.screen.height / 2 / zoom;
|
| 124 |
+
const loX = bounds.x0 + hw, hiX = bounds.x1 - hw, loY = bounds.y0 + hh, hiY = bounds.y1 - hh;
|
| 125 |
+
camera.x = loX <= hiX ? Math.min(hiX, Math.max(loX, camera.x)) : (bounds.x0 + bounds.x1) / 2;
|
| 126 |
+
camera.y = loY <= hiY ? Math.min(hiY, Math.max(loY, camera.y)) : (bounds.y0 + bounds.y1) / 2;
|
| 127 |
+
}
|
| 128 |
function reconcile() {
|
| 129 |
if (!genRoot || !ctx || !app) return;
|
| 130 |
+
if (bounds) zoom = Math.max(zoom, coverZoom());
|
| 131 |
+
clampCamera();
|
| 132 |
const sx = Math.round(app.screen.width / 2 - camera.x * zoom);
|
| 133 |
const sy = Math.round(app.screen.height / 2 - camera.y * zoom);
|
| 134 |
for (const L of [macroRoot, genRoot, shadowLayer, propLayer]) {
|
|
|
|
| 165 |
}
|
| 166 |
function reconcileDetail() {
|
| 167 |
const halfW = app.screen.width / 2 / zoom, halfH = app.screen.height / 2 / zoom;
|
| 168 |
+
let c0 = Math.floor((camera.x - halfW) / CHUNKPX) - 1, c1 = Math.floor((camera.x + halfW) / CHUNKPX) + 1;
|
| 169 |
+
let r0 = Math.floor((camera.y - halfH) / CHUNKPX) - 1, r1 = Math.floor((camera.y + halfH) / CHUNKPX) + 1;
|
| 170 |
+
if (bounds) {
|
| 171 |
+
c0 = Math.max(c0, Math.floor(bounds.tx0 / CHUNK5));
|
| 172 |
+
c1 = Math.min(c1, Math.floor((bounds.tx1 - 1) / CHUNK5));
|
| 173 |
+
r0 = Math.max(r0, Math.floor(bounds.ty0 / CHUNK5));
|
| 174 |
+
r1 = Math.min(r1, Math.floor((bounds.ty1 - 1) / CHUNK5));
|
| 175 |
+
}
|
| 176 |
for (const [key, ch] of chunks) {
|
| 177 |
const [cx, cy] = key.split(",").map(Number);
|
| 178 |
if (cx < c0 - 1 || cx > c1 + 1 || cy < r0 - 1 || cy > r1 + 1) evictChunk(key, ch);
|
|
|
|
| 200 |
function reconcileMacro(L) {
|
| 201 |
const px = MCHUNK * (1 << L) * TILE6;
|
| 202 |
const halfW = app.screen.width / 2 / zoom, halfH = app.screen.height / 2 / zoom;
|
| 203 |
+
let c0 = Math.floor((camera.x - halfW) / px) - 1, c1 = Math.floor((camera.x + halfW) / px) + 1;
|
| 204 |
+
let r0 = Math.floor((camera.y - halfH) / px) - 1, r1 = Math.floor((camera.y + halfH) / px) + 1;
|
| 205 |
+
if (bounds) {
|
| 206 |
+
const mt = MCHUNK * (1 << L);
|
| 207 |
+
c0 = Math.max(c0, Math.floor(bounds.tx0 / mt));
|
| 208 |
+
c1 = Math.min(c1, Math.floor((bounds.tx1 - 1) / mt));
|
| 209 |
+
r0 = Math.max(r0, Math.floor(bounds.ty0 / mt));
|
| 210 |
+
r1 = Math.min(r1, Math.floor((bounds.ty1 - 1) / mt));
|
| 211 |
+
}
|
| 212 |
for (const [key, mc] of macroChunks) {
|
| 213 |
const [ml, mx, my] = key.split(",").map(Number);
|
| 214 |
if (ml !== L || mx < c0 - 1 || mx > c1 + 1 || my < r0 - 1 || my > r1 + 1) {
|
|
|
|
| 245 |
}
|
| 246 |
function zoomAt(factor, lx, ly) {
|
| 247 |
if (!app) return;
|
| 248 |
+
const nz = Math.min(Z_MAX, Math.max(bounds ? coverZoom() : Z_MIN, zoom * factor));
|
| 249 |
if (nz === zoom) return;
|
| 250 |
const sw = app.screen.width, sh = app.screen.height;
|
| 251 |
const wx = camera.x + (lx - sw / 2) / zoom, wy = camera.y + (ly - sh / 2) / zoom;
|
|
|
|
| 436 |
if (!ch) return null;
|
| 437 |
return config.tileIndexAt(wx, wy, ch.meta);
|
| 438 |
}
|
| 439 |
+
function biomeAt(wx, wy) {
|
| 440 |
+
return config.biomeAt ? config.biomeAt(seed, wx, wy) : null;
|
| 441 |
+
}
|
| 442 |
+
function getBounds() {
|
| 443 |
+
return bounds ? { x0: bounds.x0, y0: bounds.y0, x1: bounds.x1, y1: bounds.y1 } : null;
|
| 444 |
+
}
|
| 445 |
function setEnabled(v) {
|
| 446 |
enabled = v;
|
| 447 |
if (v) cameraDirty = true;
|
|
|
|
| 490 |
ctx = null;
|
| 491 |
texCache.clear();
|
| 492 |
}
|
| 493 |
+
return { ready, regenerate, destroy, onChange, getSnapshot, getCamera, tileIndexAt, biomeAt, getBounds, zoomBy, setEnabled, onTick, getEntityLayer, getApp, screenToWorld, worldToScreen, tile: TILE6 };
|
| 494 |
}
|
| 495 |
|
| 496 |
// ../auto-battler/src/engine/rng.js
|
|
|
|
| 2149 |
OVERWORLD_BIOMES.forEach((id, i) => {
|
| 2150 |
RANK[id] = i;
|
| 2151 |
});
|
| 2152 |
+
var overworldConfig = (seed, opts = {}) => {
|
| 2153 |
+
const region = opts.regionFn ?? biomeRegion;
|
| 2154 |
const biomes = {};
|
| 2155 |
for (const id of OVERWORLD_BIOMES) {
|
| 2156 |
const factory = BIOME_FACTORIES[id];
|
|
|
|
| 2162 |
tile: TILE5,
|
| 2163 |
chunk: CHUNK4,
|
| 2164 |
background: "#2b2f3a",
|
| 2165 |
+
bounds: opts.bounds,
|
| 2166 |
+
initialCamera: opts.initialCamera,
|
| 2167 |
// Each biome loads its own sheets into its own ctx; then build the cross-biome transition atlas
|
| 2168 |
// from their base grounds. Key everything for bake to route to.
|
| 2169 |
async load(api) {
|
|
|
|
| 2183 |
const winner = new Array(GW * GW);
|
| 2184 |
const widx = (lx, ly) => (ly + 1) * GW + (lx + 1);
|
| 2185 |
for (let ly = -1; ly <= CHUNK4; ly++) for (let lx = -1; lx <= CHUNK4; lx++) {
|
| 2186 |
+
winner[widx(lx, ly)] = region(seed2, x0 + lx, y0 + ly).id;
|
| 2187 |
}
|
| 2188 |
const at = (lx, ly) => winner[widx(lx, ly)];
|
| 2189 |
const present = /* @__PURE__ */ new Set();
|
|
|
|
| 2233 |
// little of the biome's internal terrain colour, cross-fading into the runner-up region across
|
| 2234 |
// a thin border band so regions read as soft-edged lands, not hard cells.
|
| 2235 |
macroColor(seed2, tx, ty) {
|
| 2236 |
+
const { id, id2, edge } = region(seed2, tx, ty);
|
| 2237 |
let tint = REGION_TINT[id];
|
| 2238 |
if (edge < MACRO_BLEND && id !== id2) tint = lerp3(tint, REGION_TINT[id2], 0.5 * (1 - edge / MACRO_BLEND));
|
| 2239 |
return lerp3(tint, biomes[id].macroColor(seed2, tx, ty), TINT_MIX);
|
| 2240 |
},
|
| 2241 |
+
// Which biome owns a world tile (no chunk meta needed) — for callers that key behaviour to the
|
| 2242 |
+
// land the player is on (free-roam enemy rosters, per-biome walkability).
|
| 2243 |
+
biomeAt(seed2, wx, wy) {
|
| 2244 |
+
return region(seed2, wx, wy).id;
|
| 2245 |
+
},
|
| 2246 |
// Debug grid overlay: dispatch to whichever biome owns the tile, with that biome's chunk meta.
|
| 2247 |
tileIndexAt(wx, wy, meta) {
|
| 2248 |
if (!meta) return null;
|
web/tiny.js
CHANGED
|
@@ -135,6 +135,30 @@ whenEl('skillforge-stage', (el) => { mountSkillForgePanel(el) })
|
|
| 135 |
// link / sidebar ⚙), shared across pages via the runtime.js + tts.js singletons.
|
| 136 |
mountSettingsPanel()
|
| 137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
// Sidebar "⚙ Settings" item opens the SAME Gradio settings page as the footer link.
|
| 139 |
// Wrap sidebar.js's tacNavigate (already set, since that's a non-module script): the
|
| 140 |
// "Settings" nav target clicks Gradio's footer Settings button; everything else routes
|
|
@@ -156,15 +180,33 @@ window.tacNavigate = function (target) {
|
|
| 156 |
// on proximity in real-time on the map. The old hardcoded 4v4 demo lived here; this is the shared
|
| 157 |
// comboBattler surface (auto-battler source, bundled to /web/comboBattler.js).
|
| 158 |
const sheetsOf = (c) => ({ idle: spriteUrl(c.idle), walk: spriteUrl(c.walk), attack: spriteUrl(c.attack), dmg: spriteUrl(c.dmg), die: spriteUrl(c.die) })
|
| 159 |
-
//
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
// Persona class → engine profession (the engine has templates + skills for these five).
|
| 169 |
const PERSONA_PROF = { Warrior: 'Warrior', Ranger: 'Ranger', Monk: 'Monk', Assassin: 'Assassin', Mage: 'Necromancer', Paladin: 'Monk', Cleric: 'Monk', Knight: 'Warrior' }
|
| 170 |
const ENEMY_AGGRO = 220 // FIELD units (~8 tiles): enemy idles until the player is this near
|
|
@@ -179,10 +221,11 @@ whenEl('battle-stage', async (el) => {
|
|
| 179 |
sheets: sheetsOf(pc),
|
| 180 |
unit: { profession: PERSONA_PROF[p?.unitClass] || 'Warrior', name: p?.name || 'Hero' }, // skills filled from profession
|
| 181 |
}
|
| 182 |
-
const
|
| 183 |
const c = chars[e.slug]; if (!c) return null
|
| 184 |
return { name: e.name, sheets: sheetsOf(c), unit: { name: e.name, stats: e.stats, attackType: e.attackType, skills: [], aggroRadius: ENEMY_AGGRO } }
|
| 185 |
}).filter(Boolean)
|
| 186 |
-
|
|
|
|
| 187 |
await comboCtrl.ready
|
| 188 |
})
|
|
|
|
| 135 |
// link / sidebar ⚙), shared across pages via the runtime.js + tts.js singletons.
|
| 136 |
mountSettingsPanel()
|
| 137 |
|
| 138 |
+
// Relocate Gradio's footer links — Use via API, Built with Gradio, Settings — into the sidebar's App
|
| 139 |
+
// slot (#tac-extlinks), then hide the footer. We move the LIVE elements, so each keeps its native
|
| 140 |
+
// behaviour (API panel / open gradio.app / open Settings). They are styled via .tac-extlink only —
|
| 141 |
+
// NOT .tac-nav-item, which would make sidebar.js hijack the click (mark active=white, swallow nav).
|
| 142 |
+
function relocateFooterLinks() {
|
| 143 |
+
const wanted = (t) => /^(use via api|built with gradio|settings)$/i.test((t || '').trim())
|
| 144 |
+
const move = () => {
|
| 145 |
+
const footer = document.querySelector('footer')
|
| 146 |
+
const host = document.getElementById('tac-extlinks')
|
| 147 |
+
if (!footer || !host) return false
|
| 148 |
+
const links = Array.prototype.filter.call(footer.querySelectorAll('a, button'), (e) => wanted(e.textContent))
|
| 149 |
+
if (links.length < 3) return false // wait until all three exist
|
| 150 |
+
const ref = document.querySelector('.tac-sidebar .tac-nav-item') // a real nav item, to copy its padding
|
| 151 |
+
const pad = ref ? getComputedStyle(ref).padding : ''
|
| 152 |
+
for (const e of links) { e.classList.add('tac-extlink'); if (pad) e.style.padding = pad; host.appendChild(e) }
|
| 153 |
+
footer.style.display = 'none'
|
| 154 |
+
return true
|
| 155 |
+
}
|
| 156 |
+
if (move()) return
|
| 157 |
+
const o = new MutationObserver(() => { if (move()) o.disconnect() })
|
| 158 |
+
o.observe(document.body, { childList: true, subtree: true })
|
| 159 |
+
}
|
| 160 |
+
relocateFooterLinks()
|
| 161 |
+
|
| 162 |
// Sidebar "⚙ Settings" item opens the SAME Gradio settings page as the footer link.
|
| 163 |
// Wrap sidebar.js's tacNavigate (already set, since that's a non-module script): the
|
| 164 |
// "Settings" nav target clicks Gradio's footer Settings button; everything else routes
|
|
|
|
| 180 |
// on proximity in real-time on the map. The old hardcoded 4v4 demo lived here; this is the shared
|
| 181 |
// comboBattler surface (auto-battler source, bundled to /web/comboBattler.js).
|
| 182 |
const sheetsOf = (c) => ({ idle: spriteUrl(c.idle), walk: spriteUrl(c.walk), attack: spriteUrl(c.attack), dmg: spriteUrl(c.dmg), die: spriteUrl(c.die) })
|
| 183 |
+
// Free-roam enemy rosters, one per biome band — the spawner draws from the roster of whichever land
|
| 184 |
+
// the spawn point lands on. Forgotten Plains = human patrols, Orc Kingdom = orcs, Necropolis = the
|
| 185 |
+
// Dark Brotherhood. (slug + combat stats, resolved against characters.json.)
|
| 186 |
+
const GAME_ROSTERS = {
|
| 187 |
+
forgottenPlains: [
|
| 188 |
+
{ name: 'Swordsman', slug: 'rts-humans-swordsman', stats: { hp: 110, armor: 20, basicDamage: 12 }, attackType: 'melee' },
|
| 189 |
+
{ name: 'Spearman', slug: 'rts-humans-spearman', stats: { hp: 100, armor: 18, basicDamage: 11 }, attackType: 'melee' },
|
| 190 |
+
{ name: 'Knight', slug: 'rts-humans-knight', stats: { hp: 150, armor: 35, basicDamage: 14 }, attackType: 'melee' },
|
| 191 |
+
{ name: 'Archer', slug: 'rts-humans-archer', stats: { hp: 80, armor: 10, basicDamage: 12 }, attackType: 'ranged' },
|
| 192 |
+
],
|
| 193 |
+
orc: [
|
| 194 |
+
{ name: 'Orc Blade', slug: 'dark-orc-army-orc-blade', stats: { hp: 130, armor: 25, basicDamage: 13 }, attackType: 'melee' },
|
| 195 |
+
{ name: 'Orc Raider', slug: 'dark-orc-army-orc-raider', stats: { hp: 120, armor: 20, basicDamage: 12 }, attackType: 'melee' },
|
| 196 |
+
{ name: 'Orc Scout', slug: 'dark-orc-army-orc-scout', stats: { hp: 90, armor: 12, basicDamage: 10 }, attackType: 'melee' },
|
| 197 |
+
{ name: 'Feral Berserker', slug: 'dark-orc-army-feral-berserker', stats: { hp: 160, armor: 30, basicDamage: 16 }, attackType: 'melee' },
|
| 198 |
+
{ name: 'Feral Arbalist', slug: 'dark-orc-army-feral-arbalist', stats: { hp: 95, armor: 14, basicDamage: 13 }, attackType: 'ranged' },
|
| 199 |
+
{ name: 'Cave Troll', slug: 'dark-orc-army-cave-troll', stats: { hp: 220, armor: 40, basicDamage: 20 }, attackType: 'melee' },
|
| 200 |
+
],
|
| 201 |
+
necropolis: [
|
| 202 |
+
{ name: 'Acolyte', slug: 'dark-brotherhood-acolyte', stats: { hp: 100, armor: 15, basicDamage: 10 }, attackType: 'melee' },
|
| 203 |
+
{ name: 'Dark Cultist', slug: 'dark-brotherhood-dark-cultist', stats: { hp: 95, armor: 12, basicDamage: 12 }, attackType: 'ranged' },
|
| 204 |
+
{ name: 'Devoted Blade', slug: 'dark-brotherhood-devoted-blade', stats: { hp: 120, armor: 20, basicDamage: 12 }, attackType: 'melee' },
|
| 205 |
+
{ name: 'Dark Hound', slug: 'dark-brotherhood-dark-hound', stats: { hp: 85, armor: 10, basicDamage: 13 }, attackType: 'melee' },
|
| 206 |
+
{ name: 'Zealot', slug: 'dark-brotherhood-zealot', stats: { hp: 110, armor: 18, basicDamage: 13 }, attackType: 'melee' },
|
| 207 |
+
{ name: 'Dark Abomination', slug: 'dark-brotherhood-dark-abomination', stats: { hp: 200, armor: 35, basicDamage: 18 }, attackType: 'melee' },
|
| 208 |
+
],
|
| 209 |
+
}
|
| 210 |
// Persona class → engine profession (the engine has templates + skills for these five).
|
| 211 |
const PERSONA_PROF = { Warrior: 'Warrior', Ranger: 'Ranger', Monk: 'Monk', Assassin: 'Assassin', Mage: 'Necromancer', Paladin: 'Monk', Cleric: 'Monk', Knight: 'Warrior' }
|
| 212 |
const ENEMY_AGGRO = 220 // FIELD units (~8 tiles): enemy idles until the player is this near
|
|
|
|
| 221 |
sheets: sheetsOf(pc),
|
| 222 |
unit: { profession: PERSONA_PROF[p?.unitClass] || 'Warrior', name: p?.name || 'Hero' }, // skills filled from profession
|
| 223 |
}
|
| 224 |
+
const buildRoster = (list) => list.map((e) => {
|
| 225 |
const c = chars[e.slug]; if (!c) return null
|
| 226 |
return { name: e.name, sheets: sheetsOf(c), unit: { name: e.name, stats: e.stats, attackType: e.attackType, skills: [], aggroRadius: ENEMY_AGGRO } }
|
| 227 |
}).filter(Boolean)
|
| 228 |
+
const rosters = Object.fromEntries(Object.entries(GAME_ROSTERS).map(([k, v]) => [k, buildRoster(v)]))
|
| 229 |
+
comboCtrl = mountComboBattler(PIXI, el, { seed: 1, player, rosters })
|
| 230 |
await comboCtrl.ready
|
| 231 |
})
|