Wire up talent effects: stat bonuses, AC bonus, attack/damage bonuses, gear slot bonuses
This commit is contained in:
parent
859ac64868
commit
0bd83e5f92
7 changed files with 148 additions and 17 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import type { Character, GameItem } from "../types";
|
||||
import { getTalentGearSlotsBonus } from "../utils/talent-effects";
|
||||
import GearList from "./GearList";
|
||||
import styles from "./GearPanel.module.css";
|
||||
|
||||
|
|
@ -27,6 +28,8 @@ export default function GearPanel({
|
|||
onCurrencyChange,
|
||||
}: GearPanelProps) {
|
||||
const slotsUsed = character.gear.reduce((sum, g) => sum + g.slot_count, 0);
|
||||
const slotsMax =
|
||||
character.gear_slots_max + getTalentGearSlotsBonus(character);
|
||||
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
|
|
@ -36,7 +39,7 @@ export default function GearPanel({
|
|||
sp={character.sp}
|
||||
cp={character.cp}
|
||||
slotsUsed={slotsUsed}
|
||||
slotsMax={character.gear_slots_max}
|
||||
slotsMax={slotsMax}
|
||||
onAddFromItem={(item) => onAddGearFromItem(character.id, item)}
|
||||
onAddCustom={(data) => onAddGearCustom(character.id, data)}
|
||||
onRemove={(gearId) => onRemoveGear(character.id, gearId)}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@
|
|||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.bonus {
|
||||
color: #4caf50;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const STAT_ORDER = ["STR", "DEX", "CON", "INT", "WIS", "CHA"];
|
|||
|
||||
interface StatBlockProps {
|
||||
stats: Stat[];
|
||||
statBonuses?: Map<string, number>;
|
||||
onStatChange: (statName: string, newValue: number) => void;
|
||||
mode?: "view" | "edit";
|
||||
campaignId?: number;
|
||||
|
|
@ -17,6 +18,7 @@ interface StatBlockProps {
|
|||
|
||||
export default function StatBlock({
|
||||
stats,
|
||||
statBonuses,
|
||||
onStatChange,
|
||||
mode = "view",
|
||||
campaignId,
|
||||
|
|
@ -29,7 +31,9 @@ export default function StatBlock({
|
|||
return (
|
||||
<div className={styles.statGrid}>
|
||||
{STAT_ORDER.map((name) => {
|
||||
const value = statMap.get(name) ?? 10;
|
||||
const baseValue = statMap.get(name) ?? 10;
|
||||
const bonus = statBonuses?.get(name) ?? 0;
|
||||
const value = baseValue + bonus;
|
||||
const mod = getModifier(value);
|
||||
return (
|
||||
<div key={name} className={styles.stat}>
|
||||
|
|
@ -38,7 +42,7 @@ export default function StatBlock({
|
|||
{mode === "edit" && (
|
||||
<button
|
||||
className={styles.btn}
|
||||
onClick={() => onStatChange(name, value - 1)}
|
||||
onClick={() => onStatChange(name, baseValue - 1)}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
|
|
@ -58,13 +62,16 @@ export default function StatBlock({
|
|||
{mode === "edit" && (
|
||||
<button
|
||||
className={styles.btn}
|
||||
onClick={() => onStatChange(name, value + 1)}
|
||||
onClick={() => onStatChange(name, baseValue + 1)}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<span className={styles.score}>{value}</span>
|
||||
<span className={styles.score}>
|
||||
{value}
|
||||
{bonus > 0 && <span className={styles.bonus}> (+{bonus})</span>}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import StatBlock from "./StatBlock";
|
|||
import AttackBlock from "./AttackBlock";
|
||||
import styles from "./StatsPanel.module.css";
|
||||
|
||||
const STATS = ["STR", "DEX", "CON", "INT", "WIS", "CHA"];
|
||||
|
||||
interface StatsPanelProps {
|
||||
character: Character;
|
||||
mode: "view" | "edit";
|
||||
|
|
@ -21,11 +23,24 @@ export default function StatsPanel({
|
|||
}: StatsPanelProps) {
|
||||
const attacks = generateAttacks(character);
|
||||
|
||||
// Compute talent stat bonuses
|
||||
const statBonuses = new Map<string, number>();
|
||||
for (const stat of STATS) {
|
||||
let bonus = 0;
|
||||
for (const talent of character.talents) {
|
||||
if (talent.effect.stat_bonus === stat) {
|
||||
bonus += (talent.effect.stat_bonus_value as number) || 0;
|
||||
}
|
||||
}
|
||||
if (bonus > 0) statBonuses.set(stat, bonus);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.sectionTitle}>Ability Scores</div>
|
||||
<StatBlock
|
||||
stats={character.stats}
|
||||
statBonuses={statBonuses.size > 0 ? statBonuses : undefined}
|
||||
onStatChange={(statName, value) =>
|
||||
onStatChange(character.id, statName, value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { Character } from "../types";
|
||||
import { getModifier } from "./modifiers";
|
||||
import { getEffectiveStat, getTalentAcBonus } from "./talent-effects";
|
||||
|
||||
export interface AcBreakdown {
|
||||
calculated: number;
|
||||
|
|
@ -9,9 +10,7 @@ export interface AcBreakdown {
|
|||
}
|
||||
|
||||
export function calculateAC(character: Character): AcBreakdown {
|
||||
const dexMod = getModifier(
|
||||
character.stats.find((s) => s.stat_name === "DEX")?.value ?? 10,
|
||||
);
|
||||
const dexMod = getModifier(getEffectiveStat(character, "DEX"));
|
||||
|
||||
let base = 10 + dexMod;
|
||||
let source = "Unarmored";
|
||||
|
|
@ -36,6 +35,13 @@ export function calculateAC(character: Character): AcBreakdown {
|
|||
source += " + " + shield.name;
|
||||
}
|
||||
|
||||
// Talent AC bonuses (e.g. Shield of Faith)
|
||||
const talentAcBonus = getTalentAcBonus(character);
|
||||
if (talentAcBonus > 0) {
|
||||
base += talentAcBonus;
|
||||
source += " + talents";
|
||||
}
|
||||
|
||||
const override = (character.overrides?.ac as number | undefined) ?? null;
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import type { Character, AttackLine } from "../types";
|
||||
import { getModifier, formatModifier } from "./modifiers";
|
||||
import {
|
||||
getEffectiveStat,
|
||||
getTalentAttackBonus,
|
||||
getTalentDamageBonus,
|
||||
} from "./talent-effects";
|
||||
|
||||
export function generateAttacks(character: Character): AttackLine[] {
|
||||
const strMod = getModifier(
|
||||
character.stats.find((s) => s.stat_name === "STR")?.value ?? 10,
|
||||
);
|
||||
const dexMod = getModifier(
|
||||
character.stats.find((s) => s.stat_name === "DEX")?.value ?? 10,
|
||||
);
|
||||
const strMod = getModifier(getEffectiveStat(character, "STR"));
|
||||
const dexMod = getModifier(getEffectiveStat(character, "DEX"));
|
||||
const talentAttackBonus = getTalentAttackBonus(character);
|
||||
const talentDamageBonus = getTalentDamageBonus(character);
|
||||
|
||||
const attacks: AttackLine[] = [];
|
||||
|
||||
|
|
@ -15,7 +18,7 @@ export function generateAttacks(character: Character): AttackLine[] {
|
|||
if (gear.type !== "weapon") continue;
|
||||
|
||||
const effects = gear.effects;
|
||||
const damage = (effects.damage as string) || "1d4";
|
||||
let damage = (effects.damage as string) || "1d4";
|
||||
const tags: string[] = [];
|
||||
|
||||
let mod: number;
|
||||
|
|
@ -28,8 +31,14 @@ export function generateAttacks(character: Character): AttackLine[] {
|
|||
mod = strMod;
|
||||
}
|
||||
|
||||
const bonus = (effects.bonus as number) || 0;
|
||||
mod += bonus;
|
||||
// Gear-specific bonus
|
||||
const gearBonus = (effects.bonus as number) || 0;
|
||||
mod += gearBonus + talentAttackBonus;
|
||||
|
||||
// Talent damage bonus — append to damage expression
|
||||
if (talentDamageBonus > 0) {
|
||||
damage = `${damage}+${talentDamageBonus}`;
|
||||
}
|
||||
|
||||
if (effects.two_handed) tags.push("2H");
|
||||
if (effects.thrown) tags.push("T");
|
||||
|
|
|
|||
86
client/src/utils/talent-effects.ts
Normal file
86
client/src/utils/talent-effects.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import type { Character } from "../types";
|
||||
|
||||
/**
|
||||
* Get the effective stat value after talent bonuses are applied.
|
||||
* e.g. if character has base STR 14 and a talent with { stat_bonus: 'STR', stat_bonus_value: 2 },
|
||||
* this returns 16.
|
||||
*/
|
||||
export function getEffectiveStat(
|
||||
character: Character,
|
||||
statName: string,
|
||||
): number {
|
||||
const base =
|
||||
character.stats.find((s) => s.stat_name === statName)?.value ?? 10;
|
||||
let bonus = 0;
|
||||
for (const talent of character.talents) {
|
||||
if (talent.effect.stat_bonus === statName) {
|
||||
bonus += (talent.effect.stat_bonus_value as number) || 0;
|
||||
}
|
||||
}
|
||||
return base + bonus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total AC bonus from talents (e.g. Shield of Faith +1 AC).
|
||||
*/
|
||||
export function getTalentAcBonus(character: Character): number {
|
||||
let bonus = 0;
|
||||
for (const talent of character.talents) {
|
||||
if (typeof talent.effect.ac_bonus === "number") {
|
||||
bonus += talent.effect.ac_bonus;
|
||||
}
|
||||
}
|
||||
return bonus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total attack bonus from talents (e.g. Weapon Mastery +1).
|
||||
*/
|
||||
export function getTalentAttackBonus(character: Character): number {
|
||||
let bonus = 0;
|
||||
for (const talent of character.talents) {
|
||||
if (typeof talent.effect.attack_bonus === "number") {
|
||||
bonus += talent.effect.attack_bonus;
|
||||
}
|
||||
}
|
||||
return bonus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total damage bonus from talents (e.g. Weapon Mastery +1).
|
||||
*/
|
||||
export function getTalentDamageBonus(character: Character): number {
|
||||
let bonus = 0;
|
||||
for (const talent of character.talents) {
|
||||
if (typeof talent.effect.damage_bonus === "number") {
|
||||
bonus += talent.effect.damage_bonus;
|
||||
}
|
||||
}
|
||||
return bonus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extra gear slots from talents (e.g. Hauler +2).
|
||||
*/
|
||||
export function getTalentGearSlotsBonus(character: Character): number {
|
||||
let bonus = 0;
|
||||
for (const talent of character.talents) {
|
||||
if (typeof talent.effect.gear_slots_bonus === "number") {
|
||||
bonus += talent.effect.gear_slots_bonus;
|
||||
}
|
||||
}
|
||||
return bonus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HP bonus from talents (e.g. Grit +2).
|
||||
*/
|
||||
export function getTalentHpBonus(character: Character): number {
|
||||
let bonus = 0;
|
||||
for (const talent of character.talents) {
|
||||
if (typeof talent.effect.hp_bonus === "number") {
|
||||
bonus += talent.effect.hp_bonus;
|
||||
}
|
||||
}
|
||||
return bonus;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue