V1.30.0: class features that change a number are worked out on the sheet
- Unarmored Defense (barbarian, monk), Draconic Resilience, Fast Movement, Unarmored Movement, Aura of Protection, Diamond Soul, Slippery Mind and Jack of All Trades are counted, by level in the class that gives them - Armor and shields are items marked with what kind they are, so the sheet knows when a character is unarmored or in heavy armor; the library's and the seeds' are marked - The sheet lists the class features in force; a speed the DM set is final - A cantrip can be cast with every spell slot spent (tested end to end) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "1.29.0",
|
||||
"version": "1.30.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient, type QueryClient } from '@tansta
|
||||
import { apiClient } from './client';
|
||||
import type {
|
||||
Ability,
|
||||
ArmorType,
|
||||
ActionPatch,
|
||||
ActionSource,
|
||||
BuildOptions,
|
||||
@@ -652,6 +653,7 @@ export function usePatchPoolEntry(campaignId: string, pool: Pool) {
|
||||
saveAbility?: Ability | null;
|
||||
halfOnSave?: boolean;
|
||||
locationId?: string | null;
|
||||
armorType?: ArmorType | null; // an item: what kind of armor it is, if any
|
||||
}) =>
|
||||
(await apiClient.patch(`/campaigns/${campaignId}/${pool}/${id}`, data)).data,
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -384,6 +384,7 @@ export interface CharacterSheet {
|
||||
spellSlots: SpellSlots | null; // null for a character with no spell slots
|
||||
resources: ClassResource[]; // the class counters, with how many of each are left
|
||||
gear: ActiveGear;
|
||||
features: FeatureInForce[]; // class features already counted in the numbers above
|
||||
}
|
||||
|
||||
export interface Weapon {
|
||||
@@ -461,6 +462,18 @@ export interface Item {
|
||||
isStartingOption: boolean;
|
||||
effects: GearEffect[]; // armor is an item with an AC effect
|
||||
effectNote: string | null;
|
||||
armorType: ArmorType | null; // set on armor and shields: what kind it is
|
||||
}
|
||||
|
||||
// What kind of armor an item is. The sheet goes by what is equipped for the
|
||||
// class features that depend on it (Unarmored Defense, a monk's speed...).
|
||||
export type ArmorType = 'LIGHT' | 'MEDIUM' | 'HEAVY' | 'SHIELD';
|
||||
|
||||
// A class feature counted in the sheet's numbers, in a line.
|
||||
export interface FeatureInForce {
|
||||
name: string;
|
||||
source: string; // the class or subclass that gives it
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface InventoryItem {
|
||||
|
||||
@@ -16,7 +16,7 @@ const STAT_HELP: Record<string, string> = {
|
||||
SKILL_BONUS: 'Skill Bonus — added when you roll to use a skill. The governing ability modifier, plus your proficiency bonus if you are proficient (doubled if you have expertise).',
|
||||
EXPERTISE: 'Expertise — doubles your proficiency bonus for this skill, on top of being proficient.',
|
||||
ABILITY_MODIFIER: 'Ability Modifier — the number added to rolls that use this ability. floor((score − 10) / 2).',
|
||||
ARMOR_BONUS: 'Extra AC — a number the DM sets, added to AC on top of 10 + Dex modifier and whatever equipped gear gives. Armor itself is an equipped item.',
|
||||
ARMOR_BONUS: 'Extra AC — a number the DM sets, added to AC on top of 10 + Dex modifier and whatever equipped gear gives. Armor itself is an equipped item. Unarmored Defense (barbarian, monk) and a draconic sorcerer’s scales are worked out for you: do not add them here.',
|
||||
SPEED: 'Speed — how far you can move on your turn, in feet. Set by your species, unless overridden.',
|
||||
SPELL_SAVE_DC: 'Spell Save DC — the difficulty a target must beat to resist your spell. 8 + proficiency bonus + your spellcasting ability modifier.',
|
||||
SPELL_ATTACK_BONUS: 'Spell Attack Bonus — added when you make a spell attack roll. Proficiency bonus + your spellcasting ability modifier.',
|
||||
|
||||
@@ -9,7 +9,7 @@ import { extractErrorMessage, fmtMod, spellLevelLabel, spellRequirementLabel } f
|
||||
import { abilityLine, rateLabel, type EntryKind, type EntryRef } from '../../lib/campaignEntries';
|
||||
import { Field, MonsterForm, ScalingSelect, SpellDamageSelect, SpellLandingSelect, StartLevelSelect } from './parts';
|
||||
import { landingValue } from '../../lib/campaignEntries';
|
||||
import type { GearEffect, Item, Monster, Npc, Spell, Weapon } from '../../api/types';
|
||||
import type { GearEffect, Item, Monster, Npc, Spell, Weapon, ArmorType } from '../../api/types';
|
||||
|
||||
const KIND_NOUNS: Record<EntryKind, string> = { weapon: 'weapon', item: 'item', spell: 'spell', monster: 'monster', npc: 'NPC' };
|
||||
const ATTACK_ABILITY_LABELS: Record<Weapon['attackAbility'], string> = { STR: 'STR', DEX: 'DEX', FINESSE: 'Finesse (the better of STR and DEX)' };
|
||||
@@ -90,6 +90,27 @@ function GearDetail({ campaignId, pool, entry, added, onClose }: { added: ReactN
|
||||
Starting option: a player may pick it while building a character
|
||||
</label>
|
||||
|
||||
{!weapon && 'armorType' in entry && (
|
||||
<div className="mt-md">
|
||||
<Field label="Armor">
|
||||
{(id) => (
|
||||
<select id={id} value={entry.armorType ?? ''} disabled={patch.isPending} onChange={(e) => patch.mutate({ id: entry.id, armorType: (e.target.value || null) as ArmorType | null }, { onError })}>
|
||||
<option value="">Not armor</option>
|
||||
<option value="LIGHT">Light armor</option>
|
||||
<option value="MEDIUM">Medium armor</option>
|
||||
<option value="HEAVY">Heavy armor</option>
|
||||
<option value="SHIELD">Shield</option>
|
||||
</select>
|
||||
)}
|
||||
</Field>
|
||||
<p className="combatant-sub mt-sm">
|
||||
Its AC is still set below, as +AC. This says what kind it is, for the class features that depend on what is worn: a barbarian’s and a
|
||||
monk’s Unarmored Defense and a draconic sorcerer’s scales stop while armor is equipped, a monk’s also behind a shield, and a
|
||||
barbarian’s extra speed stops in heavy armor.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="section-label mt-md">While equipped</div>
|
||||
<p className="combatant-sub" style={{ marginTop: 0 }}>
|
||||
These count for a character only while it is equipped and they are high enough level to use it. Armor is an item with
|
||||
|
||||
@@ -259,6 +259,7 @@ export function ItemsSection({ campaignId, entries, onOpen, formRequested, onFor
|
||||
isStartingOption: fd.get('isStartingOption') === 'on',
|
||||
effects: [],
|
||||
effectNote: null,
|
||||
armorType: null,
|
||||
});
|
||||
close();
|
||||
onOpen({ kind: 'item', id: item.id, isNew: true });
|
||||
|
||||
@@ -1589,6 +1589,8 @@ function BuildPreview({
|
||||
for (const ability of all) if (!abilityList.some((a) => a.name === ability.name)) abilityList.push(ability);
|
||||
}
|
||||
const wisMod = abilityFinals.WIS !== undefined ? abilityModifier(abilityFinals.WIS) : null;
|
||||
// Unarmored Defense, as the sheet will count it: a new character wears nothing yet.
|
||||
const unarmoredDefense = Math.max(0, characterClass === 'BARBARIAN' ? (conMod ?? 0) : characterClass === 'MONK' ? (wisMod ?? 0) : 0);
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
@@ -1651,7 +1653,7 @@ function BuildPreview({
|
||||
<span className="stat-label">
|
||||
AC <InfoTip term="AC" />
|
||||
</span>
|
||||
<div className="stat-value">{dexMod !== null ? armorClass(dexMod, armorBonus) : '—'}</div>
|
||||
<div className="stat-value">{dexMod !== null ? armorClass(dexMod, armorBonus) + unarmoredDefense : '—'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="stat-label">
|
||||
|
||||
@@ -682,6 +682,27 @@ export function CharacterSheetPage() {
|
||||
<p className="subtitle mt-sm">📍 Currently at: <strong>{character.currentLocation.name}</strong></p>
|
||||
)}
|
||||
|
||||
{sheet.features.length > 0 && (
|
||||
<div className="card mt-md">
|
||||
<div className="card-title">From Class Features</div>
|
||||
{sheet.features.map((feature) => (
|
||||
<div key={`${feature.source}:${feature.name}:${feature.text}`} className="detail-row">
|
||||
<span>
|
||||
<strong>{feature.name}</strong>
|
||||
<span className="combatant-sub" style={{ marginLeft: 6 }}>
|
||||
{feature.source}
|
||||
</span>
|
||||
</span>
|
||||
<span className="combatant-sub">{feature.text}</span>
|
||||
</div>
|
||||
))}
|
||||
<p className="subtitle" style={{ margin: '0.4rem 0 0' }}>
|
||||
Already counted in every number on this sheet: nothing to add by hand. What is worn decides some of them (armor and shields are
|
||||
items marked as such); a speed set by the DM is used as it is.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(sheet.gear.effects.length > 0 || sheet.gear.notes.length > 0) && (
|
||||
<div className="card mt-md">
|
||||
<div className="card-title">From Equipped Gear</div>
|
||||
|
||||
Reference in New Issue
Block a user