Spaces:
Running
Running
Game: weaker early foes, no-kite ranged, C=character sheet, hide sidebar toggle
Browse files- Early enemies weaker (HP*0.2, armor*0.25): most die in 2 Warrior basics, then ramp with
threat via hpMul. Verified Swordsman/Spearman/Knight=2 hits, Archer/Scout=1.
- Roam foes set noKite: ranged enemies hold ground + shoot instead of backing away.
- Character sheet: press C to open/close + a 'C' indicator on the menu button.
- Hide the app-sidebar toggle (it sat over the XP bar); show/hide the nav drawer with the
backtick/tilde key instead. Both guarded against text inputs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- web/classesSandbox.js +5 -2
- web/comboBattler.js +5 -2
- web/enemiesSandbox.js +5 -2
- web/engine.js +5 -2
- web/tiny.js +25 -3
web/classesSandbox.js
CHANGED
|
@@ -1025,8 +1025,10 @@ function makeActor(unit, team, id, slot) {
|
|
| 1025 |
alive: true,
|
| 1026 |
mods: [],
|
| 1027 |
kd: 0,
|
| 1028 |
-
aggroRadius: unit.aggroRadius ?? null
|
| 1029 |
// optional: idle until a foe is within this distance (else always engage)
|
|
|
|
|
|
|
| 1030 |
};
|
| 1031 |
}
|
| 1032 |
function makeTeamBattle({ seed = 1, players = [], enemies = [], sandbox = false, respawnDummies = 0, freeCast = false, world = null, field = null } = {}) {
|
|
@@ -1567,7 +1569,8 @@ function moveActor(b, a, enemy, dt) {
|
|
| 1567 |
const d = dist(a, enemy);
|
| 1568 |
let toward = 0;
|
| 1569 |
if (a.role === "ranged") {
|
| 1570 |
-
if (
|
|
|
|
| 1571 |
else if (d > a.weapon.range) toward = 1;
|
| 1572 |
} else if (edgeGap(a, enemy) > reachOf(a)) {
|
| 1573 |
toward = 1;
|
|
|
|
| 1025 |
alive: true,
|
| 1026 |
mods: [],
|
| 1027 |
kd: 0,
|
| 1028 |
+
aggroRadius: unit.aggroRadius ?? null,
|
| 1029 |
// optional: idle until a foe is within this distance (else always engage)
|
| 1030 |
+
noKite: !!unit.noKite
|
| 1031 |
+
// ranged: hold ground in range instead of backing away (kiting)
|
| 1032 |
};
|
| 1033 |
}
|
| 1034 |
function makeTeamBattle({ seed = 1, players = [], enemies = [], sandbox = false, respawnDummies = 0, freeCast = false, world = null, field = null } = {}) {
|
|
|
|
| 1569 |
const d = dist(a, enemy);
|
| 1570 |
let toward = 0;
|
| 1571 |
if (a.role === "ranged") {
|
| 1572 |
+
if (a.noKite) toward = d > a.weapon.range ? 1 : 0;
|
| 1573 |
+
else if (d < (a.preferredRange ?? a.weapon.range * 0.7)) toward = -1;
|
| 1574 |
else if (d > a.weapon.range) toward = 1;
|
| 1575 |
} else if (edgeGap(a, enemy) > reachOf(a)) {
|
| 1576 |
toward = 1;
|
web/comboBattler.js
CHANGED
|
@@ -5109,8 +5109,10 @@ function makeActor(unit, team, id, slot) {
|
|
| 5109 |
alive: true,
|
| 5110 |
mods: [],
|
| 5111 |
kd: 0,
|
| 5112 |
-
aggroRadius: unit.aggroRadius ?? null
|
| 5113 |
// optional: idle until a foe is within this distance (else always engage)
|
|
|
|
|
|
|
| 5114 |
};
|
| 5115 |
}
|
| 5116 |
function makeTeamBattle({ seed = 1, players = [], enemies = [], sandbox = false, respawnDummies = 0, freeCast = false, world = null, field = null } = {}) {
|
|
@@ -5661,7 +5663,8 @@ function moveActor(b, a, enemy, dt) {
|
|
| 5661 |
const d = dist2(a, enemy);
|
| 5662 |
let toward = 0;
|
| 5663 |
if (a.role === "ranged") {
|
| 5664 |
-
if (
|
|
|
|
| 5665 |
else if (d > a.weapon.range) toward = 1;
|
| 5666 |
} else if (edgeGap(a, enemy) > reachOf(a)) {
|
| 5667 |
toward = 1;
|
|
|
|
| 5109 |
alive: true,
|
| 5110 |
mods: [],
|
| 5111 |
kd: 0,
|
| 5112 |
+
aggroRadius: unit.aggroRadius ?? null,
|
| 5113 |
// optional: idle until a foe is within this distance (else always engage)
|
| 5114 |
+
noKite: !!unit.noKite
|
| 5115 |
+
// ranged: hold ground in range instead of backing away (kiting)
|
| 5116 |
};
|
| 5117 |
}
|
| 5118 |
function makeTeamBattle({ seed = 1, players = [], enemies = [], sandbox = false, respawnDummies = 0, freeCast = false, world = null, field = null } = {}) {
|
|
|
|
| 5663 |
const d = dist2(a, enemy);
|
| 5664 |
let toward = 0;
|
| 5665 |
if (a.role === "ranged") {
|
| 5666 |
+
if (a.noKite) toward = d > a.weapon.range ? 1 : 0;
|
| 5667 |
+
else if (d < (a.preferredRange ?? a.weapon.range * 0.7)) toward = -1;
|
| 5668 |
else if (d > a.weapon.range) toward = 1;
|
| 5669 |
} else if (edgeGap(a, enemy) > reachOf(a)) {
|
| 5670 |
toward = 1;
|
web/enemiesSandbox.js
CHANGED
|
@@ -858,8 +858,10 @@ function makeActor(unit, team, id, slot) {
|
|
| 858 |
alive: true,
|
| 859 |
mods: [],
|
| 860 |
kd: 0,
|
| 861 |
-
aggroRadius: unit.aggroRadius ?? null
|
| 862 |
// optional: idle until a foe is within this distance (else always engage)
|
|
|
|
|
|
|
| 863 |
};
|
| 864 |
}
|
| 865 |
function makeTeamBattle({ seed = 1, players = [], enemies = [], sandbox = false, respawnDummies = 0, freeCast = false, world = null, field = null } = {}) {
|
|
@@ -1400,7 +1402,8 @@ function moveActor(b, a, enemy, dt) {
|
|
| 1400 |
const d = dist(a, enemy);
|
| 1401 |
let toward = 0;
|
| 1402 |
if (a.role === "ranged") {
|
| 1403 |
-
if (
|
|
|
|
| 1404 |
else if (d > a.weapon.range) toward = 1;
|
| 1405 |
} else if (edgeGap(a, enemy) > reachOf(a)) {
|
| 1406 |
toward = 1;
|
|
|
|
| 858 |
alive: true,
|
| 859 |
mods: [],
|
| 860 |
kd: 0,
|
| 861 |
+
aggroRadius: unit.aggroRadius ?? null,
|
| 862 |
// optional: idle until a foe is within this distance (else always engage)
|
| 863 |
+
noKite: !!unit.noKite
|
| 864 |
+
// ranged: hold ground in range instead of backing away (kiting)
|
| 865 |
};
|
| 866 |
}
|
| 867 |
function makeTeamBattle({ seed = 1, players = [], enemies = [], sandbox = false, respawnDummies = 0, freeCast = false, world = null, field = null } = {}) {
|
|
|
|
| 1402 |
const d = dist(a, enemy);
|
| 1403 |
let toward = 0;
|
| 1404 |
if (a.role === "ranged") {
|
| 1405 |
+
if (a.noKite) toward = d > a.weapon.range ? 1 : 0;
|
| 1406 |
+
else if (d < (a.preferredRange ?? a.weapon.range * 0.7)) toward = -1;
|
| 1407 |
else if (d > a.weapon.range) toward = 1;
|
| 1408 |
} else if (edgeGap(a, enemy) > reachOf(a)) {
|
| 1409 |
toward = 1;
|
web/engine.js
CHANGED
|
@@ -832,8 +832,10 @@ function makeActor(unit, team, id, slot) {
|
|
| 832 |
alive: true,
|
| 833 |
mods: [],
|
| 834 |
kd: 0,
|
| 835 |
-
aggroRadius: unit.aggroRadius ?? null
|
| 836 |
// optional: idle until a foe is within this distance (else always engage)
|
|
|
|
|
|
|
| 837 |
};
|
| 838 |
}
|
| 839 |
function makeTeamBattle({ seed = 1, players = [], enemies = [], sandbox = false, respawnDummies = 0, freeCast = false, world = null, field = null } = {}) {
|
|
@@ -1384,7 +1386,8 @@ function moveActor(b, a, enemy, dt) {
|
|
| 1384 |
const d = dist(a, enemy);
|
| 1385 |
let toward = 0;
|
| 1386 |
if (a.role === "ranged") {
|
| 1387 |
-
if (
|
|
|
|
| 1388 |
else if (d > a.weapon.range) toward = 1;
|
| 1389 |
} else if (edgeGap(a, enemy) > reachOf(a)) {
|
| 1390 |
toward = 1;
|
|
|
|
| 832 |
alive: true,
|
| 833 |
mods: [],
|
| 834 |
kd: 0,
|
| 835 |
+
aggroRadius: unit.aggroRadius ?? null,
|
| 836 |
// optional: idle until a foe is within this distance (else always engage)
|
| 837 |
+
noKite: !!unit.noKite
|
| 838 |
+
// ranged: hold ground in range instead of backing away (kiting)
|
| 839 |
};
|
| 840 |
}
|
| 841 |
function makeTeamBattle({ seed = 1, players = [], enemies = [], sandbox = false, respawnDummies = 0, freeCast = false, world = null, field = null } = {}) {
|
|
|
|
| 1386 |
const d = dist(a, enemy);
|
| 1387 |
let toward = 0;
|
| 1388 |
if (a.role === "ranged") {
|
| 1389 |
+
if (a.noKite) toward = d > a.weapon.range ? 1 : 0;
|
| 1390 |
+
else if (d < (a.preferredRange ?? a.weapon.range * 0.7)) toward = -1;
|
| 1391 |
else if (d > a.weapon.range) toward = 1;
|
| 1392 |
} else if (edgeGap(a, enemy) > reachOf(a)) {
|
| 1393 |
toward = 1;
|
web/tiny.js
CHANGED
|
@@ -65,6 +65,17 @@ function whenEl(id, cb) {
|
|
| 65 |
o.observe(document.body, { childList: true, subtree: true })
|
| 66 |
}
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
// Gradio wraps each gr.HTML block in a `<div class="prose gradio-style …">` whose versioned rules
|
| 69 |
// (.gradio-container-X .prose h2 / .prose * ; .gradio-container-X .gradio-style button) outrank the
|
| 70 |
// shared component CSS — recolouring/resizing our headings and stripping pill/button borders +
|
|
@@ -226,6 +237,8 @@ const ENEMY_AGGRO = 220 // FIELD units (~8 tiles): enemy idles until the player
|
|
| 226 |
// Ranged reach is authored in arena GW units (BOW_GW=1000 ≈ 37 roam tiles — whole-screen). The
|
| 227 |
// roam map is far smaller, so scale ranged weapon range + kite distance down to ~8 tiles here.
|
| 228 |
const ROAM_RANGE_MUL = 0.22
|
|
|
|
|
|
|
| 229 |
// A persona → controllable hero (class → sheets + engine profession).
|
| 230 |
const buildPlayer = (chars, p) => {
|
| 231 |
const pc = chars[CLASS_SLUG[p?.unitClass]] || chars['true-heroes-iii-fighter']
|
|
@@ -354,7 +367,10 @@ whenEl('battle-stage', async (el) => {
|
|
| 354 |
const buildRoster = (list) => list.map((e) => {
|
| 355 |
const c = chars[e.slug]; if (!c) return null
|
| 356 |
// aggroRadius null = always engage → foes advance toward the hero (waves), not idle until near.
|
| 357 |
-
|
|
|
|
|
|
|
|
|
|
| 358 |
}).filter(Boolean)
|
| 359 |
const rosters = Object.fromEntries(Object.entries(GAME_ROSTERS).map(([k, v]) => [k, buildRoster(v)]))
|
| 360 |
// Mount with NO hero → the map shows and the player picks a persona from the bottom picker. The
|
|
@@ -447,8 +463,9 @@ whenEl('battle-stage', async (el) => {
|
|
| 447 |
|
| 448 |
// ── Character sheet — a top-right button slides a hero panel in from the right (sidebar-style). ──
|
| 449 |
let sheetOpen = false
|
| 450 |
-
const sheetBtn = document.createElement('button'); sheetBtn.type = 'button'; sheetBtn.
|
| 451 |
-
sheetBtn.
|
|
|
|
| 452 |
const sheet = document.createElement('aside')
|
| 453 |
sheet.style.cssText = 'position:absolute;top:0;right:0;bottom:0;width:min(300px,82vw);z-index:8;background:rgba(16,20,27,.97);border-left:1px solid #2a3340;box-shadow:-8px 0 24px rgba(0,0,0,.45);transform:translateX(100%);transition:transform .22s ease;overflow-y:auto;color:#e8e8e8;font:13px var(--tac-font,system-ui);box-sizing:border-box;padding:18px 16px'
|
| 454 |
// A forged-skill detail bottom-sheet: big action illustration (lazy Klein render, cached),
|
|
@@ -701,6 +718,11 @@ whenEl('battle-stage', async (el) => {
|
|
| 701 |
} else { sheetBackdrop?.remove(); sheetBackdrop = null }
|
| 702 |
}
|
| 703 |
sheetBtn.addEventListener('click', () => setSheet(!sheetOpen))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 704 |
el.append(sheetBtn, sheet)
|
| 705 |
|
| 706 |
// ── XP / level HUD chip (top-left) — always visible while a hero is in play ──
|
|
|
|
| 65 |
o.observe(document.body, { childList: true, subtree: true })
|
| 66 |
}
|
| 67 |
|
| 68 |
+
// The app-sidebar toggle (‹/›) sits top-left over the XP bar — hide it; the nav drawer is now
|
| 69 |
+
// shown/hidden with the ` or ~ key instead. (The drawer starts collapsed via tac-collapsed.)
|
| 70 |
+
;(function navDrawerKey() {
|
| 71 |
+
const css = document.createElement('style'); css.textContent = '.tac-toggle{display:none !important}'
|
| 72 |
+
document.head.appendChild(css)
|
| 73 |
+
window.addEventListener('keydown', (e) => {
|
| 74 |
+
const t = e.target; if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return
|
| 75 |
+
if (e.code === 'Backquote' || e.key === '`' || e.key === '~') { document.body.classList.toggle('tac-collapsed'); e.preventDefault() }
|
| 76 |
+
})
|
| 77 |
+
})()
|
| 78 |
+
|
| 79 |
// Gradio wraps each gr.HTML block in a `<div class="prose gradio-style …">` whose versioned rules
|
| 80 |
// (.gradio-container-X .prose h2 / .prose * ; .gradio-container-X .gradio-style button) outrank the
|
| 81 |
// shared component CSS — recolouring/resizing our headings and stripping pill/button borders +
|
|
|
|
| 237 |
// Ranged reach is authored in arena GW units (BOW_GW=1000 ≈ 37 roam tiles — whole-screen). The
|
| 238 |
// roam map is far smaller, so scale ranged weapon range + kite distance down to ~8 tiles here.
|
| 239 |
const ROAM_RANGE_MUL = 0.22
|
| 240 |
+
// Early foes should die in ~2 Warrior basics, then ramp with threat (hpMul). Lower base HP + armor.
|
| 241 |
+
const ENEMY_HP_SCALE = 0.2, ENEMY_ARMOR_SCALE = 0.25
|
| 242 |
// A persona → controllable hero (class → sheets + engine profession).
|
| 243 |
const buildPlayer = (chars, p) => {
|
| 244 |
const pc = chars[CLASS_SLUG[p?.unitClass]] || chars['true-heroes-iii-fighter']
|
|
|
|
| 367 |
const buildRoster = (list) => list.map((e) => {
|
| 368 |
const c = chars[e.slug]; if (!c) return null
|
| 369 |
// aggroRadius null = always engage → foes advance toward the hero (waves), not idle until near.
|
| 370 |
+
// Weaker base stats so early foes die in ~2 Warrior basics (they still ramp with threat via hpMul).
|
| 371 |
+
// noKite: ranged foes hold ground + shoot instead of backing away.
|
| 372 |
+
const stats = { ...e.stats, hp: Math.max(1, Math.round((e.stats.hp || 100) * ENEMY_HP_SCALE)), armor: Math.round((e.stats.armor || 0) * ENEMY_ARMOR_SCALE) }
|
| 373 |
+
return { name: e.name, sheets: sheetsOf(c), unit: { name: e.name, stats, attackType: e.attackType, skills: [], aggroRadius: null, rangeMul: ROAM_RANGE_MUL, noKite: true } }
|
| 374 |
}).filter(Boolean)
|
| 375 |
const rosters = Object.fromEntries(Object.entries(GAME_ROSTERS).map(([k, v]) => [k, buildRoster(v)]))
|
| 376 |
// Mount with NO hero → the map shows and the player picks a persona from the bottom picker. The
|
|
|
|
| 463 |
|
| 464 |
// ── Character sheet — a top-right button slides a hero panel in from the right (sidebar-style). ──
|
| 465 |
let sheetOpen = false
|
| 466 |
+
const sheetBtn = document.createElement('button'); sheetBtn.type = 'button'; sheetBtn.title = 'Character (C)'
|
| 467 |
+
sheetBtn.innerHTML = '☰<span style="font:700 9px var(--tac-font,system-ui);display:block;margin-top:-2px;color:#9aa4b2;letter-spacing:.04em">C</span>'
|
| 468 |
+
sheetBtn.style.cssText = 'position:absolute;top:14px;right:14px;z-index:7;width:44px;height:46px;border-radius:10px;border:1px solid #2a3340;background:rgba(20,24,33,.85);color:#e8e8e8;font:600 17px var(--tac-font,system-ui);cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;line-height:1'
|
| 469 |
const sheet = document.createElement('aside')
|
| 470 |
sheet.style.cssText = 'position:absolute;top:0;right:0;bottom:0;width:min(300px,82vw);z-index:8;background:rgba(16,20,27,.97);border-left:1px solid #2a3340;box-shadow:-8px 0 24px rgba(0,0,0,.45);transform:translateX(100%);transition:transform .22s ease;overflow-y:auto;color:#e8e8e8;font:13px var(--tac-font,system-ui);box-sizing:border-box;padding:18px 16px'
|
| 471 |
// A forged-skill detail bottom-sheet: big action illustration (lazy Klein render, cached),
|
|
|
|
| 718 |
} else { sheetBackdrop?.remove(); sheetBackdrop = null }
|
| 719 |
}
|
| 720 |
sheetBtn.addEventListener('click', () => setSheet(!sheetOpen))
|
| 721 |
+
// Press C to open/close the character sheet (ignored while typing).
|
| 722 |
+
window.addEventListener('keydown', (e) => {
|
| 723 |
+
const t = e.target; if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return
|
| 724 |
+
if (e.key === 'c' || e.key === 'C') { setSheet(!sheetOpen); e.preventDefault() }
|
| 725 |
+
})
|
| 726 |
el.append(sheetBtn, sheet)
|
| 727 |
|
| 728 |
// ── XP / level HUD chip (top-left) — always visible while a hero is in play ──
|