84 lines
11 KiB
Vue
84 lines
11 KiB
Vue
<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>
|