525 lines
12 KiB
HTML
525 lines
12 KiB
HTML
<!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 += (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>
|