feat: Add template spell picker from spells.yml; polish spell table
All checks were successful
Deploy / deploy (push) Successful in 1m20s
All checks were successful
Deploy / deploy (push) Successful in 1m20s
- Add yaml-loader; import data/spells.yml at build time - Add SpellTemplate/SpellsFile types to globals.d.ts - Add 'Add Template Spell' button that opens a modal picker pre-filling all spell fields from the YAML data - Move spell data files into data/ directory - Split spell rows into inputs row + full-width notes row (colspan=6) - Shrink delete column to fit-content; bold spell name input - Add class column to spell table; change MP cost to free-text input - Auto-resize spell notes textarea on load and on input - Add 10px padding between spells for visual separation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
0
data/spells.json
Normal file
0
data/spells.json
Normal file
50
data/spells.schema.json
Normal file
50
data/spells.schema.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "data/spells.schema.json",
|
||||
"title": "Spell Sheet",
|
||||
"description": "A collection of Fabula Ultima spells and arcana entries.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"spells": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the spell or arcana."
|
||||
},
|
||||
"cost": {
|
||||
"type": "string",
|
||||
"minimum": 0,
|
||||
"description": "MP cost to cast the spell."
|
||||
},
|
||||
"targets": {
|
||||
"type": "string",
|
||||
"description": "Who or what the spell affects (e.g. \"One creature\", \"All enemies\")."
|
||||
},
|
||||
"duration": {
|
||||
"type": "string",
|
||||
"description": "How long the spell's effect lasts (e.g. \"Instantaneous\", \"Scene\")."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Full effect description and any special rules for the spell."
|
||||
},
|
||||
"class": {
|
||||
"type": "string",
|
||||
"enum": ["arcanist", "chimerist", "elementalist", "entropist", "spiritist"],
|
||||
"description": "The magic-using class this spell belongs to."
|
||||
},
|
||||
"offensive": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the spell is offensive in nature."
|
||||
}
|
||||
},
|
||||
"required": ["name", "cost", "targets", "duration", "description", "class"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["spells"]
|
||||
}
|
||||
287
data/spells.yml
Normal file
287
data/spells.yml
Normal file
@@ -0,0 +1,287 @@
|
||||
spells:
|
||||
- name: Elemental Shroud
|
||||
cost: 5 x T
|
||||
targets: Up to three creatures
|
||||
duration: scene
|
||||
description: "You weave magical energy and protect the targets from the fury of the elements. Choose a damage type: air, bolt, earth, fire or ice. Until this spell ends, each target gains Resistance against the chosen damage type."
|
||||
class: elementalist
|
||||
|
||||
- name: Elemental Weapon
|
||||
cost: "10"
|
||||
targets: One weapon
|
||||
duration: Scene
|
||||
description: "You imbue a weapon with elemental energy. Choose a damage type: air, bolt, earth, fire, or ice. Until this spell ends, all damage dealt by the weapon becomes of the chosen damage type. If you have that weapon equipped while you cast this spell, you may perform a free attack with it as part of the same action. This spell can only be cast on a weapon equipped by a willing creature."
|
||||
class: elementalist
|
||||
|
||||
- name: Flare
|
||||
cost: "20"
|
||||
targets: One creature
|
||||
duration: Instantaneous
|
||||
description: "You channel a single ray of fire towards your foe, its temperature so high that it will pierce through most defenses. The target suffers【 HR + 25】 fire damage. Damage dealt by this spell ignores Resistances."
|
||||
class: elementalist
|
||||
offensive: true
|
||||
|
||||
- name: Fulgur
|
||||
cost: "10 x T"
|
||||
targets: Up to three creatures
|
||||
duration: Instantaneous
|
||||
description: "You weave electricity into a wave of crackling bolts. Each target hit by this spell suffers 【HR + 15】 bolt damage. Opportunity: Each target hit by this spell suffers dazed."
|
||||
class: elementalist
|
||||
offensive: true
|
||||
|
||||
- name: Glacies
|
||||
cost: 10 x T
|
||||
targets: Up to three creatures
|
||||
duration: Instantaneous
|
||||
description: "You coat your foes under a thick layer of frost. Each target hit by this spell suffers【 HR + 15】 ice damage. Opportunity: Each target hit by this spell suffers slow."
|
||||
class: elementalist
|
||||
offensive: true
|
||||
|
||||
- name: Iceberg
|
||||
cost: "20"
|
||||
targets: One creature
|
||||
duration: Instantaneous
|
||||
description: "A pillar of ice magic envelops your foe, suddenly dropping their body temperature to a critical level. The target suffers【 HR + 25】 ice damage. Damage dealt by this spell ignores Resistances."
|
||||
class: elementalist
|
||||
offensive: true
|
||||
|
||||
- name: Ignis
|
||||
cost: 10 x T
|
||||
targets: Up to three creatures
|
||||
duration: Instantaneous
|
||||
description: "You unleash a searing barrage against your foes, conjuring flames out of thin air. Each target hit by this spell suffers【 HR + 15】 fire damage. Opportunity: Each target hit by this spell suffers shaken."
|
||||
class: elementalist
|
||||
offensive: true
|
||||
|
||||
- name: Soaring Strike
|
||||
cost: "10"
|
||||
targets: Self
|
||||
duration: Instantaneous
|
||||
description: "The wind carries your strikes across the battlefield. You may immediately perform a free attack with a melee weapon you have equipped. This attack may target creatures that can only be targeted by ranged attacks. If you used a weapon belonging to the brawling or spear Category for this attack, it deals 5 extra damage. If you hit a flying target with this attack, you may force them to land immediately."
|
||||
class: elementalist
|
||||
|
||||
- name: Terra
|
||||
cost: 10 x T
|
||||
targets: Up to three creatures
|
||||
duration: Instantaneous
|
||||
description: "Spires of jagged rock erupt from the ground beneath your foes, closing around them. Each target hit by this spell suffers【 HR + 15】 earth damage. This spell cannot target creatures who are flying, floating, falling, or otherwise in mid-air. Opportunity: Each target hit by this spell performs one fewer action on their next turn (to a minimum of 0 actions)."
|
||||
class: elementalist
|
||||
offensive: true
|
||||
|
||||
- name: Thunderbolt
|
||||
cost: "20"
|
||||
targets: One creature
|
||||
duration: Instantaneous
|
||||
description: "You send lightning striking at your foe. The target suffers【 HR + 25】 bolt damage. Damage dealt by this spell ignores Resistances."
|
||||
class: elementalist
|
||||
offensive: true
|
||||
|
||||
- name: Ventus
|
||||
cost: 10 x T
|
||||
targets: One creature
|
||||
duration: Instantaneosu
|
||||
description: "You summon the power of winds against your enemy. Each target hit by this spell suffers【 HR + 15】 air damage. Opportunity: Each flying target hit by this spell is forced to land immediately."
|
||||
class: elementalist
|
||||
offensive: true
|
||||
|
||||
- name: Acceleration
|
||||
cost: "20"
|
||||
targets: One creature
|
||||
duration: Scene
|
||||
description: "You bend the fabric of time. Until this spell ends, the target gains the ability to perform a single additional action during each of their turns. Once the target has performed a total of two additional actions granted by this spell, this spell ends."
|
||||
class: entropist
|
||||
offensive: false
|
||||
|
||||
- name: Anomaly
|
||||
cost: "20"
|
||||
targets: One creature
|
||||
duration: Scene
|
||||
description: "You alter the very nature of your target. Until this spell ends, if the target would suffer damage of a type they Absorb or are Immune to, they are instead treated as if they were Vulnerable to that damage type. Once that happens, this spell ends."
|
||||
class: entropist
|
||||
offensive: true
|
||||
|
||||
- name: Dark Weapon
|
||||
cost: "10"
|
||||
targets: One equipped weapon
|
||||
duration: Scene
|
||||
description: "You imbue a weapon with dark energy. Until this spell ends, all damage dealt by the weapon becomes of the dark type. If you have that weapon equipped while you cast this spell, you may perform a free attack with it as part of the same action. This spell can only be cast on a weapon equipped by a willing creature."
|
||||
class: entropist
|
||||
offensive: false
|
||||
|
||||
- name: Dispel
|
||||
cost: "10"
|
||||
targets: One creature
|
||||
duration: Instantaneous
|
||||
description: "You release a wave of negative energy and cleanse all magic from a creature. If the target is affected by one or more spells with a duration of Scene, they are no longer affected by any of those spells instead."
|
||||
class: entropist
|
||||
offensive: false
|
||||
|
||||
- name: Divination
|
||||
cost: "10"
|
||||
targets: Self
|
||||
duration: Scene
|
||||
description: "You glimpse briefly into the future. Until this spell ends, after a creature you can see performs a Check, if it was not a fumble nor a critical success, you may force that creature to reroll both dice. Once you have forced two rerolls this way, this spell ends."
|
||||
class: entropist
|
||||
offensive: false
|
||||
|
||||
- name: Drain Spirit
|
||||
cost: "5"
|
||||
targets: One creature
|
||||
duration: Instantaneous
|
||||
description: "You consume a creature's psyche. The target loses【 HR + 15】 Mind Points. Then, you recover an amount of Mind Points equal to half the Mind Points loss they suffered (if the loss was reduced to 0 in some way, you recover none)."
|
||||
class: entropist
|
||||
offensive: true
|
||||
|
||||
- name: Drain Vigor
|
||||
cost: "10"
|
||||
targets: One creature
|
||||
duration: Instantaneous
|
||||
description: "You steal another creature's life force. The target suffers【 HR + 15】 dark damage. Then, you recover an amount of Hit Points equal to half the Hit Points loss they suffered (if the loss was reduced to 0 in some way, you recover none)."
|
||||
class: entropist
|
||||
offensive: true
|
||||
|
||||
- name: Gamble
|
||||
cost: Up to 20
|
||||
targets: Special
|
||||
duration: Scene
|
||||
description: |
|
||||
You summon a vortex of chaotic energy. Roll your current Willpower die once for every 10 Mind Points spent while casting this spell, then keep the single die you prefer: the number on that die determines the effects of this spell.
|
||||
1) You lose half of your current Hit Points and half of your current Mind Points.
|
||||
2-3) Each creature present on the scene, including yourself, suffers poisoned.
|
||||
4-6) Each creature present on the scene, including yourself, suffers slow.
|
||||
7-8) Choose up to three creatures you can see: each of them recovers 50 Hit Points and also recovers from all status effects.
|
||||
9+) Choose any number of creatures you can see: each of them suffers 30 damage. The damage type is determined randomly by rolling a d6: 1. air 2. bolt 3. dark 4. earth 5. fire 6. poison
|
||||
class: entropist
|
||||
offensive: false
|
||||
|
||||
- name: Mirror
|
||||
cost: "10"
|
||||
targets: One creature
|
||||
duration: Scene
|
||||
description: "You twist the laws of magic. Until this spell ends, if an offensive (r) spell is cast on the target, the creature who cast that offensive spell will be targeted in their stead (any other targets of the offensive spell will be targeted as normal). Once that happens, this spell ends."
|
||||
class: entropist
|
||||
offensive: false
|
||||
|
||||
- name: Omega
|
||||
cost: "20"
|
||||
targets: One creature
|
||||
duration: Instantaneous
|
||||
description: "You invoke doom on your foe, turning strength into frailty. The target loses an amount of Hit Points equal to【 20 + half the target's level】"
|
||||
class: entropist
|
||||
offensive: true
|
||||
|
||||
- name: Stop
|
||||
cost: "10"
|
||||
targets: One creature
|
||||
duration: Instantaneous
|
||||
description: "You trap a foe inside a circle of altered time and space. The target will perform one fewer action on their next turn (to a minimum of 0 actions)."
|
||||
class: entropist
|
||||
offensive: true
|
||||
|
||||
- name: Umbra
|
||||
cost: 10 x T
|
||||
targets: Up to three creatures
|
||||
duration: Scene
|
||||
description: "A storm of dark energy turns matter into ash. Each target hit by this spell suffers 【HR + 15】 dark damage. Opportunity: Each target hit by this spell suffers weak."
|
||||
class: entropist
|
||||
offensive: true
|
||||
|
||||
- name: Vortex
|
||||
cost: "10"
|
||||
targets: Self
|
||||
duration: Scene
|
||||
description: "A roaring gale surrounds you, blowing away arrows and bullets. Until this spell ends, you gain a +2 bonus to your Defense against ranged attacks."
|
||||
class: elementalist
|
||||
|
||||
- name: Aura
|
||||
cost: 5 x T
|
||||
targets: Up to three creatures
|
||||
duration: scene
|
||||
description: You project your soul outside your body and direct it to surround the targets, shielding them from dangerous magic. Until this spell ends, each target may treat their Magic Defense as being equal to 12 against any effects that target it (they are still free to use their normal Defense score if higher than 12).
|
||||
class: spiritist
|
||||
|
||||
- name: Awaken
|
||||
cost: "20"
|
||||
targets: One creature
|
||||
duration: scene
|
||||
description: "You allow a creature to focus their vital energy into accomplishing what they previously could not. Choose one Attribute: Dexterity, Insight, Might, or Willpower. Until this spell ends, the target treats the chosen Attribute as if it were one die size higher (up to a maximum of d12)."
|
||||
class: spiritist
|
||||
|
||||
- name: Barrier
|
||||
cost: 5 x T
|
||||
targets: Up to three creatures
|
||||
duration: scene
|
||||
description: You project your soul outside your body and weave it into a barrier to protect the targets from attacks. Until this spell ends, each target may treat their Defense as being equal to 12 against any effects that target it (they are still free to use their normal Defense score if higher than 12).
|
||||
class: spiritist
|
||||
|
||||
- name: Cleanse
|
||||
cost: 5 x T
|
||||
targets: Up to three creatures
|
||||
duration: Instantaneous
|
||||
description: You strengthen and purify the soul energy coursing through your companions. Each target recovers from all status effects.
|
||||
class: spiritist
|
||||
|
||||
- name: Enrage
|
||||
cost: rr 10
|
||||
targets: One creature
|
||||
duration: Instantaneous
|
||||
description: You cause a creature to lose any semblance of temper and act brazenly. The target suffers enraged and cannot perform the Guard or Spell actions during their next turn.
|
||||
class: spiritist
|
||||
offensive: true
|
||||
|
||||
- name: Hallucination
|
||||
cost: rr 5 x T
|
||||
targets: Up to three creatures
|
||||
duration: Instantaneous
|
||||
description: "You alter the senses of your enemies, causing them to experience bizarre or frightening hallucinations. Choose dazed or shaken: you inflict the chosen status effect on each target hit by this spell."
|
||||
class: spiritist
|
||||
offensive: true
|
||||
|
||||
- name: Heal
|
||||
cost: 10 x T
|
||||
targets: Up to three creatures
|
||||
duration: Instantaneous
|
||||
description: "You invigorate your companions, soothing their pain and healing their fatigue. Each target recovers 40 Hit Points. This amount increases to 50 Hit Points if you are level 20 or higher, or to 60 Hit Points if you are level 40 or higher."
|
||||
class: spiritist
|
||||
offensive: false
|
||||
|
||||
- name: Lux
|
||||
cost: 10 x T
|
||||
targets: Up to three creatures
|
||||
duration: Instantaneous
|
||||
description: "You focus your inner energy into a barrage of blinding soul rays. Each target hit by this spell suffers【 HR + 15】 light damage. Opportunity: Each target hit by this spell suffers dazed."
|
||||
class: spiritist
|
||||
offensive: true
|
||||
|
||||
- name: Mercy
|
||||
cost: "20"
|
||||
targets: One creature
|
||||
duration: Scene
|
||||
description: "You strengthen the heart of a creature against suffering and despair. Until this spell ends, if the target would be reduced to 0 Hit Points, they are instead left standing with exactly 1 Hit Point. Once that happens, this spell ends."
|
||||
class: spiritist
|
||||
offensive: false
|
||||
|
||||
- name: Reinforce
|
||||
cost: 5 x T
|
||||
targets: Up to three creatures
|
||||
duration: Scene
|
||||
description: "You protect the targets from attacks that would corrupt their body and spirit. Choose dazed, enraged, poisoned, shaken, slow, or weak. Until this spell ends, each target becomes immune to the chosen status effect."
|
||||
class: spiritist
|
||||
offensive: false
|
||||
|
||||
- name: Soul Weapon
|
||||
cost: "10"
|
||||
targets: One equipped weapon
|
||||
duration: Scene
|
||||
description: "You imbue a weapon with the cleansing energy of your spirit. Until this spell ends, all damage dealt by the weapon becomes of the light type. If you have that weapon equipped while you cast this spell, you may perform a free attack with it as part of the same action. This spell can only be cast on a weapon equipped by a willing creature."
|
||||
class: spiritist
|
||||
offensive: false
|
||||
|
||||
- name: Torpor
|
||||
cost: 5 x T
|
||||
targets: Up to three creatures
|
||||
duration: Instantaneous
|
||||
description: "You smother the soul energy coursing through the bodies of your foes, hindering their movements. Choose slow or weak: you inflict the chosen status effect on each target hit by this spell."
|
||||
class: spiritist
|
||||
offensive: true
|
||||
|
||||
40
package-lock.json
generated
40
package-lock.json
generated
@@ -24,7 +24,8 @@
|
||||
"typescript": "^6.0.3",
|
||||
"webpack": "^5.107.2",
|
||||
"webpack-cli": "^7.0.3",
|
||||
"webpack-dev-server": "^5.2.4"
|
||||
"webpack-dev-server": "^5.2.4",
|
||||
"yaml-loader": "^0.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -4243,6 +4244,13 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/javascript-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jest-regex-util": {
|
||||
"version": "30.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz",
|
||||
@@ -7233,6 +7241,36 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||||
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml-loader": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml-loader/-/yaml-loader-0.9.0.tgz",
|
||||
"integrity": "sha512-N0bPfDR42qCruELfmpF6Q8XnDu1elTULGjvVn7u6rsfDa8WhlmJbjXQRh+x5TGmpOqw8R9qfiUgc3aKP+8Hd4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"javascript-stringify": "^2.0.1",
|
||||
"yaml": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"typescript": "^6.0.3",
|
||||
"webpack": "^5.107.2",
|
||||
"webpack-cli": "^7.0.3",
|
||||
"webpack-dev-server": "^5.2.4"
|
||||
"webpack-dev-server": "^5.2.4",
|
||||
"yaml-loader": "^0.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
69
scripts/anchors.py
Normal file
69
scripts/anchors.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
# dependencies = [
|
||||
# "bs4",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# Dictionary to track how many times we've seen each header ID
|
||||
header_seen = {}
|
||||
|
||||
def add_anchors_to_headers(html_content):
|
||||
# Parse the HTML content
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
|
||||
# Find all header tags (h1 through h6)
|
||||
header_tags = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
|
||||
|
||||
# Add a count-based ID to each header tag
|
||||
for header in header_tags:
|
||||
# Extract header text (fallback to tag name if empty)
|
||||
header_text = header.get_text(strip=True) or header.name
|
||||
|
||||
# Normalize header
|
||||
header_text = header_text.lower().replace(" ", "-")
|
||||
|
||||
# Create base ID: hN-<text>
|
||||
base_id = f"{header.name}-{header_text}"
|
||||
|
||||
# Check if we've seen this base ID before
|
||||
if base_id in header_seen:
|
||||
# Append next count: -1, -2, -3...
|
||||
count = header_seen[base_id]
|
||||
header_seen[base_id] = count + 1
|
||||
header_id = f"{base_id}-{count + 1}"
|
||||
else:
|
||||
# First time seeing this text → count starts at 1
|
||||
header_seen[base_id] = 1
|
||||
header_id = base_id
|
||||
|
||||
# Add the ID to the header tag
|
||||
header['id'] = header_id
|
||||
|
||||
# Wrap the header in an anchor link (clickable permalink)
|
||||
# The link points to itself via the id attribute
|
||||
# anchor_tag = soup.new_tag("a", href=f"#{header_id}", class_="anchor-link")
|
||||
# anchor_tag.string = f"🔗"
|
||||
# header.wrap(anchor_tag)
|
||||
|
||||
# Return the modified HTML content
|
||||
return str(soup)
|
||||
|
||||
for book in ('./books/core', './books/natural-fantasy-atlas'):
|
||||
for root, dirs, files in Path(book).walk():
|
||||
for fn in files:
|
||||
path = root / fn
|
||||
if path.suffix != ".html":
|
||||
continue
|
||||
with path.open('r') as fh:
|
||||
raw_html = fh.read()
|
||||
|
||||
new_html = add_anchors_to_headers(raw_html)
|
||||
|
||||
with path.open('w') as fh:
|
||||
fh.write(new_html)
|
||||
15
shell.nix
Normal file
15
shell.nix
Normal file
@@ -0,0 +1,15 @@
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
|
||||
pkgs.mkShellNoCC {
|
||||
packages = with pkgs; [
|
||||
go
|
||||
gopls
|
||||
just
|
||||
nodejs
|
||||
starship
|
||||
typescript-language-server
|
||||
];
|
||||
shellHook = ''
|
||||
eval "$(starship init bash)"
|
||||
'';
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from "react";
|
||||
import "./fabula-ultima-sheet.css";
|
||||
import spellsFile from "../data/spells.yml";
|
||||
|
||||
const STATUSES = ["Slow", "Enraged", "Dazed", "Weak", "Poisoned", "Shaken"];
|
||||
const FEELINGS = [
|
||||
@@ -77,8 +78,9 @@ interface ClassEntry {
|
||||
|
||||
interface Spell {
|
||||
name: string;
|
||||
spellClass: string;
|
||||
notes: string;
|
||||
mp: string | number;
|
||||
mp: string;
|
||||
targets: string;
|
||||
duration: string;
|
||||
}
|
||||
@@ -232,6 +234,7 @@ export default function CharacterSheet() {
|
||||
const [primaryClasses, setPrimaryClasses] = useState<ClassEntry[]>([]);
|
||||
const [otherClasses, setOtherClasses] = useState<ClassEntry[]>([]);
|
||||
const [spells, setSpells] = useState<Spell[]>([]);
|
||||
const [spellPickerOpen, setSpellPickerOpen] = useState(false);
|
||||
const [statuses, setStatuses] = useState<CheckMap>({});
|
||||
const [martial, setMartial] = useState<CheckMap>({});
|
||||
const [disciplines, setDisciplines] = useState<CheckMap>({});
|
||||
@@ -315,6 +318,7 @@ export default function CharacterSheet() {
|
||||
oc: otherClasses.map((c) => ({ n: c.name, b: c.benefits, s: c.skills })),
|
||||
sp: spells.map((s) => ({
|
||||
n: s.name,
|
||||
cl: s.spellClass,
|
||||
nt: s.notes,
|
||||
mp: s.mp,
|
||||
tg: s.targets,
|
||||
@@ -422,6 +426,7 @@ export default function CharacterSheet() {
|
||||
setSpells(
|
||||
rawSpells.map((s: SavedData) => ({
|
||||
name: s.n ?? s.name ?? "",
|
||||
spellClass: s.cl ?? s.spellClass ?? "",
|
||||
notes: s.nt ?? s.notes ?? "",
|
||||
mp: s.mp ?? "",
|
||||
targets: s.tg ?? s.targets ?? "",
|
||||
@@ -1327,6 +1332,7 @@ export default function CharacterSheet() {
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="spell-name-col">Name / Notes</th>
|
||||
<th className="spell-class-col">Class</th>
|
||||
<th className="spell-mp-col">MP Cost</th>
|
||||
<th className="spell-targets-col">Targets</th>
|
||||
<th className="spell-dur-col">Duration</th>
|
||||
@@ -1351,10 +1357,24 @@ export default function CharacterSheet() {
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className="spell-class-col">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Class…"
|
||||
value={s.spellClass || ""}
|
||||
onChange={(e) =>
|
||||
setSpells((prev) =>
|
||||
prev.map((sp, j) =>
|
||||
j === i ? { ...sp, spellClass: e.target.value } : sp,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td className="spell-mp-col">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="0"
|
||||
type="text"
|
||||
placeholder="MP cost…"
|
||||
value={s.mp || ""}
|
||||
onChange={(e) =>
|
||||
setSpells((prev) =>
|
||||
@@ -1363,7 +1383,6 @@ export default function CharacterSheet() {
|
||||
),
|
||||
)
|
||||
}
|
||||
style={{ minHeight: 32 }}
|
||||
/>
|
||||
</td>
|
||||
<td className="spell-targets-col">
|
||||
@@ -1406,7 +1425,7 @@ export default function CharacterSheet() {
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="spell-notes-row">
|
||||
<td colSpan={5}>
|
||||
<td colSpan={6}>
|
||||
<textarea
|
||||
placeholder="Notes / effect description…"
|
||||
value={s.notes || ""}
|
||||
@@ -1426,18 +1445,61 @@ export default function CharacterSheet() {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<button
|
||||
className="add-btn"
|
||||
style={{ marginTop: 10 }}
|
||||
onClick={() =>
|
||||
setSpells((prev) => [
|
||||
...prev,
|
||||
{ name: "", notes: "", mp: "", targets: "", duration: "" },
|
||||
])
|
||||
}
|
||||
>
|
||||
+ Add Spell / Arcana
|
||||
</button>
|
||||
<div style={{ display: "flex", gap: 8, marginTop: 10 }}>
|
||||
<button
|
||||
className="add-btn"
|
||||
onClick={() =>
|
||||
setSpells((prev) => [
|
||||
...prev,
|
||||
{ name: "", spellClass: "", notes: "", mp: "", targets: "", duration: "" },
|
||||
])
|
||||
}
|
||||
>
|
||||
+ Add Spell / Arcana
|
||||
</button>
|
||||
<button
|
||||
className="add-btn"
|
||||
onClick={() => setSpellPickerOpen(true)}
|
||||
>
|
||||
+ Add Template Spell
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{spellPickerOpen && (
|
||||
<div className="spell-picker-overlay" onClick={() => setSpellPickerOpen(false)}>
|
||||
<div className="spell-picker-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="spell-picker-header">
|
||||
<span>Choose a spell</span>
|
||||
<button className="spell-picker-close" onClick={() => setSpellPickerOpen(false)}>✕</button>
|
||||
</div>
|
||||
<ul className="spell-picker-list">
|
||||
{(spellsFile as SpellsFile).spells.map((t, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="spell-picker-item"
|
||||
onClick={() => {
|
||||
setSpells((prev) => [
|
||||
...prev,
|
||||
{
|
||||
name: t.name,
|
||||
spellClass: t.class,
|
||||
notes: t.description,
|
||||
mp: t.cost,
|
||||
targets: t.targets,
|
||||
duration: t.duration,
|
||||
},
|
||||
]);
|
||||
setSpellPickerOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="spell-picker-name">{t.name}</span>
|
||||
<span className="spell-picker-class">{t.class}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
|
||||
@@ -786,8 +786,16 @@ input[type="number"] {
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
.spell-name-col input {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.spell-class-col {
|
||||
width: 14%;
|
||||
}
|
||||
|
||||
.spell-mp-col {
|
||||
width: 12%;
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.spell-targets-col {
|
||||
@@ -839,6 +847,82 @@ input[type="number"] {
|
||||
color: var(--text-bright);
|
||||
}
|
||||
|
||||
.spell-picker-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.spell-picker-modal {
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border-bright);
|
||||
width: min(480px, 90vw);
|
||||
max-height: 70vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.spell-picker-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--border-bright);
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.6rem;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
color: var(--teal);
|
||||
}
|
||||
|
||||
.spell-picker-close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--text-dim);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.spell-picker-close:hover {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.spell-picker-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.spell-picker-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
padding: 8px 14px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.spell-picker-item:hover {
|
||||
background: var(--surface3);
|
||||
}
|
||||
|
||||
.spell-picker-name {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.spell-picker-class {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-dim);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
/* ── DISCIPLINES ─────────────────────────────────── */
|
||||
.disciplines-row {
|
||||
display: flex;
|
||||
|
||||
19
src/globals.d.ts
vendored
19
src/globals.d.ts
vendored
@@ -1,5 +1,24 @@
|
||||
declare module "*.css";
|
||||
|
||||
declare module "*.yml" {
|
||||
const data: unknown;
|
||||
export default data;
|
||||
}
|
||||
|
||||
interface SpellTemplate {
|
||||
name: string;
|
||||
cost: string;
|
||||
targets: string;
|
||||
duration: string;
|
||||
description: string;
|
||||
class: string;
|
||||
offensive?: boolean;
|
||||
}
|
||||
|
||||
interface SpellsFile {
|
||||
spells: SpellTemplate[];
|
||||
}
|
||||
|
||||
interface BookPage {
|
||||
n: number;
|
||||
content: string;
|
||||
|
||||
@@ -65,6 +65,10 @@ module.exports = (env, argv) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.ya?ml$/,
|
||||
use: "yaml-loader",
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
|
||||
Reference in New Issue
Block a user