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>
142 lines
3.9 KiB
Svelte
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>
|