Spaces:
Running
Running
Rebuild comboBattler: escalation waves/elites + hit juice
Browse filesCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- web/comboBattler.js +100 -12
web/comboBattler.js
CHANGED
|
@@ -4953,12 +4953,16 @@ async function createCombatRenderer({ pixi, defsById = {}, layers, coords, getBa
|
|
| 4953 |
const v = view[e.who];
|
| 4954 |
if (v) v.flash = 130;
|
| 4955 |
playHurt(e.who, actorOf(e.who));
|
|
|
|
| 4956 |
e.empowered ? floatText(e.who, "-" + e.amount + "!", GOLD, { big: true }) : floatText(e.who, "-" + e.amount, 16743018);
|
| 4957 |
} else if (e.kind === "heal" && e.amount > 0) e.empowered ? floatText(e.who, "+" + e.amount + "!", GOLD, { big: true }) : floatText(e.who, "+" + e.amount, 9429896);
|
| 4958 |
else if (e.kind === "cond" && e.empowered) floatText(e.who, (e.cond === "interrupted" ? "INTERRUPT" : String(e.cond).toUpperCase()) + "!", GOLD, { big: true });
|
| 4959 |
else if (e.kind === "empower") flagEmpower(e.who);
|
| 4960 |
else if (e.kind === "miss") floatText(e.who, "dodge", 10405352);
|
| 4961 |
-
else if (e.kind === "death")
|
|
|
|
|
|
|
|
|
|
| 4962 |
}
|
| 4963 |
}
|
| 4964 |
return {
|
|
@@ -6062,6 +6066,13 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 6062 |
let emoteLayer = null;
|
| 6063 |
const emoPrev = /* @__PURE__ */ new Map();
|
| 6064 |
let idleEmoAcc = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6065 |
const depthScale = { v: 2 };
|
| 6066 |
let offTick = null, keyHandlers = null, tapHandlers = null, controlsEl = null, alive = true;
|
| 6067 |
const keys = { x: 0, y: 0 };
|
|
@@ -6315,10 +6326,13 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 6315 |
req.skill = 0;
|
| 6316 |
if (s) setInput(battle, "P0", { action: s.id });
|
| 6317 |
}
|
| 6318 |
-
|
| 6319 |
-
|
| 6320 |
-
|
| 6321 |
-
acc.t
|
|
|
|
|
|
|
|
|
|
| 6322 |
}
|
| 6323 |
if (!dead && p && !p.alive) {
|
| 6324 |
dead = true;
|
|
@@ -6331,7 +6345,41 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 6331 |
R.syncActors(battle, dtMS, battle.t);
|
| 6332 |
R.updateFloats(dtMS);
|
| 6333 |
R.drawProjectiles(battle);
|
| 6334 |
-
R.processLog(battle, cursor
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6335 |
updateEmotes(dt);
|
| 6336 |
drawRings();
|
| 6337 |
drawMarkers();
|
|
@@ -6469,6 +6517,13 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 6469 |
}
|
| 6470 |
emoteLayer = null;
|
| 6471 |
emoPrev.clear();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6472 |
try {
|
| 6473 |
combatRoot?.destroy({ children: true });
|
| 6474 |
} catch {
|
|
@@ -6559,27 +6614,49 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 6559 |
let foeN = 0;
|
| 6560 |
const deadAt = /* @__PURE__ */ new Map();
|
| 6561 |
const spawnAcc = { t: SPAWN_INTERVAL };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6562 |
const rosterAt = (fx2, fy2) => {
|
| 6563 |
const w = fieldToWorld(fx2, fy2);
|
| 6564 |
const b = map.biomeAt(Math.round(w.x / TILE8), Math.round(w.y / TILE8)) || "forgottenPlains";
|
| 6565 |
const r = rosters[b];
|
| 6566 |
return r && r.length ? r : rosters.forgottenPlains || [];
|
| 6567 |
};
|
| 6568 |
-
const addFoe = async (entry, pos) => {
|
| 6569 |
const id = "R" + foeN++;
|
| 6570 |
-
const
|
|
|
|
| 6571 |
a.x = pos.x;
|
| 6572 |
a.y = pos.y;
|
| 6573 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6574 |
};
|
| 6575 |
spawnFn = (dt) => {
|
| 6576 |
if (dead || !battle) return;
|
| 6577 |
const p = pa();
|
| 6578 |
if (!p) return;
|
|
|
|
|
|
|
| 6579 |
for (const a of ea()) {
|
| 6580 |
if (!a.alive) {
|
| 6581 |
if (!deadAt.has(a.id)) {
|
| 6582 |
deadAt.set(a.id, battle.t);
|
|
|
|
| 6583 |
heroEmote("happy");
|
| 6584 |
try {
|
| 6585 |
opts.onEnemyDefeated?.({ id: a.id, name: a.name, profession: a.profession, tier: a.tier, x: a.x, y: a.y, t: battle.t });
|
|
@@ -6598,14 +6675,25 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 6598 |
spawnAcc.t += dt;
|
| 6599 |
if (spawnAcc.t < SPAWN_INTERVAL) return;
|
| 6600 |
spawnAcc.t = 0;
|
| 6601 |
-
const
|
|
|
|
|
|
|
| 6602 |
for (let i = 0; i < need; i++) {
|
| 6603 |
const ang = Math.random() * Math.PI * 2;
|
| 6604 |
const rr = (SPAWN_TILES + (Math.random() * 2 - 1) * SPAWN_JITTER) * FSTEP;
|
| 6605 |
const pos = snapField(p.x + Math.cos(ang) * rr, p.y + Math.sin(ang) * rr);
|
| 6606 |
const roster = rosterAt(pos.x, pos.y);
|
| 6607 |
if (!roster.length) continue;
|
| 6608 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6609 |
}
|
| 6610 |
};
|
| 6611 |
bindKeys();
|
|
@@ -6680,7 +6768,7 @@ function mountComboBattler(pixi, host, opts = {}) {
|
|
| 6680 |
const p = pa();
|
| 6681 |
return p ? { name: p.name, profession: p.profession, hp: Math.round(p.hp), maxHp: Math.round(p.maxHp), skills: (p.bar || []).map((s) => s.name) } : null;
|
| 6682 |
}
|
| 6683 |
-
const ctrl = { ready, selectHero, getSpawnWorld, getSnapshot, getHero, heroEmote, levelUpHero, setHeroSkills, resize, onChange, destroy, map, walkable: (wx, wy) => roamWalkable(wx, wy) };
|
| 6684 |
if (typeof window !== "undefined") {
|
| 6685 |
window.__comboSnap = () => ctrl.getSnapshot();
|
| 6686 |
window.__combo = ctrl;
|
|
|
|
| 4953 |
const v = view[e.who];
|
| 4954 |
if (v) v.flash = 130;
|
| 4955 |
playHurt(e.who, actorOf(e.who));
|
| 4956 |
+
hooks.onHit?.(e);
|
| 4957 |
e.empowered ? floatText(e.who, "-" + e.amount + "!", GOLD, { big: true }) : floatText(e.who, "-" + e.amount, 16743018);
|
| 4958 |
} else if (e.kind === "heal" && e.amount > 0) e.empowered ? floatText(e.who, "+" + e.amount + "!", GOLD, { big: true }) : floatText(e.who, "+" + e.amount, 9429896);
|
| 4959 |
else if (e.kind === "cond" && e.empowered) floatText(e.who, (e.cond === "interrupted" ? "INTERRUPT" : String(e.cond).toUpperCase()) + "!", GOLD, { big: true });
|
| 4960 |
else if (e.kind === "empower") flagEmpower(e.who);
|
| 4961 |
else if (e.kind === "miss") floatText(e.who, "dodge", 10405352);
|
| 4962 |
+
else if (e.kind === "death") {
|
| 4963 |
+
playDie(e.who, actorOf(e.who));
|
| 4964 |
+
hooks.onDeath?.(e);
|
| 4965 |
+
}
|
| 4966 |
}
|
| 4967 |
}
|
| 4968 |
return {
|
|
|
|
| 6066 |
let emoteLayer = null;
|
| 6067 |
const emoPrev = /* @__PURE__ */ new Map();
|
| 6068 |
let idleEmoAcc = 0;
|
| 6069 |
+
let runState = { time: 0, kills: 0, diff: 1 };
|
| 6070 |
+
let hitstopT = 0;
|
| 6071 |
+
const shake = { t: 0, mag: 0 };
|
| 6072 |
+
const addShake = (dur, mag) => {
|
| 6073 |
+
shake.t = Math.max(shake.t, dur);
|
| 6074 |
+
shake.mag = Math.max(shake.mag, mag);
|
| 6075 |
+
};
|
| 6076 |
const depthScale = { v: 2 };
|
| 6077 |
let offTick = null, keyHandlers = null, tapHandlers = null, controlsEl = null, alive = true;
|
| 6078 |
const keys = { x: 0, y: 0 };
|
|
|
|
| 6326 |
req.skill = 0;
|
| 6327 |
if (s) setInput(battle, "P0", { action: s.id });
|
| 6328 |
}
|
| 6329 |
+
if (hitstopT > 0) hitstopT = Math.max(0, hitstopT - dt);
|
| 6330 |
+
else {
|
| 6331 |
+
acc.t += Math.min(dt, 0.1);
|
| 6332 |
+
while (acc.t >= STEP) {
|
| 6333 |
+
step(battle, STEP);
|
| 6334 |
+
acc.t -= STEP;
|
| 6335 |
+
}
|
| 6336 |
}
|
| 6337 |
if (!dead && p && !p.alive) {
|
| 6338 |
dead = true;
|
|
|
|
| 6345 |
R.syncActors(battle, dtMS, battle.t);
|
| 6346 |
R.updateFloats(dtMS);
|
| 6347 |
R.drawProjectiles(battle);
|
| 6348 |
+
R.processLog(battle, cursor, {
|
| 6349 |
+
onHit: (e) => {
|
| 6350 |
+
const p2 = pa();
|
| 6351 |
+
if (!p2) return;
|
| 6352 |
+
if (e.who === p2.id) {
|
| 6353 |
+
addShake(0.16, 5);
|
| 6354 |
+
hitstopT = Math.max(hitstopT, 0.03);
|
| 6355 |
+
} else if (e.src === p2.id) {
|
| 6356 |
+
if (e.amount >= 12) hitstopT = Math.max(hitstopT, 0.035);
|
| 6357 |
+
if (e.amount >= 28) addShake(0.1, 3);
|
| 6358 |
+
}
|
| 6359 |
+
},
|
| 6360 |
+
onDeath: (e) => {
|
| 6361 |
+
const p2 = pa();
|
| 6362 |
+
if (!p2) return;
|
| 6363 |
+
if (e.who === p2.id) {
|
| 6364 |
+
addShake(0.45, 11);
|
| 6365 |
+
hitstopT = Math.max(hitstopT, 0.07);
|
| 6366 |
+
} else {
|
| 6367 |
+
addShake(0.2, 6);
|
| 6368 |
+
hitstopT = Math.max(hitstopT, 0.06);
|
| 6369 |
+
}
|
| 6370 |
+
}
|
| 6371 |
+
});
|
| 6372 |
+
{
|
| 6373 |
+
const app = map.getApp?.();
|
| 6374 |
+
if (app?.stage) {
|
| 6375 |
+
if (shake.t > 0) {
|
| 6376 |
+
shake.t = Math.max(0, shake.t - dt);
|
| 6377 |
+
const m = shake.mag * (shake.t / 0.2);
|
| 6378 |
+
app.stage.position.set((Math.random() * 2 - 1) * m, (Math.random() * 2 - 1) * m);
|
| 6379 |
+
if (shake.t === 0) app.stage.position.set(0, 0);
|
| 6380 |
+
}
|
| 6381 |
+
}
|
| 6382 |
+
}
|
| 6383 |
updateEmotes(dt);
|
| 6384 |
drawRings();
|
| 6385 |
drawMarkers();
|
|
|
|
| 6517 |
}
|
| 6518 |
emoteLayer = null;
|
| 6519 |
emoPrev.clear();
|
| 6520 |
+
hitstopT = 0;
|
| 6521 |
+
shake.t = 0;
|
| 6522 |
+
shake.mag = 0;
|
| 6523 |
+
try {
|
| 6524 |
+
map.getApp()?.stage?.position.set(0, 0);
|
| 6525 |
+
} catch {
|
| 6526 |
+
}
|
| 6527 |
try {
|
| 6528 |
combatRoot?.destroy({ children: true });
|
| 6529 |
} catch {
|
|
|
|
| 6614 |
let foeN = 0;
|
| 6615 |
const deadAt = /* @__PURE__ */ new Map();
|
| 6616 |
const spawnAcc = { t: SPAWN_INTERVAL };
|
| 6617 |
+
let nextEliteAt = 8, nextBossAt = 24;
|
| 6618 |
+
runState = { time: 0, kills: 0, diff: 1 };
|
| 6619 |
+
const TIER = {
|
| 6620 |
+
// hp/dmg multipliers relative to the current difficulty; bosses are tanky, not one-shotters
|
| 6621 |
+
elite: { hp: 2.4, dmg: 1.4, scale: 1.4 },
|
| 6622 |
+
boss: { hp: 4.6, dmg: 1.8, scale: 1.95 }
|
| 6623 |
+
};
|
| 6624 |
const rosterAt = (fx2, fy2) => {
|
| 6625 |
const w = fieldToWorld(fx2, fy2);
|
| 6626 |
const b = map.biomeAt(Math.round(w.x / TILE8), Math.round(w.y / TILE8)) || "forgottenPlains";
|
| 6627 |
const r = rosters[b];
|
| 6628 |
return r && r.length ? r : rosters.forgottenPlains || [];
|
| 6629 |
};
|
| 6630 |
+
const addFoe = async (entry, pos, { hpMul = 1, dmgMul = 1, tier = null } = {}) => {
|
| 6631 |
const id = "R" + foeN++;
|
| 6632 |
+
const name = tier === "boss" ? `${entry.name} the Champion` : tier === "elite" ? `Elite ${entry.name}` : entry.name;
|
| 6633 |
+
const a = spawnActor(battle, { ...entry.unit || {}, name, hpMul, dmgMul }, "enemy", id);
|
| 6634 |
a.x = pos.x;
|
| 6635 |
a.y = pos.y;
|
| 6636 |
+
if (tier) a.tier = tier;
|
| 6637 |
+
if (alive) await R.addActor(id, { name, ...sd(entry.sheets) });
|
| 6638 |
+
if (tier && TIER[tier]) {
|
| 6639 |
+
const v = R.view?.[id];
|
| 6640 |
+
if (v?.container) v.container.scale.set(TIER[tier].scale);
|
| 6641 |
+
}
|
| 6642 |
+
if (tier) {
|
| 6643 |
+
try {
|
| 6644 |
+
emoteLayer?.show(id, "angry", battle.t, 1.6);
|
| 6645 |
+
} catch {
|
| 6646 |
+
}
|
| 6647 |
+
}
|
| 6648 |
};
|
| 6649 |
spawnFn = (dt) => {
|
| 6650 |
if (dead || !battle) return;
|
| 6651 |
const p = pa();
|
| 6652 |
if (!p) return;
|
| 6653 |
+
runState.time += dt;
|
| 6654 |
+
runState.diff = Math.min(4, 1 + runState.time / 80 + runState.kills * 0.03);
|
| 6655 |
for (const a of ea()) {
|
| 6656 |
if (!a.alive) {
|
| 6657 |
if (!deadAt.has(a.id)) {
|
| 6658 |
deadAt.set(a.id, battle.t);
|
| 6659 |
+
runState.kills++;
|
| 6660 |
heroEmote("happy");
|
| 6661 |
try {
|
| 6662 |
opts.onEnemyDefeated?.({ id: a.id, name: a.name, profession: a.profession, tier: a.tier, x: a.x, y: a.y, t: battle.t });
|
|
|
|
| 6675 |
spawnAcc.t += dt;
|
| 6676 |
if (spawnAcc.t < SPAWN_INTERVAL) return;
|
| 6677 |
spawnAcc.t = 0;
|
| 6678 |
+
const diff = runState.diff;
|
| 6679 |
+
const target = Math.min(MAX_FOES, Math.round(TARGET_FOES + diff * 1.5));
|
| 6680 |
+
const need = target - ea().filter((a) => a.alive).length;
|
| 6681 |
for (let i = 0; i < need; i++) {
|
| 6682 |
const ang = Math.random() * Math.PI * 2;
|
| 6683 |
const rr = (SPAWN_TILES + (Math.random() * 2 - 1) * SPAWN_JITTER) * FSTEP;
|
| 6684 |
const pos = snapField(p.x + Math.cos(ang) * rr, p.y + Math.sin(ang) * rr);
|
| 6685 |
const roster = rosterAt(pos.x, pos.y);
|
| 6686 |
if (!roster.length) continue;
|
| 6687 |
+
const entry = roster[Math.random() * roster.length | 0];
|
| 6688 |
+
let opt = { hpMul: diff * (0.85 + Math.random() * 0.3), dmgMul: diff * (0.85 + Math.random() * 0.3) };
|
| 6689 |
+
if (i === 0 && runState.kills >= nextBossAt) {
|
| 6690 |
+
opt = { hpMul: diff * TIER.boss.hp, dmgMul: diff * TIER.boss.dmg, tier: "boss" };
|
| 6691 |
+
nextBossAt += 26 + (Math.random() * 10 | 0);
|
| 6692 |
+
} else if (i === 0 && runState.kills >= nextEliteAt) {
|
| 6693 |
+
opt = { hpMul: diff * TIER.elite.hp, dmgMul: diff * TIER.elite.dmg, tier: "elite" };
|
| 6694 |
+
nextEliteAt += 8 + (Math.random() * 6 | 0);
|
| 6695 |
+
}
|
| 6696 |
+
addFoe(entry, pos, opt);
|
| 6697 |
}
|
| 6698 |
};
|
| 6699 |
bindKeys();
|
|
|
|
| 6768 |
const p = pa();
|
| 6769 |
return p ? { name: p.name, profession: p.profession, hp: Math.round(p.hp), maxHp: Math.round(p.maxHp), skills: (p.bar || []).map((s) => s.name) } : null;
|
| 6770 |
}
|
| 6771 |
+
const ctrl = { ready, selectHero, getSpawnWorld, getSnapshot, getHero, heroEmote, levelUpHero, setHeroSkills, getThreat: () => ({ ...runState }), resize, onChange, destroy, map, walkable: (wx, wy) => roamWalkable(wx, wy) };
|
| 6772 |
if (typeof window !== "undefined") {
|
| 6773 |
window.__comboSnap = () => ctrl.getSnapshot();
|
| 6774 |
window.__combo = ctrl;
|