feat: Enforce mutual exclusivity for bond feelings

Reorder feelings so opposing pairs (Admiration/Inferiority, Loyalty/Mistrust, Hatred/Affection) are stacked vertically in the grid. Checking one feeling disables and prevents selecting its opposite.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 12:09:32 -04:00
parent 0215d8b87b
commit e6e37d5819
3 changed files with 47 additions and 24 deletions

View File

@@ -82,26 +82,26 @@ weapons:
accuracy: "【MIG + MIG】" accuracy: "【MIG + MIG】"
damage: "【HR + 10】 physical" damage: "【HR + 10】 physical"
category: "heavy" category: "heavy"
description: One-handed w Melee w No Quality. description: One-handed, Melee, No Quality.
- name: Waraxe ✦ - name: Waraxe ✦
cost: 250 cost: 250
accuracy: "【MIG + MIG】" accuracy: "【MIG + MIG】"
damage: "【HR + 14】 physical" damage: "【HR + 14】 physical"
category: "heavy" category: "heavy"
description: Two-handed w Melee w No Quality. description: Two-handed, Melee, No Quality.
- name: Light Spear ✦ - name: Light Spear ✦
cost: 200 cost: 200
accuracy: "【DEX + MIG】" accuracy: "【DEX + MIG】"
damage: "【HR + 8】 physical" damage: "【HR + 8】 physical"
category: "spear" category: "spear"
description: One-handed w Melee w No Quality. description: One-handed, Melee, No Quality.
- name: Heavy Spear ✦ - name: Heavy Spear ✦
cost: 200 cost: 200
accuracy: "【DEX + MIG】 【HR + 12】 physical" accuracy: "【DEX + MIG】 【HR + 12】 physical"
damage: "Two-handed w Melee w No Quality." damage: "Two-handed, Melee, No Quality."
category: "spear" category: "spear"
description: description:
@@ -110,40 +110,40 @@ weapons:
accuracy: "【DEX + MIG】 +1" accuracy: "【DEX + MIG】 +1"
damage: "【HR + 6】 physical" damage: "【HR + 6】 physical"
category: "sword" category: "sword"
description: One-handed w Melee w No Quality. description: One-handed, Melee, No Quality.
- name: Greatsword ✦ - name: Greatsword ✦
cost: 200 cost: 200
accuracy: "【DEX + MIG】 +1" accuracy: "【DEX + MIG】 +1"
damage: "【HR + 10】 physical" damage: "【HR + 10】 physical"
category: "sword" category: "sword"
description: Two-handed w Melee w No Quality. description: Two-handed, Melee, No Quality.
- name: Katana ✦ - name: Katana ✦
cost: 200 cost: 200
accuracy: "【DEX + INS】 +1" accuracy: "【DEX + INS】 +1"
damage: "【HR + 10】 physical" damage: "【HR + 10】 physical"
category: "sword" category: "sword"
description: Two-handed w Melee w No Quality. description: Two-handed, Melee, No Quality.
- name: Rapier ✦ - name: Rapier ✦
cost: 200 cost: 200
accuracy: "【DEX + INS】 +1" accuracy: "【DEX + INS】 +1"
damage: "【HR + 6】 physical" damage: "【HR + 6】 physical"
category: "sword" category: "sword"
description: One-handed w Melee w No Quality. description: One-handed, Melee, No Quality.
- name: Improvised (Ranged) - name: Improvised (Ranged)
cost: 0 cost: 0
accuracy: "【DEX + MIG】" accuracy: "【DEX + MIG】"
damage: "【HR + 2】 physical" damage: "【HR + 2】 physical"
category: "thrown" category: "thrown"
description: One-handed w Ranged w Breaks after the attack. description: One-handed, Ranged, Breaks after the attack.
- name: Shuriken - name: Shuriken
cost: 150 cost: 150
accuracy: "【DEX + INS】" accuracy: "【DEX + INS】"
damage: "【HR + 4】 physical" damage: "【HR + 4】 physical"
category: "thrown" category: "thrown"
description: One-handed w Ranged w No Quality. description: One-handed, Ranged, No Quality.

View File

@@ -6,12 +6,20 @@ 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 = [
"Admiration", "Admiration",
"Inferiority",
"Loyalty", "Loyalty",
"Hatred",
"Inferiority",
"Mistrust", "Mistrust",
"Affection", "Affection",
"Hatred",
]; ];
const MUTUAL_EXCLUSIVE: Record<string, string> = {
Admiration: "Inferiority",
Inferiority: "Admiration",
Loyalty: "Mistrust",
Mistrust: "Loyalty",
Hatred: "Affection",
Affection: "Hatred",
};
const MARTIAL_ITEMS = [ const MARTIAL_ITEMS = [
"Martial Armor", "Martial Armor",
"Martial Shields", "Martial Shields",
@@ -581,11 +589,13 @@ export default function CharacterSheet() {
prev.map((b, i) => { prev.map((b, i) => {
if (i !== bondIdx) return b; if (i !== bondIdx) return b;
const has = b.feelings.includes(feeling); const has = b.feelings.includes(feeling);
const partner = MUTUAL_EXCLUSIVE[feeling];
if (!has && partner && b.feelings.includes(partner)) return b;
return { return {
...b, ...b,
feelings: has feelings: has
? b.feelings.filter((f) => f !== feeling) ? b.feelings.filter((f) => f !== feeling)
: [...b.feelings, feeling], : [...b.feelings.filter((f) => f !== partner), feeling],
}; };
}), }),
); );
@@ -1039,18 +1049,26 @@ export default function CharacterSheet() {
/> />
</div> </div>
<div className="bond-feelings"> <div className="bond-feelings">
{FEELINGS.map((feeling) => ( {FEELINGS.map((feeling) => {
<div const isActive = bond.feelings.includes(feeling);
key={feeling} const isDisabled =
className={`bond-feeling${bond.feelings.includes(feeling) ? " active" : ""}`} !isActive &&
onClick={() => toggleFeeling(idx, feeling)} bond.feelings.includes(MUTUAL_EXCLUSIVE[feeling]);
> return (
<div className="bond-feeling-box"> <div
{bond.feelings.includes(feeling) ? "✓" : ""} key={feeling}
className={`bond-feeling${isActive ? " active" : ""}${isDisabled ? " disabled" : ""}`}
onClick={() =>
!isDisabled && toggleFeeling(idx, feeling)
}
>
<div className="bond-feeling-box">
{isActive ? "✓" : ""}
</div>
<span>{feeling}</span>
</div> </div>
<span>{feeling}</span> );
</div> })}
))}
</div> </div>
</div> </div>
))} ))}

View File

@@ -582,6 +582,11 @@ input[type="number"] {
color: var(--teal); color: var(--teal);
} }
.bond-feeling.disabled {
opacity: 0.3;
cursor: not-allowed;
}
.bond-feeling-box { .bond-feeling-box {
width: 12px; width: 12px;
height: 12px; height: 12px;