feat: Add weapon selection to backpack
This commit is contained in:
89
data/armor_shields.yml
Normal file
89
data/armor_shields.yml
Normal 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
149
data/weapons.yml
Normal 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
26
find-empty-pages.sh
Executable 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[@]}))"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect, useCallback, useRef } from "react";
|
import React, { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import "./fabula-ultima-sheet.css";
|
import "./fabula-ultima-sheet.css";
|
||||||
import spellsFile from "../data/spells.yml";
|
import spellsFile from "../data/spells.yml";
|
||||||
|
import weaponsFile from "../data/weapons.yml";
|
||||||
|
|
||||||
const STATUSES = ["Slow", "Enraged", "Dazed", "Weak", "Poisoned", "Shaken"];
|
const STATUSES = ["Slow", "Enraged", "Dazed", "Weak", "Poisoned", "Shaken"];
|
||||||
const FEELINGS = [
|
const FEELINGS = [
|
||||||
@@ -235,6 +236,8 @@ export default function CharacterSheet() {
|
|||||||
const [otherClasses, setOtherClasses] = useState<ClassEntry[]>([]);
|
const [otherClasses, setOtherClasses] = useState<ClassEntry[]>([]);
|
||||||
const [spells, setSpells] = useState<Spell[]>([]);
|
const [spells, setSpells] = useState<Spell[]>([]);
|
||||||
const [spellPickerOpen, setSpellPickerOpen] = useState(false);
|
const [spellPickerOpen, setSpellPickerOpen] = useState(false);
|
||||||
|
const [weaponPickerOpen, setWeaponPickerOpen] = useState(false);
|
||||||
|
const [weaponCategory, setWeaponCategory] = useState<string>("all");
|
||||||
const [statuses, setStatuses] = useState<CheckMap>({});
|
const [statuses, setStatuses] = useState<CheckMap>({});
|
||||||
const [martial, setMartial] = useState<CheckMap>({});
|
const [martial, setMartial] = useState<CheckMap>({});
|
||||||
const [disciplines, setDisciplines] = useState<CheckMap>({});
|
const [disciplines, setDisciplines] = useState<CheckMap>({});
|
||||||
@@ -1134,6 +1137,57 @@ export default function CharacterSheet() {
|
|||||||
placeholder="Items, notes, lore…"
|
placeholder="Items, notes, lore…"
|
||||||
style={{ minHeight: 200 }}
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -923,6 +923,12 @@ input[type="number"] {
|
|||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.add-btn.active-filter {
|
||||||
|
background: var(--teal);
|
||||||
|
color: var(--bg);
|
||||||
|
border-color: var(--teal);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── DISCIPLINES ─────────────────────────────────── */
|
/* ── DISCIPLINES ─────────────────────────────────── */
|
||||||
.disciplines-row {
|
.disciplines-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
13
src/globals.d.ts
vendored
13
src/globals.d.ts
vendored
@@ -5,6 +5,19 @@ declare module "*.yml" {
|
|||||||
export default data;
|
export default data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WeaponTemplate {
|
||||||
|
name: string;
|
||||||
|
cost: number;
|
||||||
|
accuracy: string;
|
||||||
|
damage: string;
|
||||||
|
category: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WeaponsFile {
|
||||||
|
weapons: WeaponTemplate[];
|
||||||
|
}
|
||||||
|
|
||||||
interface SpellTemplate {
|
interface SpellTemplate {
|
||||||
name: string;
|
name: string;
|
||||||
cost: string;
|
cost: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user