polats Claude Opus 4.8 (1M context) commited on
Commit
2394094
·
1 Parent(s): f92b745

Rebuild comboBattler: escalation waves/elites + hit juice

Browse files

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. 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") playDie(e.who, actorOf(e.who));
 
 
 
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
- acc.t += Math.min(dt, 0.1);
6319
- while (acc.t >= STEP) {
6320
- step(battle, STEP);
6321
- acc.t -= STEP;
 
 
 
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 a = spawnActor(battle, { ...entry.unit || {}, name: entry.name }, "enemy", id);
 
6571
  a.x = pos.x;
6572
  a.y = pos.y;
6573
- if (alive) await R.addActor(id, { name: entry.name, ...sd(entry.sheets) });
 
 
 
 
 
 
 
 
 
 
 
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 need = Math.min(TARGET_FOES, MAX_FOES) - ea().filter((a) => a.alive).length;
 
 
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
- addFoe(roster[Math.random() * roster.length | 0], pos);
 
 
 
 
 
 
 
 
 
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;