character sheet and dice roller for abenteuerspiel
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
abenteuerspiel/index.html

526 lines
13 KiB

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>abenteuerspiel character keeper</title>
<style type="text/css" media="screen">
:root {
--header-color: green;
--link-color: rgb(244,80,83);
}
body {
max-width: 600px;
margin: 0 auto;
padding: 1rem;
background-color: rgb(254,247,223);
}
main {
display: flex;
gap: 1em;
}
@media only screen and (max-width: 600px) {
main {
flex-direction: column;
}
}
details {
display: flex;
flex-direction: column;
flex-basis: 50%;
}
label {
text-transform: capitalize;
margin-top: 1em;
}
summary {
font-size: 2rem;
margin: 1rem 0;
color: var(--header-color);
}
summary.small {
font-size: 1.6rem;
}
.gather {
display: flex;
flex-direction: column;
}
.gather label {
display: flex;
justify-content: space-between;
}
button {
width: 100%;
padding: 1rem;
margin: 1rem 0;
}
h1, h2, h3 {
color: var(--header-color);
}
a {
color: var(--link-color);
}
abbr[title] {
position: relative;
}
abbr[title]:hover::after,
abbr[title]:focus::after {
cursor: help;
content: attr(title);
position: absolute;
left: 0;
bottom: -30px;
max-width: 60ch;
border-radius: 3px;
box-shadow: 1px 1px 5px 0 rgba(0,0,0,0.4);
padding: 3px 5px;
background: wheat;
}
footer {
margin-top: 2rem;
}
</style>
</head>
<body>
<header>
<h1>Abenteuerspiel!</h1>
</header>
<main>
<details open>
<summary>
Character
</summary>
<div class="characterOptions">
<a href="#" class="generateCharacter">Generate character</a>
</div>
<label for="name">name</label>
<input type="text" name="name" id="name" value="" />
<label for="conditions">conditions</label>
<textarea name="conditions" rows="8" cols="40"></textarea>
<label for="skills">skills</label>
<textarea name="skills" rows="8" cols="40"></textarea>
<a href="#" class="generateSkills">Generate skills</a>
<label for="equipment">equipment</label>
<textarea name="equipment" rows="8" cols="40"></textarea>
<a href="#" class="generateEquipment">Generate equipment</a>
<label for="rituals">rituals</label>
<textarea name="rituals" rows="8" cols="40"></textarea>
<div>
<a href="#" class="generateRitual">Generate ritual</a>
<label for="chaoticRitual">enable chaos?</label>
<input type="checkbox" name="chaoticRitual" id="chaoticRitual">
<sup><abbr tabindex="0" title="Pick a random verb-noun combination from the list of available rituals.">?</abbr></sup>
</div>
</details>
<details open>
<summary>Risky Action</summary>
<div class="actionOptions">
<a href="#" class="rest">🛌 Rest</a> |
<a href="#" class="tempt"> Tempt Fate</a> |
<a href="#" class="orakel">🔮 Orakel</a>
</div>
<div class="pool">
<p>Ready Pool: <span class="readyPool"><span></p>
<p>Exhausted Pool: <span class="exhaustedPool"><span></p>
</div>
<details open>
<summary class="small">Gather your dice</summary>
<div class="gather">
<label for="devil"><span>devil's bargain<sup><abbr tabindex="0" title="Gain an extra die in exchange for some kind of hardship that happens regardless of the outcome.">?</abbr></sup></span>
<input type="checkbox" name="devil" id="devil">
</label>
<label for="stressed">stressed
<input class="gatherable" type="checkbox" name="stressed" id="stressed" checked disabled>
</label>
<label for="unconditioned">unconditioned
<input class="gatherable selectable" type="checkbox" name="unconditioned" id="unconditioned">
</label>
<label for="skilled">skilled
<input class="gatherable selectable" type="checkbox" name="skilled" id="skilled">
</label>
<label for="equipped">equipped
<input class="gatherable selectable" type="checkbox" name="equipped" id="equipped">
</label>
<label for="supported">supported
<input class="gatherable selectable" type="checkbox" name="supported" id="supported">
</label>
<label for="advantaged">advantaged
<input class="gatherable selectable" type="checkbox" name="advantaged" id="advantaged">
</label>
</div>
<button class="roll">Roll</button>
</details>
<div class="outcome">
<h3>Outcome</h3>
<p class="outcomeOutput">...</p>
</div>
</details>
</main>
<footer><small><a href="https://terriblybeautiful.itch.io/abenteuerspiel">Abenteuerspiel! is by Terribly Beautiful</a></small></footer>
</body>
</html>
<script charset="utf-8">
const random = (max) => Math.floor(Math.random() * max);
const rituals = [
[
"become",
"breathe",
"charm",
"compel",
"conjure",
"control",
"create",
"detect",
"duplicate",
"exploit",
"follow",
"generate",
"hasten",
"heal",
"hold",
"induce",
"invoke",
"levitate",
"make",
"manipulate",
"neutralize",
"observe",
"open",
"parse",
"produce",
"protect",
"read",
"reverse",
"shoot",
"slow",
"spawn",
"summon",
"superior",
"swap",
"transform",
"traverse",
],
[
"beauty",
"water",
"creature",
"animal",
"wind",
"plants",
"dark",
"magic",
"self",
"flaw",
"thread",
"illusion",
"time",
"entity",
"person",
"sleep",
"light",
"thing",
"invisible",
"earth",
"ritual",
"location",
"lock",
"symbol",
"web",
"area",
"mind",
"gravity",
"lightning",
"movement",
"fire",
"spirit",
"senses",
"bodies",
"beast",
"wall"
]
]
const equipment = [
'animal traps(3)',
'bandages (3)',
'bone dust (3)',
'bow & arrows (12)',
'caltrops (9)',
'chalk (6)',
'collapsible pole',
'crowbar',
'dagger',
'dice (6)',
'fire oil (3)',
'flint & steel (3)',
'fools gold (6)',
'grappling hook',
'holy symbol',
'iron spikes (6)',
'lantern & oil (3)',
'light armor',
'lock-picks (6)',
'marbles (12)',
'mule',
'pen & parchment',
'pot of honey (3)',
'quicksilver',
'ritual incense (3)',
'rope (60 ft)',
'shield',
'skeleton key (1)',
'sling & stones (12)',
'spyglass',
'steel mirror',
'sword',
'tent',
'torches (12)',
'travel rations (3)',
'twine (300 ft)',
]
const skills = [
'alchemy',
'artifacts',
'athletics',
'bartering',
'beasts',
'craft',
'deception',
'destruction',
'dexterity',
'forensics',
'improvisation',
'intimidation',
'lairs',
'mimicry',
'myths',
'obfuscation',
'performance',
'persistance',
'plants',
'protection',
'rituals',
'secrets',
'security',
'speed',
'spontaneity',
'stealth',
'strength',
'surgery',
'surprise',
'symbols',
'tactics',
'tracking',
'traps',
'trickery',
'vigilance',
'weapons',
]
const generateRitual = (evt) => {
evt?.preventDefault();
const isRitualChaos = () => document.querySelector('input#chaoticRitual').checked
const out = document.querySelector('textarea[name=rituals]')
out.value = ''
const result = Array.from({length: 1}).map(ritual => {
const i = random(rituals[0].length)
const j = (isRitualChaos()) ? random(rituals[0].length) : i
return `${rituals[0][i]} ${rituals[1][j]}`
}).forEach(ritual => {
out.value += ritual + '\n'
})
}
document.querySelector('.generateRitual').addEventListener('click', generateRitual)
const generateEquipment = (evt) => {
evt?.preventDefault();
const out = document.querySelector('textarea[name=equipment]')
out.value = ''
const result = Array.from({length: 3}).map(eq => {
const i = random(equipment.length)
return `${equipment[i]}`
}).forEach(eq => {
out.value += eq + '\n'
})
}
document.querySelector('.generateEquipment').addEventListener('click', generateEquipment)
const generateSkills = (evt) => {
evt?.preventDefault();
const out = document.querySelector('textarea[name=skills]')
out.value = ''
const result = Array.from({length: 2}).map(skill => {
const i = random(skills.length)
return `${skills[i]}`
}).forEach(skill => {
out.value += skill + '\n'
})
}
document.querySelector('.generateSkills').addEventListener('click', generateSkills)
const generateCharacter = (evt) => {
evt.preventDefault()
generateRitual()
generateSkills()
generateEquipment()
}
document.querySelector('.generateCharacter').addEventListener('click', generateCharacter)
let dice = {
'ready': 6,
'exhausted': 0,
}
const showDice = () => {
const readyOut = document.querySelector('.readyPool')
const exhaustedOut = document.querySelector('.exhaustedPool')
readyOut.innerText = ''
exhaustedOut.innerText = ''
for(let i = 0; i < dice.ready; i++) {
readyOut.innerText += '🎲'
}
for(let i = 0; i < dice.exhausted; i++) {
exhaustedOut.innerText += '🎲'
}
}
showDice()
const rollDice = (evt) => {
const pool = Array.from({length: poolSize()}).map(x => random(6) + 1)
const high = Math.max(...pool)
const stress = pool[0]
const isStressful = handleStress({
ready: dice.ready,
stress: stress,
})
handleRiskOutput({
high,
pool,
stress: isStressful,
})
// clear gatherable selectables
Array.from(document.querySelectorAll('.gather .selectable'))
.forEach(input => {
input.checked = false;
input.disabled = false;
})
// ... and the devils bargin
document.querySelector('input#devil').checked = false
}
const poolSize = () => Array.from(document.querySelectorAll('.gather input'))
.filter(el => el.checked)
.length
const handleStress = ({ ready, stress }) => {
const isStressful = stress < ready;
if(isStressful) {
dice.ready -= 1;
dice.exhausted += 1;
}
showDice()
return isStressful;
}
const handleRiskOutput = ({ high, pool, stress }) => {
const out = document.querySelector('.outcomeOutput')
out.innerText = ''
out.innerText += 'Roll: '
out.innerText += ' ' + pool.join(', ')
out.innerText += '\nHighest: '
out.innerText += ' ' + high + ' = '
out.innerText += (pool.filter(d => d === 6).length > 1)
? ` Wow, that's a CRITICAL SUCCESS!!`
: (high < 4)
? ' Bad. Things get worse. Take a condition.'
: (high < 6)
? ' Mixed. Partial success, or success with complication'
: ' Success!'
if(stress) {
out.innerText += '\nYou exhaust one of your Ready Dice.'
out.innerHTML += '<sup><abbr tabindex="0" title="the first die in your pool is always your stress die. If its value is less than your number of ready dice, one of your ready dice is exhausted.">?</abbr></sup>'
}
}
document.querySelector('button.roll').addEventListener('click', rollDice);
const checkPoolSize = (evt) => {
const gathered = Array.from(document.querySelectorAll('.gather .gatherable'))
.filter(item => item.checked)
.length
Array.from(document.querySelectorAll('.selectable:not(:checked)'))
.forEach(box => {
box.disabled = (gathered === dice.ready)
})
}
Array.from(document.querySelectorAll('.gather .selectable'))
.forEach(input => {
input.addEventListener('change', checkPoolSize)
})
const rest = (evt) => {
evt.preventDefault()
let recovered = 0;
Array.from({length: dice.exhausted})
.map(exhausted => random(6) + 1)
.forEach(exhausted => {
if (exhausted >= 4) {
dice.ready += 1;
dice.exhausted -= 1;
recovered += 1
}
})
handleRestOutput(recovered)
showDice()
}
handleRestOutput = (recovered) => {
const out = document.querySelector('.outcomeOutput')
out.innerText = ''
out.innerText += `You rested and recovered ${recovered} dice`
}
document.querySelector('.rest').addEventListener('click', rest);
const tempt = (evt) => {
const fate = random(6) + 1
const out = document.querySelector('.outcomeOutput')
out.innerText = `${fate}: `
out.innerText += `You have tempted fate, and made things incrementally ${fate < 4 ? 'worse' : 'better'}.`
if (fate < dice.ready) {
out.innerText += `You also suffer stress.`
}
handleStress({
ready: dice.ready,
stress: fate,
})
}
document.querySelector('.tempt').addEventListener('click', tempt);
const orakel = (evt) => {
const fortune = random(6) + 1
const out = document.querySelector('.outcomeOutput')
out.innerText = ''
out.innerText += `${fortune < 4 ? '🚫' : '✅'} ${fortune}: The orakel says, signs point to ${fortune < 4 ? 'no' : 'yes'}.`
}
document.querySelector('.orakel').addEventListener('click', orakel);
</script>