diff --git a/.vscode/settings.json b/.vscode/settings.json index e736c4dd..015b22af 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,5 +28,12 @@ "statusBar.debuggingForeground": "#e7e7e7" }, "peacock.color": "#643312", - "cSpell.words": ["ageorgedev", "Halfling", "Halflings", "Khorvaire"] + "cSpell.words": [ + "ageorgedev", + "Halfling", + "Halflings", + "Khorvaire", + "spellcasting", + "Warforged" + ] } diff --git a/apps/game-tools/src/data/dnd-characters/common/common-feats.ts b/apps/game-tools/src/data/dnd-characters/common/common-feats.ts index 61f20ee9..fd7ec461 100644 --- a/apps/game-tools/src/data/dnd-characters/common/common-feats.ts +++ b/apps/game-tools/src/data/dnd-characters/common/common-feats.ts @@ -17,3 +17,9 @@ export const SAVAGE_ATTACKER: Feature = { description: "Once per turn, you may roll a weapon's damage dice twice and use either rolls.", }; + +export const SKILLED: Feature = { + name: 'Skilled', + description: + 'You have additional proficiency in any combination of 3 skills or tools (already considered in this sheet)', +}; diff --git a/apps/game-tools/src/data/dnd-characters/common/index.ts b/apps/game-tools/src/data/dnd-characters/common/index.ts index 6417e2b9..15083694 100644 --- a/apps/game-tools/src/data/dnd-characters/common/index.ts +++ b/apps/game-tools/src/data/dnd-characters/common/index.ts @@ -1,3 +1,4 @@ export * from './common-class-features'; export * from './common-feats'; export * from './common-species-traits'; +export * from './species-traits'; diff --git a/apps/game-tools/src/data/dnd-characters/common/species-traits.ts b/apps/game-tools/src/data/dnd-characters/common/species-traits.ts new file mode 100644 index 00000000..edb91445 --- /dev/null +++ b/apps/game-tools/src/data/dnd-characters/common/species-traits.ts @@ -0,0 +1,29 @@ +import type { Feature } from '@ageorgedev/dnd-character-sheet'; + +export const WarforgedTraits: Feature[] = [ + { + name: 'Construct resilience', + description: + 'You have Resistance to Poison damage. You also have Advantage on saving throws you make to avoid or end the Poisoned condition.', + }, + { + name: 'Integrated Protection', + description: + "You have +1 AC (already considered in this sheet). Your armour can't be removed against your will", + }, + { + name: "Sentry's Rest", + description: + "You don't need to sleep, and magic can't put you to sleep. You can finish a Long Rest in 6hrs if you spend that time inactive and motionless. You appear inert but are still conscious.", + }, + { + name: 'Specialized Design', + description: + 'You gain one skill proficiency and one tool proficiency of your choice (already considered in this sheet)', + }, + { + name: 'Tireless', + description: + "You don't need to consume water, food, and don't need to breathe", + }, +]; diff --git a/apps/game-tools/src/data/dnd-characters/index.ts b/apps/game-tools/src/data/dnd-characters/index.ts index 546ff3c4..4431808f 100644 --- a/apps/game-tools/src/data/dnd-characters/index.ts +++ b/apps/game-tools/src/data/dnd-characters/index.ts @@ -10,6 +10,8 @@ import { ElnorinData } from './elnorin-lunarrest/elnorin-lunarrest'; import ElnorinSpellBook from './elnorin-lunarrest/elnorrin-spellbook.pdf?url'; import { GonvarData } from './gonvar-feathertide/gonvar-feathertide'; import { OmarinData } from './omarin-kenate/omarin-kenate'; +import { RustyData } from './rusty/rusty'; +import RustySpellBook from './rusty/rusty-spellbook.pdf'; import { SaoraData } from './saora-embervale/saora-embervale'; import SaoraSpellBook from './saora-embervale/saora-spellbook.pdf?url'; import { SplitterData } from './splitter/splitter'; @@ -46,7 +48,14 @@ const characters: BasePack[] = [ { data: ElnorinData, spellBook: ElnorinSpellBook }, { data: GonvarData }, { data: ClawData, spellBook: ClawSpellBook }, - { data: OmarinData }, + { + data: OmarinData, + visualAdjustments: { + classFeaturesFontSize: 'small', + speciesAndFeatsFontSize: 'small', + speciesAndFeatsCombinedPanel: true, + }, + }, { data: SplitterData, visualAdjustments: { @@ -64,6 +73,11 @@ const characters: BasePack[] = [ notesRows: 0, }, }, + { + data: RustyData, + spellBook: RustySpellBook, + // TODO: alternate title for spellbook + }, ]; const processAndGroup = compose( diff --git a/apps/game-tools/src/data/dnd-characters/omarin-kenate/omarin-kenate.ts b/apps/game-tools/src/data/dnd-characters/omarin-kenate/omarin-kenate.ts index 71ba0e0b..b14f131e 100644 --- a/apps/game-tools/src/data/dnd-characters/omarin-kenate/omarin-kenate.ts +++ b/apps/game-tools/src/data/dnd-characters/omarin-kenate/omarin-kenate.ts @@ -102,7 +102,14 @@ export const OmarinData: Character = { notes: 'Ammo (80/20, Bolt), Loading, 2-handed', }, ], - equipment: ['Bedroll, Backpack, Lantern', 'Flasks of oil (x4)'], + equipment: [ + "Backpack, Bedroll, Iron Pot, Shovel, Traveller's clothes, Rope, Tinderbox, Waterskin", + "Carpenter's Tools, Tinker's Tools", + 'Oil flasks = 4', + 'Torches = 10', + 'Ration (days) = 10', + "Healer's Kit charges = 10", + ], armorProficiencies: [ ArmorProficiency.LightArmor, ArmorProficiency.MediumArmor, @@ -156,7 +163,6 @@ export const OmarinData: Character = { SECOND_WIND, TACTICAL_MIND, ACTION_SURGE, - // TODO: the actual manuevers COMBAT_SUPERIORITY, { name: 'Uncanny Metabolism', @@ -169,6 +175,27 @@ export const OmarinData: Character = { refresh: { kind: 'long-rest' }, }, }, + { + name: 'Pushing Attack', + cost: '1 Superiority dice', + description: ` + on Hit, add Superiority Dice to damage. If target size is Large or smaller, they make a Str. save (DC <%= abilitySaveDCs.DEX %>); on failure be pushed 15 ft directly away from you. + `, + }, + { + name: 'Goading Attack', + cost: '1 Superiority dice', + description: ` + on Hit, add Superiority Dice to damage. Target must succeed on Wis. save (DC <%= abilitySaveDCs.DEX %>); on failure, have Disadvantage on attack rolls against any target other than you, until the end of your next turn + `, + }, + { + name: 'Maneuvering Attack', + cost: '1 Superiority dice', + description: ` + on Hit, add Superiority Dice to damage. Choose a willing creature who can see or hear you; they can use their Reaction immediately to move half their speed without provoking Opportunity attacks. + `, + }, ], speciesTraits: [ darkvision('120ft'), @@ -208,9 +235,5 @@ export const OmarinData: Character = { numberOfCantrips: 0, numberOfPreparedSpells: 0, }, - - backstory: ` -

This is the story of Omarin

-

Early years Had a lot of fun

- `, + // TODO: work on the backstory and appearance }; diff --git a/apps/game-tools/src/data/dnd-characters/rusty/rusty-spellbook.pdf b/apps/game-tools/src/data/dnd-characters/rusty/rusty-spellbook.pdf new file mode 100644 index 00000000..497b80aa Binary files /dev/null and b/apps/game-tools/src/data/dnd-characters/rusty/rusty-spellbook.pdf differ diff --git a/apps/game-tools/src/data/dnd-characters/rusty/rusty.ts b/apps/game-tools/src/data/dnd-characters/rusty/rusty.ts new file mode 100644 index 00000000..48294780 --- /dev/null +++ b/apps/game-tools/src/data/dnd-characters/rusty/rusty.ts @@ -0,0 +1,175 @@ +import { + Ability, + type Character, + Skill, + SPELL, + withSpellMods, +} from '@ageorgedev/dnd-character-sheet'; +import { SKILLED, WarforgedTraits } from '../common'; + +export const RustyData: Character = { + name: 'Rusty', + species: 'Warforged', + background: 'Archaeologist', + classes: [ + { + name: 'Artificer', + level: 3, + subclass: 'Artillerist', + }, + ], + abilities: { + [Ability.Strength]: 8, + [Ability.Dexterity]: 16, + [Ability.Constitution]: 14, + [Ability.Intelligence]: 17, + [Ability.Wisdom]: 10, + [Ability.Charisma]: 8, + }, + savingThrowProficiencies: [Ability.Constitution, Ability.Intelligence], + skillProficiencies: [ + Skill.Athletics, + Skill.Acrobatics, + Skill.Arcana, + Skill.History, + Skill.Investigation, + Skill.Perception, + Skill.Survival, + Skill.Intimidation, + ], + skillExpertise: [], + baseArmorClass: 16, + speed: 30, + hitPoints: { + maximum: 27, + }, + armorProficiencies: ['Light Armor', 'Medium Armor', 'Shield'], + weaponProficiencies: [], + toolProficiencies: [ + "Cartographer's Tools", + "Thieves' Tools", + "Tinker's Tools", + "Smith's Tools", + "Navigator's Tools", + "Woodcarver's Tools", + ], + languages: ['Common', 'Elvish'], + attacks: [ + { + kind: 'weapon', + name: 'Heavy Crossbow', + ability: Ability.Dexterity, + damage: [{ dice: '1d10', type: 'Piercing' }], + notes: 'Ammunition (80/320 bolt), 2-handed, Loading', + }, + { + kind: 'weapon', + name: 'Dagger', + ability: Ability.Dexterity, + damage: [{ dice: '1d4', type: 'Piercing' }], + notes: 'Finesse, Light, Thrown (range 20/60)', + }, + { + kind: 'spell-with-attack', + name: 'Fire Bolt', + damage: [{ dice: '1d10', type: 'Fire' }], + notes: '120 ft range', + }, + ], + features: [ + { + name: "Tinker's Magic", + castingTime: 'Action', + description: + "While holding Tinker's Tools, you can create one item in an unoccupied space within 5 ft of yourself. Choose the item from the Tinker's Magic List (find them in the next sheet). The items last until you finish a Long Rest", + resource: { + id: 'TinkerMagic', + name: "Tinker's Magic", + count: { + kind: 'fixed', + value: 2, + }, + refresh: { + kind: 'long-rest', + }, + }, + }, + { + name: 'Replicate Magic Item', + description: ` + You have learned arcane plans that you use to make magic items. + On finishing a Long Rest, you can create 1 or 2 different magic items from these plans. The items last until you die. You can only have 2 such created magic items at any given point, creating another one will make the oldest one vanish. + You know the following plans: +
    +
  1. Rope of Climbing
  2. +
  3. Goggles of Night
  4. +
  5. Wand of Secrets
  6. +
  7. Repeating Shot
  8. +
+ `, + }, + { + name: 'Wand Crafting', + description: + 'When you craft a magic Wand, the amount of time required to craft is halved', + }, + { + name: 'Eldritch Canon', + castingTime: 'Action', + description: ` + Once per Long Rest or spend spell slots for additional invocations. + Using Smith's Tools, or Woodcarver's Tools, you create a Small or Tiny Eldritch Canon in an unoccupied space on a horizontal surface within 5 ft. + (Cannon's stats in additional sheet). You determine its appearance and whether you carry it or not (and your choice of legs or wheels for it in that case). + It disappears if reduced to 0 Hit Points or after 1 hour, or if you decide early with an Action. You can have only one canon alive at a time. + `, + resource: { + name: 'Invoke cannon', + id: 'InvokeCannon', + count: { + kind: 'free-then-spell-slots', + numberOfFreeUses: 1, + }, + refresh: { + kind: 'long-rest', + }, + }, + }, + ], + speciesTraits: WarforgedTraits, + feats: [SKILLED], + spellcasting: { + ability: Ability.Intelligence, + // TODO: pull up spell change traits + slots: { 1: 3 }, + numberOfCantrips: 3, + numberOfPreparedSpells: 4, + spells: [ + SPELL.Mending, + SPELL.ShockingGrasp, + SPELL.FireBolt, + withSpellMods(SPELL.Shield, { alwaysPrepared: true }), + withSpellMods(SPELL.Thunderwave, { alwaysPrepared: true }), + SPELL.Alarm, + SPELL.Grease, + SPELL.FalseLife, + SPELL.FeatherFall, + ], + }, + appearance: ` + Warforged are mechanical beings built as weapons to fight in the Last War. + You comprise a blend of organic and inorganic materials. + Rootlike chords infused with alchemical fluids serve as your muscles, wrapped around a framework of steel, darkwood, or stone + `, + backstory: ` +

Archaeologist. You've made a lifelong study of the lost and fallen cultures of the past. Perhaps you have studied at Morgrave University or similar institutions. Or perhaps you have made it your mission to dig through the remains of the Last War

+ `, + equipment: [ + 'Backpack, Caltrops, Crowbar, Rope, Tinderbox', + "Navigator's Tools, Tinker's Tools, Woodcarver's Tools", + 'Studded Leather Armour', + 'Light Crossbow, Dagger', + 'Crossbow bolts = 20', + 'Oil flasks = 2', + 'Torches = 10', + ], +}; diff --git a/apps/game-tools/src/data/dnd-characters/saora-embervale/saora-embervale.ts b/apps/game-tools/src/data/dnd-characters/saora-embervale/saora-embervale.ts index 8be687f0..e7b6291a 100644 --- a/apps/game-tools/src/data/dnd-characters/saora-embervale/saora-embervale.ts +++ b/apps/game-tools/src/data/dnd-characters/saora-embervale/saora-embervale.ts @@ -7,7 +7,7 @@ import { SPELL, } from '@ageorgedev/dnd-character-sheet'; import { mapObjIndexed } from 'ramda'; -import { expertise } from '../common'; +import { expertise, SKILLED } from '../common'; export const SaoraData: Character = { name: 'Saora Embervale', @@ -166,13 +166,7 @@ export const SaoraData: Character = { 'You can take the Hide action when you are obscured by a creature larger than you.', }, ], - feats: [ - { - name: 'Skilled', - description: - 'You have additional proficiency in any combination of 3 skills or tools (already considered in this sheet)', - }, - ], + feats: [SKILLED], spellcasting: { ability: Ability.Charisma, slots: { diff --git a/apps/game-tools/src/data/dnd-characters/splitter/splitter.ts b/apps/game-tools/src/data/dnd-characters/splitter/splitter.ts index 037d1f52..8ea46562 100644 --- a/apps/game-tools/src/data/dnd-characters/splitter/splitter.ts +++ b/apps/game-tools/src/data/dnd-characters/splitter/splitter.ts @@ -3,7 +3,7 @@ import { type Character, Skill, } from '@ageorgedev/dnd-character-sheet'; -import { SAVAGE_ATTACKER, weaponMastery } from '../common'; +import { SAVAGE_ATTACKER, WarforgedTraits, weaponMastery } from '../common'; export const SplitterData: Character = { name: 'Splitter', @@ -130,33 +130,7 @@ export const SplitterData: Character = { 'If you do Reckless Attack while Rage is active, you do an extra 2d6 damage to the first target you hit on your turn with a Str. based attack.', }, ], - speciesTraits: [ - { - name: 'Construct resilience', - description: - 'You have Resistance to Poison damage. You also have Advantage on saving throws you make to avoid or end the Poisoned condition.', - }, - { - name: 'Integrated Protection', - description: - "You have +1 AC (already considered in this sheet). Your armour can't be removed against your will", - }, - { - name: "Sentry's Rest", - description: - "You don't need to sleep, and magic can't put you to sleep. You can finish a Long Rest in 6hrs if you spend that time inactive and motionless. You appear inert but are still conscious.", - }, - { - name: 'Specialised Design', - description: - 'You gain one skill proficiency and one tool proficiency of your choice (already considered in this sheet)', - }, - { - name: 'Tireless', - description: - "You don't need to consume water, food, and don't need to breathe", - }, - ], + speciesTraits: WarforgedTraits, feats: [SAVAGE_ATTACKER], appearance: 'Warforged are mechanical beings built as weapons to fight in the Last War. You comprise a blend of organic and inorganic materials. Rootlike chords infused with alchemical fluids serve as your muscles, wrapped around a framework of steel, darkwood or stone', diff --git a/apps/game-tools/src/routes/_public/dnd/characters/index.tsx b/apps/game-tools/src/routes/_public/dnd/characters/index.tsx index 6b02482f..455ddb9e 100644 --- a/apps/game-tools/src/routes/_public/dnd/characters/index.tsx +++ b/apps/game-tools/src/routes/_public/dnd/characters/index.tsx @@ -3,6 +3,7 @@ import { InterfaceLg, } from '@ageorgedev/design-system/typography/typography-components'; import { createFileRoute } from '@tanstack/react-router'; +import { Fragment } from 'react'; import { CharacterRosterCard } from '../../../../components/dnd/CharacterRosterCard'; import { AllMyCharactersInBrief } from '../../../../data/dnd-characters'; import { groupCharactersByLevel } from '../../../../lib/group-characters-by-level'; @@ -23,27 +24,28 @@ function RouteComponent() { D&D 5.5e Characters -
+
{levelGroups.map(({ level, characters }) => ( -
- + + Level {level} -
- {characters.map((c) => ( - - ))} -
-
+ {characters.map((c) => ( + + ))} + ))}
diff --git a/packages/dnd-character-sheet/src/components/Resources/Resources.tsx b/packages/dnd-character-sheet/src/components/Resources/Resources.tsx index 50572220..79a4b84d 100644 --- a/packages/dnd-character-sheet/src/components/Resources/Resources.tsx +++ b/packages/dnd-character-sheet/src/components/Resources/Resources.tsx @@ -7,6 +7,7 @@ import { PanelTitle } from '../layout/PanelTitle'; type Refresh = NonNullable['refresh']; +// TODO: figure out a way to show resources that can still be used by expending spell slots export function Resources() { const { resources } = useCharacter(); const trackable = resources.filter((r) => r.refresh.kind !== 'per-turn'); diff --git a/packages/dnd-character-sheet/src/components/SheetHeader/NameBlock.tsx b/packages/dnd-character-sheet/src/components/SheetHeader/NameBlock.tsx index a771d6a7..2b95de31 100644 --- a/packages/dnd-character-sheet/src/components/SheetHeader/NameBlock.tsx +++ b/packages/dnd-character-sheet/src/components/SheetHeader/NameBlock.tsx @@ -1,3 +1,4 @@ +import type { Character } from '../../lib/models'; import { useCharacter } from '../CharacterSheet'; import { LabelUnder } from '../layout/labels'; import { Panel } from '../layout/Panel'; @@ -12,19 +13,14 @@ export function NameBlock() { >

{character.name}

{character.background} - - {joinItems(character.classes, 'name')} - + {getClasses(character)} {character.species} - - {joinItems(character.classes, 'subclass')} - + {getSubclasses(character)}  ); } /** Smaller utils */ - export function NameField({ label, children, @@ -40,12 +36,18 @@ export function NameField({ ); } -export function joinItems< - T extends Record, - K extends keyof T, ->(items: T[], key: K) { - return items - .map((item) => item[key]) - .filter((value) => value != null) +// If multi-classing, give the levels of individual classes, else just show the current class +function getClasses(character: Character) { + if (character.classes.length < 2) { + return character.classes[0].name; + } else { + return character.classes.map((c) => `${c.name} ${c.level}`).join(' / '); + } +} + +function getSubclasses(character: Character) { + return character.classes + .map((c) => c.subclass) + .filter(Boolean) .join(' / '); } diff --git a/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellAbilityPanel.tsx b/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellAbilityPanel.tsx index f6f4c75f..0fec4d0a 100644 --- a/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellAbilityPanel.tsx +++ b/packages/dnd-character-sheet/src/components/SpellcastingBlock/SpellAbilityPanel.tsx @@ -28,14 +28,14 @@ export function SpellAbilityPanel(props: PanelProps) { - Spellcasting Save DC + Spell Save DC {derived.spellSaveDC} - Spellcasting Attack Bonus + Spell Attack Bonus {formatMod(derived.spellAttackBonus ?? 0)} diff --git a/packages/dnd-character-sheet/src/components/layout/Page.tsx b/packages/dnd-character-sheet/src/components/layout/Page.tsx index e3dda75e..f9f471f3 100644 --- a/packages/dnd-character-sheet/src/components/layout/Page.tsx +++ b/packages/dnd-character-sheet/src/components/layout/Page.tsx @@ -1,6 +1,8 @@ import { cn } from '@ageorgedev/toolbelt/cn'; import styles from './Page.module.css'; +// TODO: outer container can be used for more than this page only + export function Page({ children, className, diff --git a/packages/dnd-character-sheet/src/lib/calculate-resources.ts b/packages/dnd-character-sheet/src/lib/calculate-resources.ts index 67a87de1..9a5feb40 100644 --- a/packages/dnd-character-sheet/src/lib/calculate-resources.ts +++ b/packages/dnd-character-sheet/src/lib/calculate-resources.ts @@ -106,6 +106,11 @@ function resourceDigester(character: Character, stats: DerivedStats) { count = Math.max(mod * (config.multiplier ?? 1), config.min ?? 0); break; } + + case 'free-then-spell-slots': { + count = config.numberOfFreeUses; + break; + } } // Resolve die if present diff --git a/packages/dnd-character-sheet/src/lib/character-brief.ts b/packages/dnd-character-sheet/src/lib/character-brief.ts index ec9bb9a7..adbbb919 100644 --- a/packages/dnd-character-sheet/src/lib/character-brief.ts +++ b/packages/dnd-character-sheet/src/lib/character-brief.ts @@ -3,9 +3,13 @@ import type { Character } from './models'; export function getCharacterBrief(character: Character) { const level = character.classes.reduce((acc, { level }) => acc + level, 0); const classes = character.classes.map(({ name }) => name); + + // Need a think about this. Maybe character overrides for special cases + // currently primary class is the one with the highest class level const primaryClass = character.classes.reduce((primary, current) => current.level > primary.level ? current : primary ).name; + let description = character.customDescription; if (!description) { const subclasses = character.classes diff --git a/packages/dnd-character-sheet/src/lib/models/character.ts b/packages/dnd-character-sheet/src/lib/models/character.ts index 801ad00d..70ba0d7f 100644 --- a/packages/dnd-character-sheet/src/lib/models/character.ts +++ b/packages/dnd-character-sheet/src/lib/models/character.ts @@ -13,6 +13,9 @@ export interface Character { species: string; background: string; customDescription?: string; + // Size + size?: Size; + creatureType?: string; classes: { name: CharacterClass; @@ -31,6 +34,12 @@ export interface Character { skillProficiencies: Skill[]; skillExpertise: Skill[]; + // Equipment training & Proficiencies + armorProficiencies: ArmorProficiency[]; + weaponProficiencies: string[]; + toolProficiencies: string[]; + languages: string[]; + // Combat baseArmorClass: number; isWieldingShield?: boolean; @@ -44,28 +53,18 @@ export interface Character { // Attacks attacks: Attack[]; - // Equipment - equipment: string[]; - // Features features: Feature[]; speciesTraits?: Feature[]; feats?: Feature[]; - // Size - size?: Size; - creatureType?: string; - - // Proficiency text lists - armorProficiencies: ArmorProficiency[]; - weaponProficiencies: string[]; - toolProficiencies: string[]; - languages: string[]; + // Spellcasting + spellcasting?: Spellcasting; // Freeform backstory?: string; appearance?: string; - // Spellcasting - spellcasting?: Spellcasting; + // Equipment + equipment: string[]; } diff --git a/packages/dnd-character-sheet/src/lib/models/feature.ts b/packages/dnd-character-sheet/src/lib/models/feature.ts index 1ed1c7e7..13e11222 100644 --- a/packages/dnd-character-sheet/src/lib/models/feature.ts +++ b/packages/dnd-character-sheet/src/lib/models/feature.ts @@ -67,6 +67,10 @@ type ResourceCount = { kind: 'fixed'; value: number; } + | { + kind: 'free-then-spell-slots'; + numberOfFreeUses: number; + } ); type ResourceDie =