feat: Add weapon selection to backpack

This commit is contained in:
2026-06-28 12:06:35 -04:00
parent 84b156313a
commit 0215d8b87b
6 changed files with 337 additions and 0 deletions

89
data/armor_shields.yml Normal file
View File

@@ -0,0 +1,89 @@
armor_shields:
- name: No Armor
cost: 0
defense: DEX size
magic_defense: INS size
initiative: 0
category: armor
description: No Quality.
- name: Silk Shirt
cost: 100
defense: DEX size
magic_defense: INS size +2
initative: -1
category: armor
description: No Quality.
- name: Travel Garb
cost: 100
defense: DEX size +1
magic_defense: INS size +1
initiative: -1
category: armor
description: No Quality.
- name: Combat Tunic
cost: 150
defense: DEX size +1
magic_defense: INS size +1
initiative: 0
category: armor
description: No Quality.
- name: Sage Robe
cost: 200
defense: "DEX size +1"
magic_defense: "INS size +2"
initiative: -2
category: armor
description: No Quality.
- name: Brigandine E
cost: 150
defense: "10"
magic_defense: "INS size"
initiative: -2
category: armor
description: No Quality.
- name: Bronze Plate (E)
cost: 200
defense: "11"
magic_defense: "INS size"
initiative: -3
category: armor
description: No Quality.
- name: Runic Plate (E)
cost: 250
defense: "11"
magic_defense: "INS size +1"
initiative: -3
category: armor
description: No Quality.
- name: Steel Plate (E)
cost: 300
defense: "12"
magic_defense: "INS size"
initative: -4
category: armor
description: No Quality.
- name: Bronze Shield
cost: 100
defense: "+2"
magic_defense: "0"
initiative: 0
category: shield
description: No Quality.
- name: Runic Shield (E)
cost: 150
defense: "+2"
magic_defense: "+2"
initiative: 0
category: shield
description: No Quality.

149
data/weapons.yml Normal file
View File

@@ -0,0 +1,149 @@
weapons:
- name: Staff
cost: 100
accuracy: "【WLP + WLP】"
damage: "【HR + 6】 physical"
category: arcane
description: Two-handed, Melee, No Quality.
- name: Tome
cost: 100
accuracy: "【INS + INS】"
damage: "【HR + 6】 physical"
category: arcane
description: Two-handed, Melee, No Quality.
- name: Crossbow
cost: 150
accuracy: "【DEX + INS】"
damage: "【HR + 8】 physical"
category: bow
description: Two-handed, Ranged, No Quality.
- name: Shortbow
cost: 200
accuracy: "【DEX + DEX】"
damage: "【HR + 8】 physical"
category: bow
description: Two-handed, Ranged, No Quality.
- name: Unarmed Strike
cost: 0
accuracy: "【DEX + MIG】"
damage: "【HR + 0】 physical"
category: brawling
description: One-handed, Melee, Automatically equipped in each empty hand slot.
- name: Improvised (Melee)
cost: 0
accuracy: "【DEX + MIG】"
damage: "【HR + 2】 physical"
category: brawling
description: One-handed, Melee, Breaks after the attack.
- name: Iron Knuckle
cost: 150
accuracy: "【DEX + MIG】"
damage: "【HR + 6】 physical"
category: brawling
description: One-handed, Melee, No Quality.
- name: Steel Dagger
cost: 150
accuracy: "【DEX + INS】 +1"
damage: "【HR + 4】 physical"
category: dagger
description: One-handed, Melee, No Quality.
- name: Pistol ✦
cost: 250
accuracy: "【DEX + INS】"
damage: "【HR + 8】 physical"
category: firearm
description: One-handed, Ranged, No Quality.
- name: Chain Whip
cost: 150
accuracy: "【DEX + DEX】"
damage: "【HR + 8】 physical"
category: flail
description: Two-handed, Melee, No Quality.
- name: Iron Hammer
cost: 200
accuracy: "【MIG + MIG】"
damage: "【HR + 6】 physical"
category: "heavy"
description: One-handed w Melee w No Quality.
- name: Broadaxe ✦
cost: 250
accuracy: "【MIG + MIG】"
damage: "【HR + 10】 physical"
category: "heavy"
description: One-handed w Melee w No Quality.
- name: Waraxe ✦
cost: 250
accuracy: "【MIG + MIG】"
damage: "【HR + 14】 physical"
category: "heavy"
description: Two-handed w Melee w No Quality.
- name: Light Spear ✦
cost: 200
accuracy: "【DEX + MIG】"
damage: "【HR + 8】 physical"
category: "spear"
description: One-handed w Melee w No Quality.
- name: Heavy Spear ✦
cost: 200
accuracy: "【DEX + MIG】 【HR + 12】 physical"
damage: "Two-handed w Melee w No Quality."
category: "spear"
description:
- name: Bronze Sword ✦
cost: 200
accuracy: "【DEX + MIG】 +1"
damage: "【HR + 6】 physical"
category: "sword"
description: One-handed w Melee w No Quality.
- name: Greatsword ✦
cost: 200
accuracy: "【DEX + MIG】 +1"
damage: "【HR + 10】 physical"
category: "sword"
description: Two-handed w Melee w No Quality.
- name: Katana ✦
cost: 200
accuracy: "【DEX + INS】 +1"
damage: "【HR + 10】 physical"
category: "sword"
description: Two-handed w Melee w No Quality.
- name: Rapier ✦
cost: 200
accuracy: "【DEX + INS】 +1"
damage: "【HR + 6】 physical"
category: "sword"
description: One-handed w Melee w No Quality.
- name: Improvised (Ranged)
cost: 0
accuracy: "【DEX + MIG】"
damage: "【HR + 2】 physical"
category: "thrown"
description: One-handed w Ranged w Breaks after the attack.
- name: Shuriken
cost: 150
accuracy: "【DEX + INS】"
damage: "【HR + 4】 physical"
category: "thrown"
description: One-handed w Ranged w No Quality.

