87 lines
24 KiB
Vue
87 lines
24 KiB
Vue
<script setup lang="ts">
|
||
import {computed,onMounted,ref,watch} from 'vue';
|
||
import {useCharacters} from './store';
|
||
import UiIcon from './UiIcon.vue';
|
||
import CampaignsView from './CampaignsView.vue';
|
||
import type {IconName} from './icons';
|
||
const navigation: {label:string,icon:IconName}[]=[{label:'Sheet',icon:'sheet'},{label:'Actions',icon:'actions'},{label:'Spells',icon:'spells'},{label:'Gear',icon:'gear'},{label:'More',icon:'notes'}];
|
||
import {abilities,modifier,proficiency,signed,skills,skillBonus,changeHP,newCharacter,characterSchema,restPreview,applyRest,type Character} from './model';
|
||
const campaignMode=ref(false);
|
||
const store=useCharacters(),activeId=ref(''),tab=ref('Sheet'),drawer=ref(''),error=ref(''),message=ref(''),user=ref<{sub:string,name:string,expiresAt:number}|null>(null),loading=ref(true),offline=ref(!navigator.onLine);
|
||
const c=computed(()=>store.all.find(x=>x.id===activeId.value));
|
||
const draft=ref<Character|null>(null),hpAmount=ref(1),restType=ref<'short-rest'|'long-rest'>('short-rest'),rollResult=ref(''),selectedAction=ref<Character['actions'][number]|null>(null);
|
||
const drawerIcon=computed<IconName>(()=>({Character:'sheet',Account:'notes','Hit points':'hp','Add action':'actions','Add resource':'charges','Manage resources':'slots','Add spell':'spells',Roll:'actions',Dice:'initiative',Rest:restType.value==='long-rest'?'long-rest':'short-rest'} as Record<string,IconName>)[drawer.value]||'sheet');
|
||
const restChanges=computed(()=>c.value?restPreview(c.value,restType.value):[]);
|
||
const actionDraft=ref({name:'',bonus:0,damage:'',notes:''}),resourceDraft=ref({name:'',current:1,max:1,reset:'long-rest' as 'short-rest'|'long-rest'|'manual'}),spellDraft=ref({name:'',level:1,prepared:true,notes:''});
|
||
watch(c,()=>{if(c.value&&user.value)store.persist(c.value)},{deep:true});
|
||
async function authenticate(){try{const response=await fetch('/api/session',{cache:'no-store'});if(!response.ok){localStorage.removeItem('character-identity');navigator.serviceWorker.controller?.postMessage('CLEAR_AUTH');throw new Error('Please sign in with Arrowstack to open your characters.')}const identity=await response.json();if(store.owner&&store.owner!==identity.sub){await store.flush();location.reload();return}user.value=identity;error.value='';localStorage.setItem('character-identity',JSON.stringify(user.value))}catch(e){if(e instanceof TypeError){try{const cached=JSON.parse(localStorage.getItem('character-identity')||'null');if(cached?.sub&&cached.expiresAt>Date.now()){user.value=cached;offline.value=true}else throw new Error()}catch{error.value='Connect to the internet to sign in with Arrowstack.'}}else error.value=(e as Error).message}}
|
||
onMounted(async()=>{await authenticate();if(user.value){try{await store.open(user.value.sub);activeId.value=store.all[0]?.id||'';if('serviceWorker'in navigator){await navigator.serviceWorker.register('/sw.js');const registration=await navigator.serviceWorker.ready;registration.active?.postMessage('CACHE_SHELL')}}catch{error.value='Could not open local character saves. Please retry.'}}loading.value=false;window.addEventListener('offline',()=>offline.value=true);window.addEventListener('online',async()=>{offline.value=false;await authenticate();if(user.value)await store.sync()});setInterval(()=>{if(user.value&&!offline.value)void store.sync()},30000);setInterval(()=>{if(user.value&&user.value.expiresAt<=Date.now()){user.value=null;drawer.value='';error.value='Your session expired. Connect and sign in again.'}},15000)});
|
||
function create(){draft.value=newCharacter();drawer.value='Character'}
|
||
function edit(){if(c.value){draft.value=structuredClone(JSON.parse(JSON.stringify(c.value)));drawer.value='Character'}}
|
||
function saveDraft(){try{const valid=characterSchema.parse(draft.value);const index=store.all.findIndex(x=>x.id===valid.id);if(index>=0)store.all[index]=valid;else store.all.push(valid);activeId.value=valid.id;store.persist(valid);drawer.value='';error.value=''}catch{message.value='Check your values: level 1–20, abilities 1–30, HP within maximum.'}}
|
||
function hp(amount:number){if(c.value)changeHP(c.value,amount)}
|
||
function addAction(){if(c.value&&actionDraft.value.name.trim()){c.value.actions.push({...actionDraft.value,id:crypto.randomUUID()});actionDraft.value={name:'',bonus:0,damage:'',notes:''};drawer.value=''}}
|
||
function addResource(){if(c.value&&resourceDraft.value.name.trim()&&resourceDraft.value.max>=0&&resourceDraft.value.current>=0&&resourceDraft.value.current<=resourceDraft.value.max){c.value.resources.push({...resourceDraft.value,id:crypto.randomUUID()});resourceDraft.value={name:'',current:1,max:1,reset:'long-rest'};drawer.value=''}else message.value='Remaining uses must be between zero and maximum.'}
|
||
function addSpell(){if(c.value&&spellDraft.value.name.trim()){c.value.spells.push({...spellDraft.value,id:crypto.randomUUID()});spellDraft.value={name:'',level:1,prepared:true,notes:''};drawer.value=''}}
|
||
function rest(type:'short-rest'|'long-rest'){restType.value=type;drawer.value='Rest'}
|
||
function confirmRest(){if(c.value)applyRest(c.value,restType.value);drawer.value='';message.value='Rest applied. Your sheet is saved.'}
|
||
function die(sides:number){const range=4294967296-(4294967296%sides);let n:number;do{n=crypto.getRandomValues(new Uint32Array(1))[0]}while(n>=range);return n%sides+1}
|
||
function roll(bonus=0,mode=0){const a=die(20),b=mode?die(20):a,v=mode===1?Math.max(a,b):mode===-1?Math.min(a,b):a;rollResult.value=`${v+bonus} · d20 ${v} ${signed(bonus)}${mode?` (${a}, ${b})`:''}`}
|
||
function exportCharacter(){if(!c.value)return;const blob=new Blob([JSON.stringify(c.value,null,2)],{type:'application/json'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=`${c.value.name.replace(/[^a-z0-9]/gi,'-')}.json`;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000)}
|
||
async function importCharacter(event:Event){const input=event.target as HTMLInputElement;try{const file=input.files?.[0];if(!file)return;if(file.size>2_000_000)throw new Error();const valid=characterSchema.parse(JSON.parse(await file.text()));valid.id=crypto.randomUUID();store.all.push(valid);activeId.value=valid.id;store.persist(valid);message.value='Character imported as a new copy.'}catch{message.value='Could not import. Choose a valid character JSON backup under 2 MB.'}input.value=''}
|
||
async function logout(){await store.flush();localStorage.removeItem('character-identity');navigator.serviceWorker.controller?.postMessage('CLEAR_AUTH');for(const key of await caches.keys())if(key.startsWith('character-shell-'))await caches.delete(key);user.value=null;const form=document.createElement('form');form.method='POST';form.action='/auth/logout';document.body.appendChild(form);form.submit()}
|
||
async function remove(){if(c.value&&confirm(`Delete ${c.value.name} from your account and synced devices? Export a backup first.`)){await store.remove(c.value.id);activeId.value=store.all[0]?.id||''}}
|
||
function openAction(action:Character['actions'][number]){selectedAction.value=action;rollResult.value='';drawer.value='Roll'}
|
||
</script>
|
||
|
||
<template>
|
||
<div v-if="loading" class="gate"><span class="sigil">✦</span><h1>Opening your adventure…</h1></div>
|
||
<div v-else-if="!user||error" class="gate"><span class="sigil">✦</span><p class="eyebrow">ARROWSTACK · CHARACTER</p><h1>Your next adventure<br>starts here.</h1><p>{{error||'Sign in to open your character sheet.'}}</p><a class="button primary" href="/auth/login">Sign in with Arrowstack →</a></div>
|
||
<div v-else class="app-shell">
|
||
<header class="topbar"><a class="brand" href="/">✦ <span>CHARACTER<span class="brand-sub">BY ARROWSTACK</span></span></a><span class="status">{{offline?'Offline session':store.saved?store.syncStatus:'Saving…'}}</span><button class="quiet" @click="drawer='Account'" aria-label="Account and settings">{{user.name.split(' ')[0]}}</button></header>
|
||
<main>
|
||
<nav class="workspace-tabs" aria-label="Workspace"><button :class="{active:!campaignMode}" @click="campaignMode=false"><UiIcon name="sheet" />Characters</button><button :class="{active:campaignMode}" @click="campaignMode=true"><UiIcon name="notes" />Campaigns</button></nav>
|
||
<CampaignsView v-if="campaignMode" :characters="store.all" :sync-characters="store.flush" />
|
||
<div class="character-switch" v-if="!campaignMode"><select v-if="store.all.length" v-model="activeId" aria-label="Choose character"><option v-for="character in store.all" :value="character.id">{{character.name}}</option></select><span v-else>Your character collection</span><button class="quiet" @click="create">+ New character</button></div>
|
||
<p v-if="store.saveError" class="notice danger" role="alert">{{store.saveError}}</p><p v-if="message" class="notice" role="status">{{message}} <button class="quiet" @click="message=''">Dismiss</button></p>
|
||
<section v-if="!campaignMode&&!c" class="empty"><UiIcon name="sheet" class="empty-icon" /><p class="eyebrow">A BLANK PAGE. A NEW STORY.</p><h1>Who will you become?</h1><p>Create a character and keep everything you need<br>for game night in your pocket.</p><button class="primary" @click="create">Create your first character →</button><label class="button quiet">Import a character<input type="file" accept="application/json,.json" hidden @change="importCharacter"></label></section>
|
||
<template v-if="!campaignMode&&c">
|
||
<section class="hero"><div class="monogram">{{c.name.slice(0,1).toUpperCase()}}<span>✦</span></div><div><p class="eyebrow">LEVEL {{c.level}} · {{c.className||'ADVENTURER'}}</p><h1>{{c.name}}</h1><p>{{[c.species,c.background].filter(Boolean).join(' · ')||'Your story is waiting to be written.'}}</p></div><button class="edit" @click="edit">Edit character ↗</button></section>
|
||
<template v-if="tab==='Sheet'">
|
||
<div class="combat-grid"><button class="hp-card" @click="drawer='Hit points'"><div class="card-label"><span class="label-with-icon"><UiIcon name="hp" /> HIT POINTS</span><span>+ / −</span></div><div class="hp-value">{{c.hp.current}}<small>/ {{c.hp.max}}</small></div><div class="hp-track"><i :style="{width:`${100*c.hp.current/c.hp.max}%`}"></i></div><div class="hp-footer"><span>{{c.hp.temp}} temporary</span><span>Tap to damage or heal</span></div></button><div class="stat-card"><UiIcon name="ac" /><span>ARMOUR CLASS</span><strong>{{c.ac}}</strong><small>AC</small></div><div class="stat-card"><UiIcon name="initiative" /><span>INITIATIVE</span><strong>{{signed(modifier(c.abilities.dex))}}</strong><button class="quiet" @click="roll(modifier(c.abilities.dex));drawer='Dice'">Roll d20 ↗</button></div><div class="stat-card"><UiIcon name="speed" /><span>SPEED</span><strong>{{c.speed}}</strong><small>feet</small></div></div>
|
||
<div class="ability-row"><button v-for="a in abilities" @click="roll(modifier(c.abilities[a]));drawer='Dice'"><UiIcon :name="a" /><span>{{a.toUpperCase()}}</span><strong>{{signed(modifier(c.abilities[a]))}}</strong><small>{{c.abilities[a]}}</small></button></div>
|
||
<div class="two-column"><section class="panel"><div class="section-title"><h2><UiIcon name="actions" />Ready for your turn</h2><button class="quiet" @click="tab='Actions'">All actions ↗</button></div><div v-if="!c.actions.length" class="hint">Add your weapons and actions to keep them close at hand.<button class="quiet" @click="drawer='Add action'">+ Add action</button></div><button class="action-row" v-for="action in c.actions.slice(0,4)" @click="openAction(action)"><UiIcon name="actions" class="action-symbol" /><span><b>{{action.name}}</b><small>{{action.damage||'Custom action'}}</small></span><strong>{{signed(action.bonus)}}</strong></button></section><section class="panel"><div class="section-title"><h2><UiIcon name="slots" />Resources</h2><button class="quiet" @click="drawer='Add resource'">+ Add</button></div><p class="hint" v-if="!c.resources.length">Spell slots, class features and item charges — tracked your way.</p><div class="resource" v-for="r in c.resources"><UiIcon name="charges" /><div class="resource-details"><b>{{r.name}}</b><small>{{r.current}} / {{r.max}} · {{r.reset.replace('-',' ')}}</small></div><div class="counter"><button @click="r.current=Math.max(0,r.current-1)" :disabled="r.current===0" :aria-label="`Spend ${r.name}`">−</button><strong>{{r.current}}</strong><button @click="r.current=Math.min(r.max,r.current+1)" :disabled="r.current===r.max" :aria-label="`Restore ${r.name}`">+</button></div></div></section></div>
|
||
<section class="panel"><div class="section-title"><h2>Skills & saving throws</h2><span class="chip">Proficiency {{signed(proficiency(c.level))}}</span></div><div class="save-row"><button v-for="a in abilities" @click="roll(modifier(c.abilities[a])+(c.saves.includes(a)?proficiency(c.level):0));drawer='Dice'"><span>{{c.saves.includes(a)?'●':'○'}} {{a.toUpperCase()}}</span><b>{{signed(modifier(c.abilities[a])+(c.saves.includes(a)?proficiency(c.level):0))}}</b></button></div><div class="skill-grid"><button v-for="(a,name) in skills" @click="roll(skillBonus(c,String(name)));drawer='Dice'"><span>{{c.skills[name]==='expert'?'◆':c.skills[name]==='proficient'?'●':'○'}} {{name}}</span><b>{{signed(skillBonus(c,String(name)))}}</b></button></div></section>
|
||
<div class="rest-bar"><span>Take a moment to recover.</span><button @click="rest('short-rest')"><UiIcon name="short-rest" />Short rest</button><button class="primary" @click="rest('long-rest')"><UiIcon name="long-rest" />Long rest</button></div>
|
||
</template>
|
||
<section class="panel" v-if="tab==='Actions'"><div class="section-title"><h2><UiIcon name="actions" />Actions</h2><button @click="drawer='Add action'">+ Add action</button></div><p class="hint" v-if="!c.actions.length">Add attacks, features and custom actions with your own modifiers.</p><div class="list-entry" v-for="action in c.actions"><button class="action-row" @click="openAction(action)"><span><b>{{action.name}}</b><small>{{action.damage}} · {{action.notes}}</small></span><strong>{{signed(action.bonus)}}</strong></button><button class="quiet" @click="c.actions=c.actions.filter(a=>a.id!==action.id)" :aria-label="`Remove ${action.name}`">Remove</button></div></section>
|
||
<section class="panel" v-if="tab==='Spells'"><div class="section-title"><h2><UiIcon name="spells" />Spellbook</h2><button @click="drawer='Add spell'">+ Add spell</button></div><p class="hint">Record your own spells. Track spell slots with resources on the Sheet tab.</p><div class="spell-row" v-for="spell in [...c.spells].sort((a,b)=>a.level-b.level)"><label><input type="checkbox" v-model="spell.prepared"><span><b>{{spell.name}}</b><small>{{spell.level?'Level '+spell.level:'Cantrip'}} · {{spell.prepared?'Prepared':'Not prepared'}}</small><p>{{spell.notes}}</p></span></label><button class="quiet" @click="c.spells=c.spells.filter(s=>s.id!==spell.id)">Remove</button></div></section>
|
||
<section class="panel" v-if="tab==='Gear'"><div class="section-title"><h2><UiIcon name="gear" />Gear & inventory</h2><span class="chip">Saved automatically</span></div><p class="hint">Equipment, currency, attunement and anything you picked up along the way.</p><textarea v-model="c.inventory" rows="14" placeholder="Longbow, 20 arrows…" maxlength="12000"></textarea></section>
|
||
<template v-if="tab==='More'"><section class="panel"><div class="section-title"><h2><UiIcon name="notes" />Your story</h2><span class="chip">Saved automatically</span></div><textarea v-model="c.notes" rows="8" placeholder="Backstory, allies, session notes…" maxlength="12000"></textarea></section><section class="panel"><h2>Character & account</h2><p class="hint">Signed in as {{user.name}}. Characters sync to your Arrowstack account and stay available on this device for offline play. Export backups to keep an extra copy.</p><div class="settings-actions"><button @click="edit">Edit character</button><button @click="exportCharacter">Export JSON backup</button><label class="button">Import JSON<input type="file" accept="application/json,.json" hidden @change="importCharacter"></label><button @click="create">New character</button><button @click="drawer='Manage resources'">Manage resources</button><button class="danger-text" @click="remove">Delete character</button><button @click="logout" :disabled="offline">Sign out</button></div><p class="hint">Offline play lasts until your 12-hour sign-in session expires. Reconnect to sign in again. Attack modifiers, spell details and resource reset rules are entered manually; automated class rules are a future feature.</p></section></template>
|
||
</template>
|
||
<footer>✦ ARROWSTACK CHARACTER <span>v0.3 · Made for game night</span></footer>
|
||
</main>
|
||
<nav class="bottom-nav" v-if="c&&!campaignMode"><button v-for="item in navigation" :key="item.label" :class="{active:tab===item.label}" :aria-current="tab===item.label?'page':undefined" @click="tab=item.label"><UiIcon :name="item.icon" />{{item.label}}</button></nav>
|
||
<div v-if="drawer" class="overlay" @click.self="drawer=''"><section class="drawer" :class="{'character-drawer':drawer==='Character'}" role="dialog" aria-modal="true" :aria-label="drawer"><div class="section-title drawer-heading"><h2><UiIcon :name="drawerIcon" />{{drawer==='Rest'?(restType==='long-rest'?'Long rest':'Short rest'):drawer}}</h2><button class="close" @click="drawer=''" aria-label="Close dialog">×</button></div>
|
||
<template v-if="drawer==='Account'"><p class="hint">Signed in as {{user.name}}. Characters sync to your account and remain available on this device for offline play.</p><button class="primary wide" @click="logout" :disabled="offline">Sign out of Character</button><p class="hint" v-if="offline">Reconnect to sign out.</p></template>
|
||
<form v-if="drawer==='Character'&&draft" class="character-form" @submit.prevent="saveDraft">
|
||
<div class="editor-intro"><UiIcon name="sheet" /><div><p class="eyebrow">THE NEXT CHAPTER IS YOURS</p><h3>Bring your hero to life.</h3><p>A name, a calling, a story worth telling.</p></div></div>
|
||
<fieldset class="folio-section"><legend><UiIcon name="notes" />Identity</legend><label>Name<input v-model="draft.name" required maxlength="120" autocomplete="off"></label><div class="form-grid"><label>Class<input v-model="draft.className" maxlength="120" placeholder="Your calling"></label><label>Level<input type="number" v-model.number="draft.level" min="1" max="20" required></label><label>Species<input v-model="draft.species" maxlength="120" placeholder="Your ancestry"></label><label>Background<input v-model="draft.background" maxlength="120" placeholder="Your beginnings"></label></div></fieldset>
|
||
<fieldset class="folio-section"><legend><UiIcon name="str" />Ability scores</legend><div class="form-grid three ability-fields"><label v-for="a in abilities" :key="a"><span class="field-caption"><UiIcon :name="a" />{{a.toUpperCase()}}</span><input type="number" v-model.number="draft.abilities[a]" min="1" max="30" required></label></div></fieldset>
|
||
<fieldset class="folio-section"><legend><UiIcon name="hp" />Vital statistics</legend><div class="form-grid three"><label>Max HP<input type="number" v-model.number="draft.hp.max" min="1" max="9999" required></label><label>Current HP<input type="number" v-model.number="draft.hp.current" min="0" :max="draft.hp.max" required></label><label>Temp HP<input type="number" v-model.number="draft.hp.temp" min="0" max="9999" required></label></div><div class="form-grid"><label><span class="field-caption"><UiIcon name="ac" />Armour class</span><input type="number" v-model.number="draft.ac" min="0" max="9999" required></label><label><span class="field-caption"><UiIcon name="speed" />Speed (ft)</span><input type="number" v-model.number="draft.speed" min="0" max="9999" required></label></div></fieldset>
|
||
<fieldset class="folio-section"><legend><UiIcon name="ac" />Saving throws</legend><div class="check-row"><label v-for="a in abilities" :key="a"><input type="checkbox" :value="a" v-model="draft.saves">{{a.toUpperCase()}}</label></div></fieldset>
|
||
<fieldset class="folio-section"><legend><UiIcon name="wis" />Skills & talents</legend><div class="form-grid"><label v-for="(a,name) in skills" :key="name">{{name}}<select v-model="draft.skills[name]"><option value="none">None</option><option value="proficient">Proficient</option><option value="expert">Expertise</option></select></label></div></fieldset>
|
||
<div class="folio-submit"><button class="primary wide"><UiIcon name="sheet" />Save character<span aria-hidden="true">✦</span></button></div>
|
||
</form>
|
||
<template v-if="drawer==='Hit points'&&c"><p class="big-number">{{c.hp.current}} <small>/ {{c.hp.max}}</small></p><div class="quick-hp"><button v-for="n in [-10,-5,-1,1,5,10]" @click="hp(n)">{{signed(n)}}</button></div><label>Amount<input type="number" v-model.number="hpAmount" min="1" max="9999"></label><div class="form-grid"><button class="danger-text" @click="hp(-Math.max(0,Math.trunc(hpAmount||0)))">Take damage</button><button class="primary" @click="hp(Math.max(0,Math.trunc(hpAmount||0)))">Heal</button></div><label>Temporary HP<input type="number" :value="c.hp.temp" @change="c.hp.temp=Math.max(0,Math.min(9999,Math.trunc(Number(($event.target as HTMLInputElement).value)||0)))" min="0" max="9999"></label><p class="hint">Damage uses temporary HP first. Healing cannot exceed your maximum.</p></template>
|
||
<form v-if="drawer==='Add action'" @submit.prevent="addAction"><label>Name<input v-model="actionDraft.name" required maxlength="120"></label><div class="form-grid"><label>Attack bonus<input type="number" v-model.number="actionDraft.bonus" min="-50" max="100" required></label><label>Damage<input v-model="actionDraft.damage" placeholder="1d8 + 4 piercing" maxlength="120"></label></div><label>Notes<textarea v-model="actionDraft.notes" maxlength="12000"></textarea></label><button class="primary wide">Add action</button></form>
|
||
<form v-if="drawer==='Add resource'" @submit.prevent="addResource"><label>Name<input v-model="resourceDraft.name" placeholder="Level 1 spell slots" required maxlength="120"></label><div class="form-grid"><label>Remaining<input type="number" v-model.number="resourceDraft.current" min="0" :max="resourceDraft.max" required></label><label>Maximum<input type="number" v-model.number="resourceDraft.max" min="0" max="9999" required></label></div><label>Recovery<select v-model="resourceDraft.reset"><option value="short-rest">Short or long rest</option><option value="long-rest">Long rest</option><option value="manual">Manual only</option></select></label><button class="primary wide">Add resource</button></form>
|
||
<template v-if="drawer==='Manage resources'&&c"><div class="resource" v-for="r in c.resources"><b>{{r.name}}</b><button class="quiet danger-text" @click="c.resources=c.resources.filter(x=>x.id!==r.id)">Remove</button></div><button @click="drawer='Add resource'">+ Add resource</button></template>
|
||
<form v-if="drawer==='Add spell'" @submit.prevent="addSpell"><label>Name<input v-model="spellDraft.name" required maxlength="120"></label><label>Level (0 = cantrip)<input type="number" v-model.number="spellDraft.level" min="0" max="9" required></label><label>Spell notes<textarea v-model="spellDraft.notes" rows="6" maxlength="12000" placeholder="Casting time, range, components, duration, effects…"></textarea></label><button class="primary wide">Add spell</button></form>
|
||
<template v-if="drawer==='Rest'&&c"><p class="hint">Review before applying your rest.</p><div class="preview-row" v-if="restType==='long-rest'"><span>Hit points</span><b>{{c.hp.current}} → {{c.hp.max}}</b></div><div class="preview-row" v-if="restType==='long-rest'"><span>Temporary HP</span><b>{{c.hp.temp}} → 0</b></div><div class="preview-row" v-for="r in restChanges"><span>{{r.name}}</span><b>{{r.current}} → {{r.max}}</b></div><p class="hint">Manual resources, notes and gear remain as entered. Hit dice and class-specific rest rules are not automated yet.</p><button class="primary wide" @click="confirmRest">Confirm rest</button></template>
|
||
<template v-if="drawer==='Roll'&&selectedAction"><h3>{{selectedAction.name}}</h3><p class="hint">Attack {{signed(selectedAction.bonus)}} · {{selectedAction.damage}}</p><p>{{selectedAction.notes}}</p><div class="roll-buttons"><button @click="roll(selectedAction.bonus,-1)">Disadvantage</button><button class="primary" @click="roll(selectedAction.bonus)">Roll d20</button><button @click="roll(selectedAction.bonus,1)">Advantage</button></div><p class="roll-result" role="status">{{rollResult}}</p></template>
|
||
<template v-if="drawer==='Dice'"><p class="roll-result" role="status">{{rollResult}}</p><p class="hint">Includes the modifier shown on your sheet.</p></template>
|
||
</section></div>
|
||
</div>
|
||
</template>
|