dnd-character-deploy/src/CompanionsFolio.vue

84 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import {computed,ref,watch} from 'vue';
import UiIcon from './UiIcon.vue';
import {abilities,modifier,signed,companionSchema,createCompanion,addCompanion,stableCompanion,retrieveCompanion,changeCompanionHP,type Character,type Companion,type StableLocation} from './model';
const props=defineProps<{character:Character}>();
const animals=computed(()=>props.character.companions||[]);
const editing=ref(false),editingId=ref<string|null>(null),draft=ref<Companion>(createCompanion()),preset=ref<'riding-horse'|'warhorse'|'pony'|'mastiff'|'custom'>('custom');
const stabling=ref(false),stableId=ref<string|null>(null),stableDraft=ref<StableLocation>({name:'',settlement:'',date:today()});
const error=ref(''),stableError=ref('');
const kinds:Companion['kind'][]=['horse','mount','pet'];
function today(){const now=new Date();return new Date(now.getTime()-now.getTimezoneOffset()*60000).toISOString().slice(0,10)}
function label(kind:Companion['kind']){return kind==='horse'?'Horse':kind==='mount'?'Mount':'Pet'}
function showError(e:unknown){return e instanceof Error&&'issues' in e?'Please check the name and statistics. Current HP cannot exceed maximum HP.':e instanceof Error?e.message:'Please check the companion details.'}
function add(kind:Companion['kind']){
preset.value=kind==='horse'?'riding-horse':kind==='pet'?'mastiff':'pony';
draft.value=createCompanion(kind,preset.value);editingId.value=null;error.value='';editing.value=true;
}
function edit(animal:Companion){draft.value=structuredClone(JSON.parse(JSON.stringify(animal)));editingId.value=animal.id;preset.value='custom';error.value='';editing.value=true}
function applyPreset(){
const example=createCompanion(draft.value.kind,preset.value);
draft.value={...draft.value,species:example.species,ac:example.ac,hp:example.hp,speed:example.speed,abilities:example.abilities,carryingCapacity:example.carryingCapacity};
}
function save(){
try{
const value=companionSchema.parse(draft.value);
if(editingId.value){
if(!animals.value.some(a=>a.id===editingId.value))throw new Error('This companion was changed. Reopen the editor.');
props.character.companions=animals.value.map(a=>a.id===editingId.value?value:a);
}else addCompanion(props.character,value);
editing.value=false;error.value='';
}catch(e){error.value=showError(e)}
}
function remove(animal:Companion){if(confirm('Remove '+animal.name+' from this character?'))props.character.companions=animals.value.filter(a=>a.id!==animal.id)}
function stableEditor(animal:Companion){
stableId.value=animal.id;stableDraft.value=animal.lastStabled?{...animal.lastStabled}:{name:'',settlement:'',date:today()};
stableError.value='';stabling.value=true;
}
function saveStable(){
const animal=animals.value.find(a=>a.id===stableId.value);
if(!animal){stableError.value='Companion not found. Reopen the stable record.';return}
try{stableCompanion(animal,stableDraft.value);stableError.value='';stabling.value=false}catch(e){stableError.value=showError(e)}
}
function collect(animal:Companion){retrieveCompanion(animal)}
function hp(animal:Companion,amount:number){changeCompanionHP(animal,amount)}
watch(()=>props.character.id,()=>{editing.value=false;stabling.value=false;error.value='';stableError.value=''});
</script>
<template>
<section class="actions-folio companions-folio" aria-labelledby="companions-heading">
<div class="folio-heading"><h2 id="companions-heading"><UiIcon name="speed" />Mounts & pets</h2><div class="folio-heading-tools"><button class="quiet folio-add" @click="add('horse')">+ Horse</button><button class="quiet folio-add" @click="add('mount')">+ Mount</button><button class="quiet folio-add" @click="add('pet')">+ Pet</button></div></div>
<p class="folio-spell-hint">Keep their stats, whereabouts and the last place you stabled them. These records stay with your character, even offline.</p>
<div v-if="!animals.length" class="folio-empty"><UiIcon name="speed" /><h3>Every adventurer needs a companion.</h3><p>Whether it's a trusty steed, a stubborn mule or a curious familiar, give them a place in your story.</p><button class="primary" @click="add('horse')">Add your first companion</button></div>
<article v-for="animal in animals" :key="animal.id" class="companion-entry">
<div class="companion-heading"><span class="companion-emblem" aria-hidden="true">{{animal.kind==='horse'?'♞':animal.kind==='mount'?'✦':'♡'}}</span><div class="companion-intro"><p class="eyebrow">{{label(animal.kind)}} · {{animal.species||'Custom companion'}}</p><h3>{{animal.name}}</h3></div><span class="companion-status" :class="animal.status">{{animal.status==='travelling'?'With party':animal.status==='stabled'?'Stabled':'Elsewhere'}}</span></div>
<div class="companion-location"><div><small>Current whereabouts</small><strong>{{animal.currentLocation||'Not recorded'}}</strong></div><div><small>Last stabled</small><strong v-if="animal.lastStabled">{{animal.lastStabled.name}}{{animal.lastStabled.settlement?', '+animal.lastStabled.settlement:''}}</strong><strong v-else>Not yet recorded</strong><span v-if="animal.lastStabled?.date">{{animal.lastStabled.date}}</span></div></div>
<div class="companion-stats"><div><small>AC</small><strong>{{animal.ac}}</strong></div><div><small>Speed</small><strong>{{animal.speed}} <em>ft</em></strong></div><div><small>Carry</small><strong>{{animal.carryingCapacity||'—'}} <em v-if="animal.carryingCapacity">lb</em></strong></div><div class="companion-hp"><small>Hit points</small><div><button class="quiet" @click="hp(animal,-1)" :disabled="animal.hp.current===0" :aria-label="'Damage '+animal.name+' by one HP'">−</button><strong>{{animal.hp.current}} / {{animal.hp.max}}</strong><button class="quiet" @click="hp(animal,1)" :disabled="animal.hp.current===animal.hp.max" :aria-label="'Heal '+animal.name+' by one HP'">+</button></div></div></div>
<details class="folio-options companion-details"><summary>Abilities, traits & notes</summary><div class="companion-abilities"><div v-for="a in abilities" :key="a"><small>{{a.toUpperCase()}}</small><strong>{{animal.abilities[a]}}</strong><span>{{signed(modifier(animal.abilities[a]))}}</span></div></div><p v-if="animal.traits" class="folio-action-notes">{{animal.traits}}</p><p v-if="animal.notes" class="folio-action-notes">{{animal.notes}}</p><p v-if="!animal.traits&&!animal.notes" class="hint">Add attacks, abilities, tack, bonds or feeding notes in the editor.</p></details>
<div class="companion-actions"><button class="quiet" @click="stableEditor(animal)">Record stable</button><button v-if="animal.status==='stabled'" class="quiet" @click="collect(animal)">Collect animal</button><button class="quiet" @click="edit(animal)">Edit stats</button><button class="quiet danger-text" @click="remove(animal)">Remove</button></div>
</article>
</section>
<div v-if="editing" class="overlay" @click.self="editing=false"><section class="drawer companion-drawer" role="dialog" aria-modal="true" :aria-label="editingId?'Edit companion':'Add companion'"><div class="section-title drawer-heading"><h2><UiIcon name="speed" />{{editingId?'Edit companion':'New companion'}}</h2><button class="close" @click="editing=false" aria-label="Close companion editor">×</button></div>
<p v-if="error" class="notice danger" role="alert">{{error}}</p>
<form @submit.prevent="save">
<div class="form-grid"><label>Name<input v-model="draft.name" maxlength="120" required placeholder="Biscuit"></label><label>Kind<select v-model="draft.kind"><option v-for="kind in kinds" :key="kind" :value="kind">{{label(kind)}}</option></select></label></div>
<label>Starting stat block<select v-model="preset" @change="applyPreset"><option value="custom">Custom (leave current stats)</option><option value="riding-horse">Riding horse</option><option value="warhorse">Warhorse</option><option value="pony">Pony</option><option value="mastiff">Mastiff</option></select></label>
<p class="hint">Starter stats are editable. Your DM can adjust them for your game.</p>
<label>Creature / species<input v-model="draft.species" maxlength="120" placeholder="Riding horse"></label>
<div class="form-grid companion-vitals"><label>Armour class<input type="number" v-model.number="draft.ac" min="0" max="50" required></label><label>Current HP<input type="number" v-model.number="draft.hp.current" min="0" :max="draft.hp.max" required></label><label>Max HP<input type="number" v-model.number="draft.hp.max" min="1" max="9999" required></label><label>Speed (ft)<input type="number" v-model.number="draft.speed" min="0" max="500" required></label><label>Carry (lb)<input type="number" v-model.number="draft.carryingCapacity" min="0" max="99999" required></label></div>
<fieldset class="folio-section"><legend>Ability scores</legend><div class="form-grid three"><label v-for="a in abilities" :key="a">{{a.toUpperCase()}}<input type="number" v-model.number="draft.abilities[a]" min="1" max="30" required></label></div></fieldset>
<div class="form-grid"><label>Current status<select v-model="draft.status"><option value="travelling">Travelling</option><option value="stabled">Stabled</option><option value="elsewhere">Elsewhere</option></select></label><label>Current whereabouts<input v-model="draft.currentLocation" maxlength="200" placeholder="With the party"></label></div>
<p class="hint">Use “Record stable” on the companion card to save the stable's name, town and date.</p>
<label>Traits & actions<textarea v-model="draft.traits" rows="3" maxlength="12000" placeholder="Special abilities, attacks, senses…"></textarea></label>
<label>Care & story notes<textarea v-model="draft.notes" rows="3" maxlength="12000" placeholder="Temperament, tack, supplies, bonds…"></textarea></label>
<button class="primary wide">{{editingId?'Save companion':'Add companion'}}</button>
</form></section></div>
<div v-if="stabling" class="overlay" @click.self="stabling=false"><section class="drawer companion-drawer" role="dialog" aria-modal="true" aria-label="Record last stable"><div class="section-title drawer-heading"><h2><UiIcon name="speed" />Record stable</h2><button class="close" @click="stabling=false" aria-label="Close stable editor">×</button></div>
<p class="hint">Where did you leave this companion? Their last stable remains recorded after you collect them.</p><p v-if="stableError" class="notice danger" role="alert">{{stableError}}</p>
<form @submit.prevent="saveStable"><label>Stable or keeper<input v-model="stableDraft.name" maxlength="120" required placeholder="The Copper Horseshoe"></label><label>Town / region<input v-model="stableDraft.settlement" maxlength="120" placeholder="Neverwinter"></label><label>Date stabled<input type="date" v-model="stableDraft.date" required></label><button class="primary wide">Save stable & mark as stabled</button></form>
</section></div>
</template>