26
find-empty-pages.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Finds blank (empty or whitespace-only) HTML files in books/core
DIR="$(dirname "$0")/books/core"
empty=()
whitespace_only=()
for f in "$DIR"/*.html; do
if [ ! -s "$f" ]; then
empty+=("$f")
elif ! grep -qE '[^[:space:]]' "$f"; then
whitespace_only+=("$f")
fi
done
echo "=== Empty (0 bytes): ${#empty[@]} files ==="
for f in "${empty[@]}"; do printf ' %s\n' "$(basename "$f")"; done | sort -V
if [ ${#whitespace_only[@]} -gt 0 ]; then
echo ""
echo "=== Whitespace-only: ${#whitespace_only[@]} files ==="
for f in "${whitespace_only[@]}"; do printf ' %s\n' "$(basename "$f")"; done | sort -V
fi
echo ""
echo "Total blank: $((${#empty[@]} + ${#whitespace_only[@]}))"

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect, useCallback, useRef } from "react";
import "./fabula-ultima-sheet.css";
import spellsFile from "../data/spells.yml";
import weaponsFile from "../data/weapons.yml";
const STATUSES = ["Slow", "Enraged", "Dazed", "Weak", "Poisoned", "Shaken"];
const FEELINGS = [
@@ -235,6 +236,8 @@ export default function CharacterSheet() {
const [otherClasses, setOtherClasses] = useState<ClassEntry[]>([]);
const [spells, setSpells] = useState<Spell[]>([]);
const [spellPickerOpen, setSpellPickerOpen] = useState(false);
const [weaponPickerOpen, setWeaponPickerOpen] = useState(false);
const [weaponCategory, setWeaponCategory] = useState<string>("all");
const [statuses, setStatuses] = useState<CheckMap>({});
const [martial, setMartial] = useState<CheckMap>({});
const [disciplines, setDisciplines] = useState<CheckMap>({});
@@ -1134,6 +1137,57 @@ export default function CharacterSheet() {
placeholder="Items, notes, lore…"
style={{ minHeight: 200 }}
/>
<button
className="add-btn"
style={{ marginTop: 8 }}
onClick={() => { setWeaponCategory("all"); setWeaponPickerOpen(true); }}
>
+ Add Equipment
</button>
{weaponPickerOpen && (() => {
const allWeapons = (weaponsFile as WeaponsFile).weapons;
const categories = ["all", ...Array.from(new Set(allWeapons.map(w => w.category))).sort()];
const visible = weaponCategory === "all" ? allWeapons : allWeapons.filter(w => w.category === weaponCategory);
return (
<div className="spell-picker-overlay" onClick={() => setWeaponPickerOpen(false)}>
<div className="spell-picker-modal" onClick={e => e.stopPropagation()}>
<div className="spell-picker-header">
<span>Choose a weapon</span>
<button className="spell-picker-close" onClick={() => setWeaponPickerOpen(false)}></button>
</div>
<div style={{ display: "flex", gap: 6, flexWrap: "wrap", padding: "8px 14px", borderBottom: "1px solid var(--border)" }}>
{categories.map(cat => (
<button
key={cat}
className={`add-btn${weaponCategory === cat ? " active-filter" : ""}`}
style={{ padding: "2px 8px", fontSize: "0.65rem", textTransform: "capitalize" }}
onClick={() => setWeaponCategory(cat)}
>
{cat}
</button>
))}
</div>
<ul className="spell-picker-list">
{visible.map((w, i) => (
<li
key={i}
className="spell-picker-item"
onClick={() => {
const line = `${w.name}: Acc ${w.accuracy}, Dmg ${w.damage}${w.description ? ` | ${w.description}` : ""}${w.cost > 0 ? ` (${w.cost}z)` : ""}`;
f("backpack", fields.backpack ? fields.backpack + "\n" + line : line);
setWeaponPickerOpen(false);
}}
>
<span className="spell-picker-name">{w.name}</span>
<span className="spell-picker-class">{w.category}</span>
</li>
))}
</ul>
</div>
</div>
);
})()}
</div>
</div>
</div>

View File

@@ -923,6 +923,12 @@ input[type="number"] {
text-transform: capitalize;
}
.add-btn.active-filter {
background: var(--teal);
color: var(--bg);
border-color: var(--teal);
}
/* ── DISCIPLINES ─────────────────────────────────── */
.disciplines-row {
display: flex;

13
src/globals.d.ts vendored
View File

@@ -5,6 +5,19 @@ declare module "*.yml" {
export default data;
}
interface WeaponTemplate {
name: string;
cost: number;
accuracy: string;
damage: string;
category: string;
description?: string;
}
interface WeaponsFile {
weapons: WeaponTemplate[];
}
interface SpellTemplate {
name: string;
cost: string;