Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions apps/sidekick/components/character-config-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface CharacterConfigDialogProps {
}

export function CharacterConfigDialog({ onClose }: CharacterConfigDialogProps) {
const { character, updateCharacter } = useCharacterService();
const { character, updateCharacter, getMaxHp } = useCharacterService();

// Don't render if character is null
if (!character) {
Expand All @@ -46,16 +46,19 @@ export function CharacterConfigDialog({ onClose }: CharacterConfigDialogProps) {
await updateCharacter(updatedCharacter);
};

const hpBonus = getMaxHp() - character.hitPoints.max;

const updateMaxHP = async (value: string) => {
const numValue = parseInt(value) || 1;
const maxHP = Math.max(1, numValue);
const effectiveMax = Math.max(1, numValue);
const baseMax = effectiveMax - hpBonus;

const updatedCharacter = {
...character,
hitPoints: {
...character.hitPoints,
max: maxHP,
current: Math.min(character.hitPoints.current, maxHP),
max: baseMax,
current: Math.min(character.hitPoints.current, effectiveMax),
},
};
await updateCharacter(updatedCharacter);
Expand Down Expand Up @@ -118,6 +121,7 @@ export function CharacterConfigDialog({ onClose }: CharacterConfigDialogProps) {
{/* Basic Settings */}
<BasicSettingsSection
character={character}
effectiveMaxHp={getMaxHp()}
updateMaxWounds={updateMaxWounds}
updateMaxHP={updateMaxHP}
updateInitiativeModifier={updateInitiativeModifier}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

import { Character } from "@/lib/schemas/character";

import { Input } from "../ui/input";
import { Label } from "../ui/label";
import { NumericInput } from "../ui/numeric-input";

interface BasicSettingsSectionProps {
character: Character;
effectiveMaxHp: number;
updateMaxWounds: (value: string) => Promise<void>;
updateMaxHP: (value: string) => Promise<void>;
updateInitiativeModifier: (value: string) => Promise<void>;
Expand All @@ -16,6 +17,7 @@ interface BasicSettingsSectionProps {

export function BasicSettingsSection({
character,
effectiveMaxHp,
updateMaxWounds,
updateMaxHP,
updateInitiativeModifier,
Expand All @@ -30,13 +32,12 @@ export function BasicSettingsSection({
Maximum Wounds
</Label>
<div className="space-y-1">
<Input
<NumericInput
id="max-wounds"
type="number"
min="1"
max="20"
min={1}
max={20}
value={character.config.maxWounds}
onChange={(e) => updateMaxWounds(e.target.value)}
onChange={(v) => updateMaxWounds(v)}
className="w-full"
/>
<p className="text-xs text-muted-foreground">
Expand All @@ -51,18 +52,15 @@ export function BasicSettingsSection({
Maximum Hit Points
</Label>
<div className="space-y-1">
<Input
<NumericInput
id="max-hp"
type="number"
min="1"
max="1000"
value={character.hitPoints.max}
onChange={(e) => updateMaxHP(e.target.value)}
min={1}
max={1000}
value={effectiveMaxHp}
onChange={(v) => updateMaxHP(v)}
className="w-full"
/>
<p className="text-xs text-muted-foreground">
Base maximum hit points for the character.
</p>
<p className="text-xs text-muted-foreground">Maximum hit points for the character.</p>
</div>
</div>

Expand All @@ -72,13 +70,12 @@ export function BasicSettingsSection({
Initiative Modifier
</Label>
<div className="space-y-1">
<Input
<NumericInput
id="initiative-modifier"
type="number"
min="-10"
max="10"
min={-10}
max={10}
value={character._initiative.modifier}
onChange={(e) => updateInitiativeModifier(e.target.value)}
onChange={(v) => updateInitiativeModifier(v)}
className="w-full"
/>
<p className="text-xs text-muted-foreground">
Expand All @@ -95,13 +92,12 @@ export function BasicSettingsSection({
Base Inventory Size
</Label>
<div className="space-y-1">
<Input
<NumericInput
id="max-inventory"
type="number"
min="1"
max="100"
min={1}
max={100}
value={character.config.maxInventorySize}
onChange={(e) => updateMaxInventorySize(e.target.value)}
onChange={(v) => updateMaxInventorySize(v)}
className="w-full"
/>
<p className="text-xs text-muted-foreground">
Expand All @@ -122,13 +118,12 @@ export function BasicSettingsSection({
Starting Skill Points
</Label>
<div className="space-y-1">
<Input
<NumericInput
id="starting-points"
type="number"
min="1"
max="20"
min={1}
max={20}
value={character.config.skillPoints.startingPoints}
onChange={(e) => updateSkillPointsConfig("startingPoints", e.target.value)}
onChange={(v) => updateSkillPointsConfig("startingPoints", v)}
className="w-full"
/>
<p className="text-xs text-muted-foreground">Skill points available at level 1.</p>
Expand All @@ -141,13 +136,12 @@ export function BasicSettingsSection({
Points Per Level
</Label>
<div className="space-y-1">
<Input
<NumericInput
id="points-per-level"
type="number"
min="0"
max="10"
min={0}
max={10}
value={character.config.skillPoints.pointsPerLevel}
onChange={(e) => updateSkillPointsConfig("pointsPerLevel", e.target.value)}
onChange={(v) => updateSkillPointsConfig("pointsPerLevel", v)}
className="w-full"
/>
<p className="text-xs text-muted-foreground">
Expand Down
11 changes: 11 additions & 0 deletions apps/sidekick/components/feature-traits-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,17 @@ const formatEffectDescription = (effect: FeatureTrait): string => {
const formatted = formatValue(effect.statBonus.hitDiceBonus);
if (formatted) bonuses.push(`${formatted} Hit Dice`);
}
if (effect.statBonus.hitDieSizeOverride) {
bonuses.push(`Hit Die → d${effect.statBonus.hitDieSizeOverride}`);
}
if (effect.statBonus.hitDieSizeStep) {
const steps = effect.statBonus.hitDieSizeStep;
bonuses.push(steps > 0 ? `Hit Die +${steps} step` : `Hit Die ${steps} step`);
}
if (effect.statBonus.maxHpBonus) {
const formatted = formatValue(effect.statBonus.maxHpBonus);
if (formatted) bonuses.push(`${formatted} Max HP`);
}

// Resources
if (effect.statBonus.resourceMaxBonuses) {
Expand Down
5 changes: 3 additions & 2 deletions apps/sidekick/components/level-up-guide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const STEPS = [
];

export function LevelUpGuide({ open, onOpenChange }: LevelUpGuideProps) {
const { character, updateCharacter } = useCharacterService();
const { character, updateCharacter, getHitDice, getMaxHp } = useCharacterService();
const { showError } = useToastService();
const [currentStep, setCurrentStep] = useState(0);
const [levelUpData, setLevelUpData] = useState<LevelUpData>({
Expand All @@ -71,7 +71,7 @@ export function LevelUpGuide({ open, onOpenChange }: LevelUpGuideProps) {
// Early return if no character loaded (after all hooks)
if (!character) return null;

const hitDieSize = character._hitDice.size;
const hitDieSize = getHitDice().size;

const handleNext = () => {
if (currentStep === 0) {
Expand Down Expand Up @@ -289,6 +289,7 @@ export function LevelUpGuide({ open, onOpenChange }: LevelUpGuideProps) {
character={character}
levelUpData={levelUpData}
hitDieSize={`d${hitDieSize}`}
currentMaxHp={getMaxHp()}
onHpChange={(newHp) => setLevelUpData((prev) => ({ ...prev, newMaxHp: newHp }))}
onReroll={rollHitPoints}
/>
Expand Down
4 changes: 3 additions & 1 deletion apps/sidekick/components/level-up-guide/hit-points-step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface HitPointsStepProps {
character: Character;
levelUpData: LevelUpData;
hitDieSize: string;
currentMaxHp: number;
onHpChange: (hp: number) => void;
onReroll: () => void;
}
Expand All @@ -37,6 +38,7 @@ export function HitPointsStep({
character,
levelUpData,
hitDieSize,
currentMaxHp,
onHpChange,
onReroll,
}: HitPointsStepProps) {
Expand All @@ -54,7 +56,7 @@ export function HitPointsStep({
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Current HP</Label>
<div className="text-2xl font-bold">{character.hitPoints.max}</div>
<div className="text-2xl font-bold">{currentMaxHp}</div>
</div>
<div>
<Label htmlFor="new-hp">New HP</Label>
Expand Down
11 changes: 7 additions & 4 deletions apps/sidekick/components/sections/combat-summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ import { GeneralActionsRow } from "./general-actions-row";

// Health Bar Subcomponent
function HealthBar() {
const { character } = useCharacterService();
const { character, getMaxHp } = useCharacterService();

// All hooks called first, then safety check
if (!character) return null;

const { current, max, temporary } = character.hitPoints;
const max = getMaxHp();
const { current, temporary } = character.hitPoints;
const healthPercentage = (current / max) * 100;

const getHealthBarColor = () => {
Expand Down Expand Up @@ -352,13 +353,15 @@ function HPActionDialog({

// Quick Actions Bar Subcomponent
function QuickActionsBar() {
const { character, applyDamage, applyHealing, applyTemporaryHP } = useCharacterService();
const { character, applyDamage, applyHealing, applyTemporaryHP, getMaxHp } =
useCharacterService();
const [openDialog, setOpenDialog] = useState<ActionType | null>(null);

// All hooks called first, then safety check
if (!character) return null;

const { current: currentHp, max: maxHp } = character.hitPoints;
const maxHp = getMaxHp();
const currentHp = character.hitPoints.current;

const handleApplyAction = (actionType: ActionType, amount: number) => {
switch (actionType) {
Expand Down
36 changes: 26 additions & 10 deletions apps/sidekick/components/sections/hit-dice-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,22 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".

export function HitDiceSection() {
// Get everything we need from service hooks
const { character, performSafeRest, performCatchBreath, performMakeCamp, updateCharacterFields } =
useCharacterService();
const {
character,
performSafeRest,
performCatchBreath,
performMakeCamp,
updateCharacterFields,
getHitDice,
} = useCharacterService();
const { uiState, updateCollapsibleState } = useUIStateService();

const computedHitDice = getHitDice();

const [isEditing, setIsEditing] = useState(false);
const [editValues, setEditValues] = useState({
level: character?.level || 1,
hitDieSize: character?._hitDice.size || 6,
hitDieSize: computedHitDice?.size || 6,
currentHitDice: character?._hitDice.current || character?.level || 1,
});

Expand All @@ -35,10 +43,18 @@ export function HitDiceSection() {
const onToggle = (isOpen: boolean) => updateCollapsibleState("hitDice", isOpen);

const handleSave = () => {
// If the user didn't change the die size, preserve the current override state.
// If they changed it, set it as a manual override.
const sizeOverride =
editValues.hitDieSize === computedHitDice.size
? character._hitDice.sizeOverride
: (editValues.hitDieSize as typeof character._hitDice.size);

const updatedCharacter = {
level: editValues.level,
_hitDice: {
size: editValues.hitDieSize,
...character._hitDice,
sizeOverride,
current: editValues.currentHitDice,
max: editValues.level, // Max hit dice always equals level
},
Expand All @@ -50,7 +66,7 @@ export function HitDiceSection() {
const handleCancel = () => {
setEditValues({
level: character.level,
hitDieSize: character._hitDice.size,
hitDieSize: computedHitDice.size,
currentHitDice: character._hitDice.current,
});
setIsEditing(false);
Expand Down Expand Up @@ -86,7 +102,7 @@ export function HitDiceSection() {
<span>
{character._hitDice.current}/{character._hitDice.max}
</span>
<span className="text-sm text-muted-foreground">d{character._hitDice.size}</span>
<span className="text-sm text-muted-foreground">d{computedHitDice.size}</span>
</div>
{isOpen ? (
<ChevronDown className="w-4 h-4" />
Expand Down Expand Up @@ -176,7 +192,7 @@ export function HitDiceSection() {
</div>

<div className="text-center p-4 bg-muted/50 rounded-lg">
<div className="text-2xl font-bold">d{character._hitDice.size}</div>
<div className="text-2xl font-bold">d{computedHitDice.size}</div>
<div className="text-sm text-muted-foreground">Hit Die</div>
</div>

Expand All @@ -193,7 +209,7 @@ export function HitDiceSection() {

<div className="space-y-3">
<div className="text-center text-sm font-medium text-muted-foreground">
Field Rest Options (d{character._hitDice.size} + STR{" "}
Field Rest Options (d{computedHitDice.size} + STR{" "}
{character._attributes.strength >= 0 ? "+" : ""}
{character._attributes.strength})
</div>
Expand All @@ -218,7 +234,7 @@ export function HitDiceSection() {
>
<Heart className="w-4 h-4 mr-2" />
Make Camp
<div className="text-xs ml-2">({character._hitDice.size} + STR)</div>
<div className="text-xs ml-2">({computedHitDice.size} + STR)</div>
</Button>
</div>

Expand Down Expand Up @@ -249,7 +265,7 @@ export function HitDiceSection() {
onClick={() => {
setEditValues({
level: character.level,
hitDieSize: character._hitDice.size,
hitDieSize: computedHitDice.size,
currentHitDice: character._hitDice.current,
});
setIsEditing(true);
Expand Down
Loading