Instant targeting, damage on bars, interrupted casts, no arrows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
bc99a1e0c5
commit
049e2c771a
6 changed files with 85 additions and 41 deletions
|
|
@ -52,7 +52,7 @@ Beta. The core game is in place; changes from here are mostly balance and tuning
|
|||
Shield Wall; the shield ones need a shield), warhammer hits hard and stuns
|
||||
(Crushing Blow, Concuss, Earthshaker, Unstoppable), staff heals and holds
|
||||
(Bolt, Mend, Ward, Thornbind), bow shoots from afar (Aimed Shot, Pinning
|
||||
Shot, Barbed Arrow, Volley, which takes 5 arrows). Trades: Find the Seam
|
||||
Shot, Barbed Arrow, Volley). Trades: Find the Seam
|
||||
(miner: a target's resistance stops counting), Barricade (builder: the group
|
||||
takes less), Hearty Meal (homesteader: the group heals over time), Mark Prey
|
||||
(ranger: a target takes more from everyone).
|
||||
|
|
|
|||
20
combat.py
20
combat.py
|
|
@ -99,9 +99,9 @@ ABILITIES = {
|
|||
"barbed_arrow": {"name": "Barbed Arrow", "target": "enemy", "cd": 12,
|
||||
"effects": [_dmg(0.4), {"t": "dot", "c": 0.25, "every": 2, "dur": 12}], "auto": {"r": "dot"},
|
||||
"tip": "Hit, then bleeds 6× 0.25 over 12s."},
|
||||
"volley": {"name": "Volley", "target": "aoe", "cd": 10, "cost": 5,
|
||||
"volley": {"name": "Volley", "target": "aoe", "cd": 10,
|
||||
"effects": [{"t": "aoe", "c": 0.9}], "auto": {"r": "aoe", "n": 2},
|
||||
"tip": "Hits every enemy. 5 arrows."},
|
||||
"tip": "Hits every enemy."},
|
||||
# Trades: one each, whatever the weapon.
|
||||
"find_the_seam": {"name": "Find the Seam", "target": "enemy", "cd": 30, "offgcd": True,
|
||||
"effects": [_aura("seam", 8, "target")], "auto": {"r": "debuff", "pct": 0.6},
|
||||
|
|
@ -147,16 +147,13 @@ def known_abilities(style: str, cls: str, level: int, shield: bool) -> list[str]
|
|||
|
||||
def modded(ab: dict, mod: dict | None) -> dict:
|
||||
"""An ability as something you wear changes it: `cd` and `cast` multiply, `c` multiplies every
|
||||
effect's size, `dur` adds seconds to every effect that lasts, `cost` replaces the arrows it takes,
|
||||
and `extra` adds effects."""
|
||||
effect's size, `dur` adds seconds to every effect that lasts, and `extra` adds effects."""
|
||||
if not mod:
|
||||
return ab
|
||||
ab = {**ab, "effects": [dict(eff) for eff in ab["effects"]]}
|
||||
for key in ("cd", "cast"):
|
||||
if key in mod and ab.get(key):
|
||||
ab[key] = ab[key] * mod[key]
|
||||
if "cost" in mod:
|
||||
ab["cost"] = mod["cost"]
|
||||
for eff in ab["effects"]:
|
||||
if "c" in eff and "c" in mod:
|
||||
eff["c"] *= mod["c"]
|
||||
|
|
@ -267,7 +264,7 @@ def _member_unit(member: dict) -> dict:
|
|||
"kit": {a: modded(ABILITIES[a], me.get("mods", {}).get(a)) for a in me["abilities"]},
|
||||
"next": member["at"] + (0.5 if me["first"] else me["interval"]), "stun": 0.0, "target": None,
|
||||
"mend": member["mend"], "wind": member["wind"], "wind_used": False,
|
||||
"potion": member.get("potion") or 0, "drank": False, "arrows": member.get("arrows", 0), "shot": 0,
|
||||
"potion": member.get("potion") or 0, "drank": False,
|
||||
"dealt": 0, "hurt": 0, "down": False, "manual": set(member.get("manual", [])),
|
||||
"watch": member.get("watch", -1.0), "inputs": sorted(member.get("inputs", []), key=lambda i: i[0]),
|
||||
"seen": 0, "queued": None, "picked": False, "ftarget": None}
|
||||
|
|
@ -309,7 +306,7 @@ def run(fight: dict) -> dict:
|
|||
unit = units[member["id"]]
|
||||
won = result == "won" and unit["out"] in (None, "down") and unit["in"]
|
||||
member.update(hp=max(0, unit["hp"]), damage=unit["hurt"], dealt=unit["dealt"], wind_used=unit["wind_used"],
|
||||
drank=unit["drank"], shot=unit["shot"], down=unit["down"],
|
||||
drank=unit["drank"], down=unit["down"],
|
||||
result="won" if won else unit["out"] or ("left" if not unit["in"] else result))
|
||||
fight.update(events=state["events"], kills=state["kills"], result=result,
|
||||
end=_real(state, t), duration=(fight["rest"] + t) / fight["pace"])
|
||||
|
|
@ -453,8 +450,6 @@ def _ready(unit: dict, ab: dict, t: float, manual: bool) -> str | None:
|
|||
return "cooldown"
|
||||
if not ab.get("offgcd") and unit["busy"] + (0 if manual else AUTO_DELAY) > t + 1e-9:
|
||||
return "gcd"
|
||||
if ab.get("cost", 0) > unit["arrows"]:
|
||||
return "arrows"
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -477,7 +472,7 @@ def _think(state: dict, unit: dict, t: float, rng: random.Random) -> None:
|
|||
_use(state, unit, ab, target, t, rng, manual=True)
|
||||
if unit["cast"] or not ab.get("offgcd"):
|
||||
return
|
||||
elif why == "arrows" or t - queued["at"] > QUEUE:
|
||||
elif t - queued["at"] > QUEUE:
|
||||
unit["queued"] = None
|
||||
for ab_id in unit["abilities"]:
|
||||
ab = unit["kit"][ab_id]
|
||||
|
|
@ -516,9 +511,6 @@ def _manual_target(state: dict, unit: dict, ab: dict, given: str | None, t: floa
|
|||
|
||||
def _use(state: dict, unit: dict, ab: dict, target, t: float, rng: random.Random, manual: bool = False) -> None:
|
||||
unit["cds"][ab["id"]] = t + ab.get("cd", 0) * (1 if manual else AUTO_CD)
|
||||
if ab.get("cost"):
|
||||
unit["arrows"] -= ab["cost"]
|
||||
unit["shot"] += ab["cost"]
|
||||
if not ab.get("offgcd"):
|
||||
unit["busy"] = t + max(GCD, ab.get("cast", 0))
|
||||
if ab.get("cast"):
|
||||
|
|
|
|||
16
game.py
16
game.py
|
|
@ -1347,8 +1347,8 @@ UNIQUES = {
|
|||
{"find_the_seam": {"dur": 8}, "mark_prey": {"dur": 15},
|
||||
"ward": {"extra": [{"t": "shield", "c": 1.0, "dur": 20, "second": True}]}}),
|
||||
"wyrmscale_quiver": ("Wyrmscale quiver", "trinket", "quiver", "#e05a5a",
|
||||
"Volley takes no arrows and hits 40% harder; Aimed Shot aims 25% faster.",
|
||||
{"volley": {"cost": 0, "c": 1.4}, "aimed_shot": {"cast": 0.75}}),
|
||||
"Volley hits 40% harder; Aimed Shot aims 25% faster.",
|
||||
{"volley": {"c": 1.4}, "aimed_shot": {"cast": 0.75}}),
|
||||
}
|
||||
# The pieces every dungeon's patterns reforge, by what they're made of at each tier.
|
||||
MASTERWORK_PIECES = ["sword", "warhammer", "staff", "bow", "heavy", "medium", "light"]
|
||||
|
|
@ -2286,13 +2286,13 @@ def fight_member(world: dict, player: dict, at: float = 0.0) -> dict:
|
|||
"start_hp": round(player["hp"]), "hp_in": player["hp"], "top": max_hp(player), "style": me["style"],
|
||||
"role": me["role"], "abilities": me["abilities"], "interval": me["interval"] / speed(world),
|
||||
"fighter": me, "eaten": {}, "potion": ITEMS[potion]["drink"] if potion else 0, "potion_item": potion,
|
||||
"arrows": player["bank"].get("arrows", 0), "mend": blessed["mend"],
|
||||
"mend": blessed["mend"],
|
||||
"wind": blessed["wind"] and not (player["action"] or {}).get("wind_used"), "pp": blessed["cost"],
|
||||
"manual": [a for a in player.get("manual", []) if a in me["abilities"]], "inputs": [], "mods": me["mods"],
|
||||
"watch": at + combat.WATCH * speed(world)}
|
||||
|
||||
|
||||
FIGHT_PRIVATE = ("fighter", "mend", "wind", "arrows")
|
||||
FIGHT_PRIVATE = ("fighter", "mend", "wind")
|
||||
|
||||
|
||||
def fight_view(fight: dict) -> dict:
|
||||
|
|
@ -2891,7 +2891,7 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
|
|||
player["pp"] -= paid
|
||||
summary["pp_spent"] += paid
|
||||
gain_xp(world, player, "prayer", paid * PRAYER_XP_PER_POINT, now, summary)
|
||||
used = {**({mine["potion_item"]: 1} if mine["drank"] else {}), **({"arrows": mine["shot"]} if mine["shot"] else {})}
|
||||
used = {mine["potion_item"]: 1} if mine["drank"] else {}
|
||||
for key, spent in (("eaten", mine["eaten"]), ("used", used)):
|
||||
for item, count in spent.items():
|
||||
count = min(count, player["bank"].get(item, 0))
|
||||
|
|
@ -5978,6 +5978,12 @@ PATCH_NOTES = [
|
|||
"lens, Wyrmscale quiver. Abilities it changes get a ✦."),
|
||||
("new", "Choose when you eat between fights: below 25 / 50 / 75% of your HP, always, or never (the food slot, "
|
||||
"under \"Eat below\"). Default 50%: above it you just rest, so a scratch doesn't cost food."),
|
||||
("new", "Tapping an enemy or ally switches at once, and your next press goes to them: tap, then interrupt, "
|
||||
"without waiting."),
|
||||
("new", "HP bars show what was just lost, with each fighter's last hit or heal by their HP."),
|
||||
("new", "An interrupted cast stays on the enemy's bar for a moment, struck through: ✋ interrupted."),
|
||||
("balance", "Volley no longer takes arrows: abilities never cost anything. Wyrmscale quiver: Volley 40% harder "
|
||||
"and Aimed Shot 25% faster."),
|
||||
]},
|
||||
{"date": "2026-10-24", "title": "A dungeon in every area", "notes": [
|
||||
("new", "Six more dungeons for 2–4, seven pulls each with two mini-bosses and a final boss: Goblin Warren "
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ let refreshing = false;
|
|||
let refreshedFor = null; // action (start and length) a completion refresh was sent for
|
||||
let polledAt = 0;
|
||||
let tagging = false; // this page just tagged into someone's fight
|
||||
// What you last tapped in this fight (an enemy to fight, an ally to heal): shown at once and sent with
|
||||
// every press, so a tap and a press straight after it don't wait on each other.
|
||||
let aim = { fight: null, foe: null, ally: null };
|
||||
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 failures = 0; // polls in a row that didn't reach the server
|
||||
|
|
@ -906,8 +909,8 @@ function combatFrame() {
|
|||
const others = f ? f.members.filter((m) => m.id !== S.uid) : [];
|
||||
const unit = (id, name, foe = false, extra = "") => `<div class="fighter ${foe ? "foe-unit" : id === S.uid ? "" : "ally"} ${f ? "aimable" : ""}"
|
||||
data-u="${esc(id)}" ${f ? `data-aim="${esc(id)}" title="${foe ? "Tap to fight this one" : "Tap to aim your heals here"}"` : ""}>
|
||||
<div class="fighter-name"><b>${name}${extra}</b><span class="small muted"><span data-u-status></span> ${foe ? "" : HEART} <span data-u-hp>${id === S.uid ? `${fmt(me.hp)} / ${me.max_hp}` : ""}</span></span></div>
|
||||
<div class="bar hp ${foe ? "foe" : "you"}"><span data-u-bar${id === S.uid ? ` data-live-hpbar style="width:${me.hp / me.max_hp * 100}%"` : ""}></span></div>
|
||||
<div class="fighter-name"><b>${name}${extra}</b><span class="small muted"><span class="hitnum" data-u-hitnum></span><span data-u-status></span> ${foe ? "" : HEART} <span data-u-hp>${id === S.uid ? `${fmt(me.hp)} / ${me.max_hp}` : ""}</span></span></div>
|
||||
<div class="bar hp ${foe ? "foe" : "you"}"><span class="lag" data-u-lag></span><span data-u-bar${id === S.uid ? ` data-live-hpbar style="width:${me.hp / me.max_hp * 100}%"` : ""}></span></div>
|
||||
<div class="bar swing"><span data-u-swing></span></div>
|
||||
<div class="cast small" data-u-cast></div>
|
||||
</div>`;
|
||||
|
|
@ -973,6 +976,8 @@ function prayerBar(me) {
|
|||
}
|
||||
|
||||
const SWINGS = new Set(["hit", "crit", "miss", "block", "absorb"]);
|
||||
const LAG = 0.6; // seconds the "just lost" part of an HP bar trails behind
|
||||
const STOPPED = 1.2; // seconds an interrupted cast stays on its bar
|
||||
|
||||
// A line of the fight, terse: who, to whom, how much, with what.
|
||||
function fightLine(e, f) {
|
||||
|
|
@ -1023,7 +1028,7 @@ function fightState(f, t) {
|
|||
f.pack.forEach((foe, i) => { st[`e${i}`] = { hp: foe.hp, top: foe.hp, from: rest, every: foe.interval / f.pace, swung: null, cast: null, out: null, held: false, enraged: false, hidden: !!foe.dormant }; });
|
||||
let shown = -1;
|
||||
while (shown + 1 < f.events.length && f.events[shown + 1][0] <= t) {
|
||||
const [time, src, tgt, kind, , hp, label] = f.events[++shown];
|
||||
const [time, src, tgt, kind, n, hp, label] = f.events[++shown];
|
||||
const who = st[src], them = st[tgt];
|
||||
if (them && hp != null) them.hp = hp;
|
||||
if (who && SWINGS.has(kind) && (label === "" || C.damage_types[label])) who.swung = time;
|
||||
|
|
@ -1039,7 +1044,9 @@ function fightState(f, t) {
|
|||
}
|
||||
if (who?.cds && tgt?.[0] === "e" && (SWINGS.has(kind) || kind === "cast" || kind === "use")) who.aim = tgt;
|
||||
if (who && kind === "heal" && label === "potion") who.drank = true;
|
||||
if (them && kind === "stop" && them.cast) them.stopped = { label: them.cast.label, at: time };
|
||||
if (them && (kind === "stop" || kind === "cc")) them.cast = null;
|
||||
if (them && ["hit", "crit", "tick", "heal", "mend"].includes(kind) && n) them.hit = { at: time, n, kind };
|
||||
if (them && kind === "cc") them.held = true;
|
||||
if (them && (kind === "free" || kind === "die")) them.held = false;
|
||||
if (them && kind === "die") them.out = "dead";
|
||||
|
|
@ -1057,6 +1064,7 @@ function animateFight(now) {
|
|||
const over = t >= f.end;
|
||||
const resting = t < rest;
|
||||
const { st, shown } = fightState(f, t);
|
||||
const before = fightState(f, t - LAG).st; // the HP a moment ago, trailing the bar
|
||||
const mine = f.members.find((m) => m.id === S.uid) || f.members[0];
|
||||
const me = st[mine.id];
|
||||
// Resting: everyone's HP climbs back to full while they eat.
|
||||
|
|
@ -1072,15 +1080,25 @@ function animateFight(now) {
|
|||
const pull = panel.querySelector("[data-pull-btn]");
|
||||
if (pull) pull.hidden = !resting;
|
||||
const pick = [...(mine.inputs || [])].reverse().find((i) => i[1] === "target" && (f.rest + i[0]) / f.pace <= t && i[2][0] === "e");
|
||||
const aim = pick && st[pick[2]] && !st[pick[2]].out ? pick[2] : me.aim && st[me.aim] && !st[me.aim].out ? me.aim : null;
|
||||
const live = (id) => id && st[id] && !st[id].out && !st[id].hidden;
|
||||
const tapped = aim.fight === a.start ? aim : {};
|
||||
const aimed = live(tapped.foe) ? tapped.foe : live(pick?.[2]) ? pick[2] : live(me.aim) ? me.aim : null;
|
||||
const healing = live(tapped.ally) ? tapped.ally : null;
|
||||
for (const el of panel.querySelectorAll("[data-u]")) {
|
||||
const u = st[el.dataset.u];
|
||||
if (!u) continue;
|
||||
el.classList.toggle("aimed", el.dataset.u === aim && !over);
|
||||
el.classList.toggle("aimed", (el.dataset.u === aimed || el.dataset.u === healing) && !over);
|
||||
const gone = !!u.out || over;
|
||||
el.classList.toggle("out", !!u.out);
|
||||
el.hidden = !!u.hidden;
|
||||
el.querySelector("[data-u-bar]").style.width = `${Math.max(0, u.hp / u.top * 100)}%`;
|
||||
const was = before[el.dataset.u];
|
||||
el.querySelector("[data-u-lag]").style.width = `${Math.max(0, Math.max(u.hp, resting ? 0 : was?.hp ?? u.hp) / u.top * 100)}%`;
|
||||
const hit = u.hit && t - u.hit.at < 1 ? u.hit : null, num = el.querySelector("[data-u-hitnum]");
|
||||
const healed = hit && (hit.kind === "heal" || hit.kind === "mend");
|
||||
num.textContent = hit ? `${healed ? "+" : "−"}${hit.n}` : "";
|
||||
num.className = `hitnum ${healed ? "good" : "bad"} ${hit?.kind === "crit" ? "crit" : ""}`;
|
||||
num.style.opacity = hit ? `${1 - (t - hit.at)}` : "0";
|
||||
el.querySelector("[data-u-hp]").textContent = u.out === "left" ? "left" : t < u.from && !resting ? "joining…" : `${fmt(u.hp)} / ${fmt(u.top)}`;
|
||||
const status = u.out === "dead" ? "" : u.out === "down" ? "concuss" : u.held ? "thornbind" : u.enraged ? "unstoppable" : "";
|
||||
const statusEl = el.querySelector("[data-u-status]");
|
||||
|
|
@ -1090,12 +1108,17 @@ function animateFight(now) {
|
|||
}
|
||||
el.querySelector("[data-u-swing]").style.width = gone || t < u.from ? "0%" : `${Math.min(100, (t - (u.swung ?? u.from)) / u.every * 100)}%`;
|
||||
const cast = !gone && u.cast && t < u.cast.until ? u.cast : null;
|
||||
const spec = cast && (el.dataset.u[0] === "e" ? C.enemy_abilities : C.abilities)[cast.label];
|
||||
// A cast someone stopped stays up a moment, struck through.
|
||||
const stopped = !cast && u.stopped && t - u.stopped.at < STOPPED ? u.stopped : null;
|
||||
const table = el.dataset.u[0] === "e" ? C.enemy_abilities : C.abilities;
|
||||
const spec = cast ? table[cast.label] : stopped ? table[stopped.label] : null;
|
||||
const castEl = el.querySelector("[data-u-cast]");
|
||||
const key = cast ? `${cast.label}:${cast.from}` : "";
|
||||
const key = cast ? `${cast.label}:${cast.from}` : stopped ? `stopped:${stopped.label}:${stopped.at}` : "";
|
||||
if (castEl.dataset.key !== key) {
|
||||
castEl.dataset.key = key;
|
||||
castEl.innerHTML = cast ? `<span>${spec.icon} ${esc(spec.name)}</span><span class="bar"><span></span></span>` : "";
|
||||
castEl.classList.toggle("stopped", !!stopped);
|
||||
castEl.innerHTML = cast ? `<span>${spec.icon} ${esc(spec.name)}</span><span class="bar"><span></span></span>`
|
||||
: stopped ? `<span>✋ <s>${spec.icon} ${esc(spec.name)}</s></span><span class="small">interrupted</span>` : "";
|
||||
}
|
||||
if (cast) castEl.querySelector(".bar > span").style.width = `${Math.min(100, (t - cast.from) / (cast.until - cast.from) * 100)}%`;
|
||||
}
|
||||
|
|
@ -2300,13 +2323,22 @@ document.addEventListener("click", (e) => {
|
|||
if (d.press) {
|
||||
if (!S.me.manual.includes(d.press)) return toast("On auto: tap AUTO to take it yourself");
|
||||
t.closest(".ab")?.classList.add("queued");
|
||||
return act("/api/press", { id: d.press });
|
||||
const kind = C.abilities[d.press].target, mine = aim.fight === S.me.action?.start;
|
||||
const target = !mine ? null : kind === "enemy" ? aim.foe : kind === "ally" ? aim.ally : null;
|
||||
return act("/api/press", { id: d.press, ...(target ? { target } : {}) });
|
||||
}
|
||||
if (d.mode) return act("/api/manual", { id: d.mode, on: !S.me.manual.includes(d.mode) });
|
||||
if ("pull" in d) return act("/api/pull", {});
|
||||
if (d.learn) return act("/api/learn", { item: d.learn });
|
||||
if (d.eat !== undefined) return act("/api/eat", { below: Number(d.eat) });
|
||||
if (d.aim) return d.aim === S.uid ? undefined : act("/api/target", { id: d.aim });
|
||||
if (d.aim) {
|
||||
const fight = S.me.action?.start;
|
||||
if (aim.fight !== fight) aim = { fight, foe: null, ally: null };
|
||||
if (d.aim[0] === "e") aim.foe = d.aim;
|
||||
else aim.ally = d.aim;
|
||||
animateFight(serverNow()); // the outline moves now; the server hears about it in the background
|
||||
return d.aim === S.uid ? undefined : act("/api/target", { id: d.aim });
|
||||
}
|
||||
if (d.start) return act("/api/action", { id: d.start });
|
||||
if (d.once) return act("/api/action", { id: d.once, once: true });
|
||||
if ("stop" in d) return act("/api/stop", {});
|
||||
|
|
|
|||
|
|
@ -587,6 +587,16 @@ button.sheet-skill:hover { background: var(--panel-2); }
|
|||
.ab-lv .gi { width: 10px; height: 10px; }
|
||||
.ab.boosted .ab-press::after { content: "✦"; position: absolute; top: 1px; right: 3px; font-size: 10px; color: var(--gold); }
|
||||
.unique-text { color: var(--gold); margin-top: 4px; }
|
||||
/* Damage on the bars: what was just lost trails behind, the last hit or heal by the HP */
|
||||
.fight .bar.hp { position: relative; }
|
||||
.fight .bar.hp > .lag { position: absolute; inset: 0 auto 0 0; background: rgba(255, 236, 200, .55); transition: width .25s ease-out; }
|
||||
.fight .bar.hp > span:not(.lag) { position: relative; }
|
||||
.hitnum { font-weight: 700; margin-right: 6px; font-variant-numeric: tabular-nums; }
|
||||
.hitnum.bad { color: var(--bad); }
|
||||
.hitnum.good { color: var(--good); }
|
||||
.hitnum.crit { font-size: 1.15em; }
|
||||
.cast.stopped { color: var(--muted); }
|
||||
.cast.stopped s { text-decoration-thickness: 2px; }
|
||||
.eat-below { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
.eat-below .btn.on { border-color: var(--gold); box-shadow: 0 0 0 1px var(--gold); }
|
||||
.ab.locked { opacity: .35; filter: grayscale(1); }
|
||||
|
|
|
|||
|
|
@ -756,15 +756,11 @@ class AbilityTest(unittest.TestCase):
|
|||
self.assertEqual(game.ITEMS["health_potion_3"]["drink"], game.HEALTH_HEAL[2])
|
||||
self.assertNotIn("health_potion_3", game.edible({"bank": {"health_potion_3": 1}, "food": None}))
|
||||
|
||||
def test_volley_spends_arrows(self):
|
||||
def test_volley_needs_no_arrows(self):
|
||||
world, p = self.fighter("ranger", 45, weapon="maple_bow")
|
||||
p["bank"] = {"arrows": 100}
|
||||
game.start_action(world, p, "fight_goblins", T0)
|
||||
summary = game.process_player(world, p, T0 + 1800, random.Random(2))
|
||||
shot = summary["used"].get("arrows", 0)
|
||||
self.assertGreater(shot, 0)
|
||||
self.assertEqual(shot % 5, 0)
|
||||
self.assertEqual(p["bank"].get("arrows", 0), 100 - shot)
|
||||
p["bank"] = {}
|
||||
fight = game.simulate_fight(world, p, game.ACTION_BY_ID["fight_goblins"], random.Random(2))
|
||||
self.assertTrue(any(e[1] == p["id"] and e[6] == "volley" for e in fight["events"]))
|
||||
|
||||
def test_a_party_rests_as_long_as_its_slowest(self):
|
||||
world = world_with(("wil", "builder"), ("kay", "builder"))
|
||||
|
|
@ -836,6 +832,13 @@ class ActivePlayTest(unittest.TestCase):
|
|||
dead = {e[2]: e[0] for e in self.fight["events"] if e[3] == "die"}
|
||||
self.assertTrue(all(e[2] == "e1" for e in swings if e[0] < dead.get("e1", 1e9)))
|
||||
|
||||
def test_a_press_lands_on_the_enemy_it_names(self):
|
||||
game.set_manual(self.world, self.p, "concuss", True, T0)
|
||||
self.p["xp"]["combat"] = game.XP_TABLE[30]
|
||||
self.assertIsNone(game.press(self.world, self.p, "concuss", self.at(3), "e1")) # not the one you're hitting
|
||||
used = [e for e in self.fight["events"] if e[1] == "ash" and e[6] == "concuss" and e[0] >= self.at(3) - T0]
|
||||
self.assertEqual(used[0][2], "e1")
|
||||
|
||||
def test_no_presses_while_resting_or_out_of_a_fight(self):
|
||||
game.set_manual(self.world, self.p, "crushing_blow", True, T0)
|
||||
self.assertEqual(game.press(self.world, self.p, "crushing_blow", T0), "Still resting")
|
||||
|
|
@ -1028,10 +1031,11 @@ class LootTest(unittest.TestCase):
|
|||
plain = game.simulate_fight(world, p, game.ACTION_BY_ID["fight_scouts"], random.Random(4))
|
||||
world, p = self.ranger("wyrmscale_quiver")
|
||||
mods = game.fighter(p, game.NO_BLESSING, {})["mods"]
|
||||
self.assertEqual(mods["volley"], {"cost": 0, "c": 1.4})
|
||||
self.assertEqual(mods["volley"], {"c": 1.4})
|
||||
quivered = game.simulate_fight(world, p, game.ACTION_BY_ID["fight_scouts"], random.Random(4))
|
||||
self.assertGreater(game.member_of(plain, "ash")["shot"], 0)
|
||||
self.assertEqual(game.member_of(quivered, "ash")["shot"], 0) # no arrows
|
||||
aims = lambda f: [e[0] for e in f["events"] if e[1] == "ash" and e[6] == "aimed_shot" and e[3] in ("cast", "hit", "crit")]
|
||||
cast, landed = aims(quivered)[:2]
|
||||
self.assertAlmostEqual(landed - cast, 1.5, places=1) # 2s of aiming, a quarter shorter
|
||||
|
||||
def test_a_pendant_mends_a_second_ally(self):
|
||||
world = world_with(("kay", "homesteader"), ("wil", "builder"), ("bo", "miner"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue