87 lines
24 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,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>