feat: Add armor and shields to equipment picker
All checks were successful
Deploy / deploy (push) Successful in 52s

Import armor_shields.yml and merge it with weapons in the '+ Add Equipment' picker. Armor/shield entries are formatted with DEF/MDEF/Init fields. Adds armor and shield filter categories alongside existing weapon categories.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 12:13:33 -04:00
parent e6e37d5819
commit dfe4e1d189
3 changed files with 45 additions and 11 deletions

View File

@@ -48,7 +48,7 @@ armor_shields:
category: armor category: armor
description: No Quality. description: No Quality.
- name: Bronze Plate (E) - name: Bronze Plate
cost: 200 cost: 200
defense: "11" defense: "11"
magic_defense: "INS size" magic_defense: "INS size"
@@ -56,7 +56,7 @@ armor_shields:
category: armor category: armor
description: No Quality. description: No Quality.
- name: Runic Plate (E) - name: Runic Plate
cost: 250 cost: 250
defense: "11" defense: "11"
magic_defense: "INS size +1" magic_defense: "INS size +1"
@@ -64,7 +64,7 @@ armor_shields:
category: armor category: armor
description: No Quality. description: No Quality.
- name: Steel Plate (E) - name: Steel Plate
cost: 300 cost: 300
defense: "12" defense: "12"
magic_defense: "INS size" magic_defense: "INS size"
@@ -80,7 +80,7 @@ armor_shields:
category: shield category: shield
description: No Quality. description: No Quality.
- name: Runic Shield (E) - name: Runic Shield
cost: 150 cost: 150
defense: "+2" defense: "+2"
magic_defense: "+2" magic_defense: "+2"

View File

@@ -2,6 +2,7 @@ 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"; import weaponsFile from "../data/weapons.yml";
import armorShieldsFile from "../data/armor_shields.yml";
const STATUSES = ["Slow", "Enraged", "Dazed", "Weak", "Poisoned", "Shaken"]; const STATUSES = ["Slow", "Enraged", "Dazed", "Weak", "Poisoned", "Shaken"];
const FEELINGS = [ const FEELINGS = [
@@ -1165,13 +1166,31 @@ export default function CharacterSheet() {
{weaponPickerOpen && (() => { {weaponPickerOpen && (() => {
const allWeapons = (weaponsFile as WeaponsFile).weapons; const allWeapons = (weaponsFile as WeaponsFile).weapons;
const categories = ["all", ...Array.from(new Set(allWeapons.map(w => w.category))).sort()]; const allArmorShields = (armorShieldsFile as ArmorShieldsFile).armor_shields;
const visible = weaponCategory === "all" ? allWeapons : allWeapons.filter(w => w.category === weaponCategory); type PickerItem =
| { kind: "weapon"; data: WeaponTemplate }
| { kind: "armor"; data: ArmorShieldTemplate };
const allItems: PickerItem[] = [
...allWeapons.map(w => ({ kind: "weapon" as const, data: w })),
...allArmorShields.map(a => ({ kind: "armor" as const, data: a })),
];
const categories = ["all", ...Array.from(new Set(allItems.map(i => i.data.category))).sort()];
const visible = weaponCategory === "all" ? allItems : allItems.filter(i => i.data.category === weaponCategory);
const formatLine = (item: PickerItem) => {
if (item.kind === "weapon") {
const w = item.data;
return `${w.name}: Acc ${w.accuracy}, Dmg ${w.damage}${w.description ? ` | ${w.description}` : ""}${w.cost > 0 ? ` (${w.cost}z)` : ""}`;
} else {
const a = item.data;
const init = a.initiative ?? a.initative ?? 0;
return `${a.name}: DEF ${a.defense}, MDEF ${a.magic_defense}, Init ${init}${a.description ? ` | ${a.description}` : ""}${a.cost > 0 ? ` (${a.cost}z)` : ""}`;
}
};
return ( return (
<div className="spell-picker-overlay" onClick={() => setWeaponPickerOpen(false)}> <div className="spell-picker-overlay" onClick={() => setWeaponPickerOpen(false)}>
<div className="spell-picker-modal" onClick={e => e.stopPropagation()}> <div className="spell-picker-modal" onClick={e => e.stopPropagation()}>
<div className="spell-picker-header"> <div className="spell-picker-header">
<span>Choose a weapon</span> <span>Choose equipment</span>
<button className="spell-picker-close" onClick={() => setWeaponPickerOpen(false)}></button> <button className="spell-picker-close" onClick={() => setWeaponPickerOpen(false)}></button>
</div> </div>
<div style={{ display: "flex", gap: 6, flexWrap: "wrap", padding: "8px 14px", borderBottom: "1px solid var(--border)" }}> <div style={{ display: "flex", gap: 6, flexWrap: "wrap", padding: "8px 14px", borderBottom: "1px solid var(--border)" }}>
@@ -1187,18 +1206,18 @@ export default function CharacterSheet() {
))} ))}
</div> </div>
<ul className="spell-picker-list"> <ul className="spell-picker-list">
{visible.map((w, i) => ( {visible.map((item, i) => (
<li <li
key={i} key={i}
className="spell-picker-item" className="spell-picker-item"
onClick={() => { onClick={() => {
const line = `${w.name}: Acc ${w.accuracy}, Dmg ${w.damage}${w.description ? ` | ${w.description}` : ""}${w.cost > 0 ? ` (${w.cost}z)` : ""}`; const line = formatLine(item);
f("backpack", fields.backpack ? fields.backpack + "\n" + line : line); f("backpack", fields.backpack ? fields.backpack + "\n" + line : line);
setWeaponPickerOpen(false); setWeaponPickerOpen(false);
}} }}
> >
<span className="spell-picker-name">{w.name}</span> <span className="spell-picker-name">{item.data.name}</span>
<span className="spell-picker-class">{w.category}</span> <span className="spell-picker-class">{item.data.category}</span>
</li> </li>
))} ))}
</ul> </ul>

15
src/globals.d.ts vendored
View File

@@ -18,6 +18,21 @@ interface WeaponsFile {
weapons: WeaponTemplate[]; weapons: WeaponTemplate[];
} }
interface ArmorShieldTemplate {
name: string;
cost: number;
defense: string;
magic_defense: string;
initiative?: number;
initative?: number; // typo present in source YAML
category: string;
description?: string;
}
interface ArmorShieldsFile {
armor_shields: ArmorShieldTemplate[];
}
interface SpellTemplate { interface SpellTemplate {
name: string; name: string;
cost: string; cost: string;