Combat: refresh at every kill, seamless countdown to the next enemy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
35491c6f4d
commit
57bcd812ee
2 changed files with 26 additions and 21 deletions
3
app.py
3
app.py
|
|
@ -113,6 +113,8 @@ def player_view(world: dict, player: dict, now: float) -> dict:
|
||||||
action, fight = game.growing(player), player["action"].get("fight")
|
action, fight = game.growing(player), player["action"].get("fight")
|
||||||
duration = fight["duration"] if fight else game.action_time(world, player, action)
|
duration = fight["duration"] if fight else game.action_time(world, player, action)
|
||||||
view["action"] = {**player["action"], "growing": action["id"], "duration": duration}
|
view["action"] = {**player["action"], "growing": action["id"], "duration": duration}
|
||||||
|
if fight:
|
||||||
|
view["action"]["respawn"] = game.RESPAWN / game.speed(world)
|
||||||
# A fight takes as long as it takes: show the average time to a kill.
|
# A fight takes as long as it takes: show the average time to a kill.
|
||||||
view["times"] = {action["id"]: (view["fight_odds"][action["id"]]["kill_time"] + game.RESPAWN) / game.speed(world)
|
view["times"] = {action["id"]: (view["fight_odds"][action["id"]]["kill_time"] + game.RESPAWN) / game.speed(world)
|
||||||
if "enemy" in action else game.action_time(world, player, action) for action in game.ACTIONS}
|
if "enemy" in action else game.action_time(world, player, action) for action in game.ACTIONS}
|
||||||
|
|
@ -289,6 +291,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||||
player["last_active"] = now
|
player["last_active"] = now
|
||||||
error = self.player_route(path, player, body, now)
|
error = self.player_route(path, player, body, now)
|
||||||
if error is None:
|
if error is None:
|
||||||
|
game.process_player(world, player, now, self.store.rng) # a new fight is drawn up at once
|
||||||
game.tick(world, now, self.store.rng)
|
game.tick(world, now, self.store.rng)
|
||||||
self.store.save()
|
self.store.save()
|
||||||
if error:
|
if error:
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ let selected = null; // bank item
|
||||||
let pickedClass = null; // registration
|
let pickedClass = null; // registration
|
||||||
const tabs = {}; // skill -> selected recipe tab
|
const tabs = {}; // skill -> selected recipe tab
|
||||||
let refreshing = false;
|
let refreshing = false;
|
||||||
let refreshedFor = null; // action start a completion refresh was sent for
|
let refreshedFor = null; // action (start and length) a completion refresh was sent for
|
||||||
|
let retriedAt = 0; // last time a fight past its kill asked again for the next one
|
||||||
let version = null; // server build this page was loaded from
|
let version = null; // server build this page was loaded from
|
||||||
let failures = 0; // polls in a row that didn't reach the server
|
let failures = 0; // polls in a row that didn't reach the server
|
||||||
|
|
||||||
|
|
@ -481,20 +482,19 @@ function animateFight(now) {
|
||||||
const a = S.me?.action, f = a?.fight;
|
const a = S.me?.action, f = a?.fight;
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
const t = now - a.start;
|
const t = now - a.start;
|
||||||
{
|
const over = t >= f.end;
|
||||||
let i = -1;
|
// From the kill until the next enemy turns up (the next fight's start, once the server sends it),
|
||||||
while (i + 1 < f.events.length && f.events[i + 1][0] <= t) i++;
|
// the panel is the same quiet countdown: no enemy, both swing timers empty.
|
||||||
const left = i >= 0 ? f.events[i][5] : f.enemy_hp;
|
const waiting = t < 0 || (over && f.result === "won");
|
||||||
for (const el of document.querySelectorAll("[data-progress]")) el.style.width = `${left / f.enemy_hp * 100}%`;
|
const respawnIn = t < 0 ? -t : f.end + (a.respawn || 1) - t;
|
||||||
}
|
|
||||||
const panel = $("[data-fight]");
|
|
||||||
if (!panel) return;
|
|
||||||
// Between kills: the next enemy is on its way, both swing timers empty.
|
|
||||||
panel.classList.toggle("waiting", t < 0);
|
|
||||||
let shown = -1;
|
let shown = -1;
|
||||||
while (shown + 1 < f.events.length && f.events[shown + 1][0] <= t) shown++;
|
while (shown + 1 < f.events.length && f.events[shown + 1][0] <= t) shown++;
|
||||||
const last = f.events[shown];
|
const last = f.events[shown];
|
||||||
const hp = last ? last[4] : f.start_hp, foeHp = last ? last[5] : f.enemy_hp;
|
const hp = last ? last[4] : f.start_hp, foeHp = waiting ? 0 : last ? last[5] : f.enemy_hp;
|
||||||
|
for (const el of document.querySelectorAll("[data-progress]")) el.style.width = `${foeHp / f.enemy_hp * 100}%`;
|
||||||
|
const panel = $("[data-fight]");
|
||||||
|
if (!panel) return;
|
||||||
|
panel.classList.toggle("waiting", waiting);
|
||||||
// Your HP everywhere follows the fight as it plays out.
|
// Your HP everywhere follows the fight as it plays out.
|
||||||
for (const el of document.querySelectorAll("[data-live-hp]")) el.textContent = fmt(hp);
|
for (const el of document.querySelectorAll("[data-live-hp]")) el.textContent = fmt(hp);
|
||||||
for (const el of document.querySelectorAll("[data-live-hpbar]")) el.style.width = `${Math.max(0, hp / f.top * 100)}%`;
|
for (const el of document.querySelectorAll("[data-live-hpbar]")) el.style.width = `${Math.max(0, hp / f.top * 100)}%`;
|
||||||
|
|
@ -502,21 +502,19 @@ function animateFight(now) {
|
||||||
set("you-bar", (el) => { el.style.width = `${Math.max(0, hp / f.top * 100)}%`; });
|
set("you-bar", (el) => { el.style.width = `${Math.max(0, hp / f.top * 100)}%`; });
|
||||||
set("foe-bar", (el) => { el.style.width = `${Math.max(0, foeHp / f.enemy_hp * 100)}%`; });
|
set("foe-bar", (el) => { el.style.width = `${Math.max(0, foeHp / f.enemy_hp * 100)}%`; });
|
||||||
set("you-hp", (el) => { el.textContent = `${fmt(hp)} / ${fmt(f.top)}`; });
|
set("you-hp", (el) => { el.textContent = `${fmt(hp)} / ${fmt(f.top)}`; });
|
||||||
set("foe-hp", (el) => { el.textContent = `${fmt(foeHp)} / ${fmt(f.enemy_hp)}`; });
|
set("foe-hp", (el) => { el.textContent = waiting ? "" : `${fmt(foeHp)} / ${fmt(f.enemy_hp)}`; });
|
||||||
const over = t >= f.end;
|
|
||||||
for (const [who, every] of [["you", f.interval], ["foe", f.enemy_interval]]) {
|
for (const [who, every] of [["you", f.interval], ["foe", f.enemy_interval]]) {
|
||||||
let swung = 0;
|
let swung = 0;
|
||||||
for (let i = shown; i >= 0; i--) if (f.events[i][1] === who && f.events[i][2] !== "eat" && f.events[i][2] !== "wind") { swung = f.events[i][0]; break; }
|
for (let i = shown; i >= 0; i--) if (f.events[i][1] === who && f.events[i][2] !== "eat" && f.events[i][2] !== "wind") { swung = f.events[i][0]; break; }
|
||||||
set(`${who}-swing`, (el) => { el.style.width = over || t < 0 ? "0%" : `${Math.min(100, (t - swung) / every * 100)}%`; });
|
set(`${who}-swing`, (el) => { el.style.width = over || waiting ? "0%" : `${Math.min(100, (t - swung) / every * 100)}%`; });
|
||||||
}
|
}
|
||||||
set("log", (el) => {
|
set("log", (el) => {
|
||||||
const key = `${a.start}:${shown}:${over}:${t < 0 ? Math.ceil(-t * 10) : ""}`;
|
const key = `${a.start}:${shown}:${over}:${waiting ? Math.ceil(respawnIn * 10) : ""}`;
|
||||||
if (el.dataset.key === key) return;
|
if (el.dataset.key === key) return;
|
||||||
el.dataset.key = key;
|
el.dataset.key = key;
|
||||||
const lines = f.events.slice(Math.max(0, shown - 5), shown + 1).reverse().map((e) => fightLine(e, f.enemy));
|
const lines = f.events.slice(Math.max(0, shown - 5), shown + 1).reverse().map((e) => fightLine(e, f.enemy));
|
||||||
if (over) lines.unshift(f.result === "won" ? `<li class="good"><b>${esc(f.enemy)} is beaten.</b> The next one is coming…</li>`
|
if (over && !waiting) lines.unshift(`<li class="bad"><b>You fall back.</b></li>`);
|
||||||
: `<li class="bad"><b>You fall back.</b></li>`);
|
el.innerHTML = waiting ? `<li class="good"><b>Victory!</b> ${respawnIn > 0.05 ? `Next enemy in ${respawnIn.toFixed(1)}s` : "Next enemy coming…"}</li>`
|
||||||
el.innerHTML = t < 0 ? `<li class="good">Victory! Next enemy in ${(-t).toFixed(1)}s</li>`
|
|
||||||
: lines.join("") || `<li class="muted">Squaring up…</li>`;
|
: lines.join("") || `<li class="muted">Squaring up…</li>`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1174,9 +1172,13 @@ function animate() {
|
||||||
const pct = Math.min(1, (now - a.start) / a.duration);
|
const pct = Math.min(1, (now - a.start) / a.duration);
|
||||||
// In a fight the bar is the enemy's health (set by animateFight), not a timer.
|
// In a fight the bar is the enemy's health (set by animateFight), not a timer.
|
||||||
if (!a.fight) for (const el of document.querySelectorAll("[data-progress]")) el.style.width = (pct * 100) + "%";
|
if (!a.fight) for (const el of document.querySelectorAll("[data-progress]")) el.style.width = (pct * 100) + "%";
|
||||||
if (pct >= 1 && refreshedFor !== a.start) {
|
const key = `${a.start}:${a.duration}`;
|
||||||
refreshedFor = a.start;
|
if (pct >= 1 && refreshedFor !== key) {
|
||||||
|
refreshedFor = key;
|
||||||
setTimeout(refresh, 150);
|
setTimeout(refresh, 150);
|
||||||
|
} else if (a.fight && now - a.start > a.duration + (a.respawn || 1) && now - retriedAt > 2) {
|
||||||
|
retriedAt = now; // the next enemy hasn't turned up yet: ask again
|
||||||
|
refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const el of document.querySelectorAll("[data-span]")) {
|
for (const el of document.querySelectorAll("[data-span]")) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue