Files
waving-hands/src/lib/components/WizardStatus.svelte
T
Eric WagonerandClaude Fable 5.1 865555e911 Waving Hands: a browser duel after Bartle's 1977 game
SvelteKit 5 single-player version against a heuristic bot. The engine
resolves all forty spells simultaneously as the rules describe, the
ledger of gestures is the interface, and the duel is saved in the
browser. Tester feedback rounds added per-hand planning, threat
evidence, a result strip with one-time rule explanations, a phone
layout and a structured rules page. Deploys as a static site behind
Caddy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 15:33:29 -04:00

142 lines
3.9 KiB
Svelte

<script lang="ts">
import { SPELL_BY_ID } from '$lib/game/spells';
import { MAX_HP, type Monster, type WizardState } from '$lib/game/state';
let { wizard, monsters, you = false }: { wizard: WizardState; monsters: Monster[]; you?: boolean } = $props();
const more = (n: number) => (n === Infinity ? 'for good' : `${n} more`);
const effects = $derived.by(() => {
const out: string[] = [];
if (wizard.resistHeat) out.push('resists heat');
if (wizard.resistCold) out.push('resists cold');
if (wizard.protectionTurns > 0) out.push(`protected from evil, ${more(wizard.protectionTurns)}`);
if (wizard.blindTurns > 0) out.push(`blind, ${more(wizard.blindTurns)}`);
if (wizard.invisibleTurns > 0) out.push(`invisible, ${more(wizard.invisibleTurns)}`);
if (wizard.hasteTurns > 0) out.push(`hastened, ${more(wizard.hasteTurns)}`);
if (wizard.banked) out.push(`holding a banked ${SPELL_BY_ID[wizard.banked.spellId].name.toLowerCase()}`);
else if (wizard.delayedTurns > 0) out.push(`next spell will be banked, ${more(wizard.delayedTurns)}`);
if (wizard.permanencyTurns > 0) out.push(`next enchantment will be permanent, ${more(wizard.permanencyTurns)}`);
if (wizard.diseaseTurns !== null) out.push(`diseased, dies in ${wizard.diseaseTurns}`);
if (wizard.poisonTurns !== null) out.push(`poisoned, dies in ${wizard.poisonTurns}`);
const c = wizard.constraints;
if (c.amnesia) out.push(wizard.permanent.amnesia ? 'amnesia for good' : 'amnesia this turn');
if (c.confusion) out.push(wizard.permanent.confusion ? 'confused for good' : 'confused this turn');
if (c.fear) out.push(wizard.permanent.fear ? 'afraid for good' : 'afraid this turn');
if (c.paralysedHand) out.push(`${c.paralysedHand} hand paralysed${wizard.permanent.paralysedHand ? ' for good' : ''}`);
if (c.charmed) out.push(`${c.charmed.hand} hand charmed${wizard.permanent.charmed ? ' for good' : ''}`);
if (wizard.usedQuickLightning) out.push('quick lightning spent');
return out;
});
</script>
<section class="wizard" class:you class:dead={!wizard.alive}>
<h2>{wizard.name}{#if you}<span class="tag"> (you)</span>{/if}</h2>
<div class="health">
<div class="pips" role="img" aria-label="{wizard.hp} of {MAX_HP} hit points">
{#each { length: MAX_HP } as _, i (i)}
<span class="pip" class:lost={i >= wizard.hp}></span>
{/each}
</div>
<span class="hp">{wizard.hp} / {MAX_HP}</span>
</div>
{#if effects.length}
<p class="effects">{effects.join('; ')}.</p>
{:else}
<p class="effects muted">Unenchanted.</p>
{/if}
{#if monsters.length}
<ul class="monsters">
{#each monsters as m (m.id)}
<li>
<span class="mname">{m.name}</span>
<span class="mhp">{m.hp}/{m.maxHp}, hits for {m.attack}</span>
{#if m.constraints.skipAttack}<span class="muted">paralysed</span>{/if}
</li>
{/each}
</ul>
{/if}
</section>
<style>
.wizard {
padding: 0.75rem 0;
border-bottom: 1px solid var(--rule);
}
.wizard.dead h2 {
text-decoration: line-through;
color: var(--bone-dim);
}
h2 {
font-size: 1.3rem;
font-style: italic;
}
.tag {
margin-left: 0.35em;
font-size: 0.9rem;
color: var(--bone-dim);
font-style: normal;
}
.health {
display: flex;
align-items: center;
gap: 0.6rem;
margin: 0.4rem 0 0.35rem;
}
.pips {
display: flex;
gap: 3px;
flex: 1;
}
.hp {
font-variant-numeric: tabular-nums;
color: var(--bone-dim);
font-size: 0.9rem;
white-space: nowrap;
}
.pip {
width: 100%;
max-width: 16px;
height: 10px;
background: var(--bone);
border-radius: 1px;
}
.pip.lost {
background: transparent;
box-shadow: inset 0 0 0 1px var(--bone-faint);
}
.effects {
font-size: 0.9rem;
}
.monsters {
list-style: none;
margin: 0.4rem 0 0;
padding: 0;
font-size: 0.9rem;
}
.monsters li {
display: flex;
gap: 0.6rem;
align-items: baseline;
}
.mname {
color: var(--ember);
}
.mhp {
color: var(--bone-dim);
}
</style>