The cloud copy was changed on another device since this one last synced. Use cloud version replaces this device with the cloud copy; Keep this device overwrites the cloud with this device.
+
);
}
diff --git a/src/features/world/map/MapEditor.tsx b/src/features/world/map/MapEditor.tsx
index c81cbce..b0ca9dd 100644
--- a/src/features/world/map/MapEditor.tsx
+++ b/src/features/world/map/MapEditor.tsx
@@ -53,6 +53,8 @@ export function MapEditor({ map, campaign }: { map: BattleMap; campaign: Campaig
const update = (patch: Partial) => setM((prev) => { const next = { ...prev, ...patch }; save(next); return next; });
const [tool, setTool] = useState('move');
+ const [textDraft, setTextDraft] = useState(null);
+ const [textValue, setTextValue] = useState('');
const [fogShape, setFogShape] = useState('brush');
const [brush, setBrush] = useState(0);
const [aoeShape, setAoeShape] = useState('circle');
@@ -167,10 +169,7 @@ export function MapEditor({ map, campaign }: { map: BattleMap; campaign: Campaig
}
if (tool === 'draw') {
if (drawKind === 'text') {
- if (phase === 'down') {
- const text = window.prompt('Label text:')?.trim();
- if (text) update({ drawings: [...m.drawings, { id: newId(), kind: 'text', points: [p.point], color: drawColor, width: 4, text, gmOnly: gmDraw }] });
- }
+ if (phase === 'down') setTextDraft(p.point);
return;
}
if (phase === 'down') { draftPts.current = [p.point]; }
@@ -222,6 +221,11 @@ export function MapEditor({ map, campaign }: { map: BattleMap; campaign: Campaig
const addToken = () => update({ tokens: [...m.tokens, { id: newId(), label: String(m.tokens.length + 1), color: TOKEN_COLORS[m.tokens.length % TOKEN_COLORS.length]!, col: 0, row: 0, size: 1, kind: 'npc', conditions: [], gmOnly: false }] });
const addTokenSpec = (spec: TokenSpec) => update({ tokens: [...m.tokens, { id: newId(), ...spec }] });
const addTokenSpecs = (specs: TokenSpec[]) => { if (specs.length) update({ tokens: [...m.tokens, ...specs.map((s) => ({ id: newId(), ...s }))] }); };
+ const addTextLabel = () => {
+ const t = textValue.trim();
+ if (t && textDraft) update({ drawings: [...m.drawings, { id: newId(), kind: 'text', points: [textDraft], color: drawColor, width: 4, text: t, gmOnly: gmDraw }] });
+ setTextDraft(null); setTextValue('');
+ };
const patchToken = (id: string, patch: Partial) => update({ tokens: m.tokens.map((t) => (t.id === id ? { ...t, ...patch } : t)) });
const moveToken = (id: string, col: number, row: number) => setM((prev) => {
const tokens = prev.tokens.map((t) => (t.id === id ? { ...t, col, row } : t));
@@ -402,6 +406,13 @@ export function MapEditor({ map, campaign }: { map: BattleMap; campaign: Campaig
)}
+ { setTextDraft(null); setTextValue(''); }} title="Text label" className="max-w-sm"
+ footer={<>
+
+
+ >}>
+ setTextValue(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && addTextLabel()} aria-label="Label text" placeholder="Label text" />
+
);
}
diff --git a/src/lib/rules/pf2e/progression.ts b/src/lib/rules/pf2e/progression.ts
index 1a7b737..f8c4ffd 100644
--- a/src/lib/rules/pf2e/progression.ts
+++ b/src/lib/rules/pf2e/progression.ts
@@ -78,3 +78,169 @@ export function pf2eBoostLevels(): number[] {
export function pf2eSkillIncreaseLevels(): number[] {
return [3, 5, 7, 9, 11, 13, 15, 17, 19];
}
+
+/**
+ * The defining 1st-level choice for each pf2e class (instinct / doctrine /
+ * bloodline / …), so the builder can offer a subclass picker like 5e does.
+ * Curated — there is no class-feature data file on disk. Keyed by class name.
+ */
+export const PF2E_SUBCLASSES: Record = {
+ Alchemist: [
+ { name: 'Bomber', desc: 'Throw alchemical bombs; splash and persistent damage are your trade.' },
+ { name: 'Chirurgeon', desc: 'A battlefield medic whose elixirs and Crafting heal allies.' },
+ { name: 'Mutagenist', desc: 'Drink your own mutagens to transform into a frontline brute.' },
+ { name: 'Toxicologist', desc: 'Coat weapons with poisons and exploit creature weaknesses.' },
+ ],
+ Animist: [
+ { name: 'Practice of the Dead', desc: 'Commune with ancestral and psychopomp spirits.' },
+ { name: 'Practice of the Wild', desc: 'Channel nature and beast apparitions.' },
+ { name: 'Practice of Stars & Dreams', desc: 'Draw on cosmic and oneiric apparitions.' },
+ ],
+ Barbarian: [
+ { name: 'Animal Instinct', desc: 'Adopt an animal aspect; rage with natural unarmed attacks.' },
+ { name: 'Dragon Instinct', desc: 'Breathe elemental damage and hit like a dragon.' },
+ { name: 'Fury Instinct', desc: 'Pure unfocused rage — flexible, an extra class feat.' },
+ { name: 'Giant Instinct', desc: 'Wield oversized weapons for the biggest raw damage.' },
+ { name: 'Spirit Instinct', desc: 'Deal void/spirit damage; strong against undead.' },
+ { name: 'Superstition Instinct', desc: 'Rage against magic — huge saves, but cannot be helped by spells.' },
+ ],
+ Bard: [
+ { name: 'Enigma', desc: 'A lore-master; True Strike and bonus knowledge.' },
+ { name: 'Maestro', desc: 'A virtuoso of composition cantrips and inspire effects.' },
+ { name: 'Polymath', desc: 'A versatile dabbler with a flexible repertoire.' },
+ { name: 'Warrior', desc: 'A martial skald who fights on the front line.' },
+ ],
+ Champion: [
+ { name: 'Paladin (Holy)', desc: 'Retributive Strike protects allies who are struck.' },
+ { name: 'Redeemer (Holy)', desc: 'Glimpse of Redemption shields and converts foes.' },
+ { name: 'Liberator (Holy)', desc: 'Liberating Step frees allies from restraint.' },
+ { name: 'Desecrator / Antipaladin (Unholy)', desc: 'Channel destructive reactions for an evil cause.' },
+ ],
+ Cleric: [
+ { name: 'Cloistered Cleric', desc: 'A full divine caster with a domain and font.' },
+ { name: 'Warpriest', desc: 'A martial cleric with armor and weapon training.' },
+ ],
+ Druid: [
+ { name: 'Animal Order', desc: 'Gain an animal companion and beast magic.' },
+ { name: 'Leaf Order', desc: 'A guardian of plants with a leshy familiar.' },
+ { name: 'Storm Order', desc: 'Command wind and weather with storm magic.' },
+ { name: 'Untamed (Wild) Order', desc: 'Shapeshift into wild forms with Untamed Form.' },
+ ],
+ Exemplar: [
+ { name: 'Ikon: Weapon', desc: 'Empower a signature weapon with your divine spark.' },
+ { name: 'Ikon: Worn', desc: 'Channel the spark through worn regalia or armor.' },
+ { name: 'Ikon: Body', desc: 'Make your own body the vessel of myth.' },
+ ],
+ Gunslinger: [
+ { name: 'Way of the Drifter', desc: 'A gun-and-blade skirmisher.' },
+ { name: 'Way of the Pistolero', desc: 'A quick-draw duelist with one-handed firearms.' },
+ { name: 'Way of the Sniper', desc: 'A patient marksman dealing huge aimed shots.' },
+ { name: 'Way of the Vanguard', desc: 'A heavy-weapon bruiser with area shots.' },
+ { name: 'Way of the Spellshot', desc: 'Fuse spells and bullets.' },
+ ],
+ Inventor: [
+ { name: 'Armor Innovation', desc: 'A walking tank in modified powered armor.' },
+ { name: 'Construct Innovation', desc: 'Build a robotic companion that fights beside you.' },
+ { name: 'Weapon Innovation', desc: 'A custom weapon you Overdrive for spikes of damage.' },
+ ],
+ Investigator: [
+ { name: 'Alchemical Sciences', desc: 'Brew quick alchemical items mid-case.' },
+ { name: 'Empiricism', desc: 'Read situations with cold deduction.' },
+ { name: 'Forensic Medicine', desc: 'A crime-scene healer who exploits wounds.' },
+ { name: 'Interrogation', desc: 'Read and pressure people for the truth.' },
+ ],
+ Kineticist: [
+ { name: 'Air', desc: 'Lightning, wind, and mobility impulses.' },
+ { name: 'Earth', desc: 'Stone and metal — durable and controlling.' },
+ { name: 'Fire', desc: 'Raw blasting damage.' },
+ { name: 'Metal', desc: 'Sharp, precise, armor-shredding impulses.' },
+ { name: 'Water', desc: 'Cold and fluid control and healing.' },
+ { name: 'Wood', desc: 'Plant growth, healing, and protection.' },
+ ],
+ Magus: [
+ { name: 'Inexorable Iron', desc: 'A two-handed weapon Spellstrike bruiser.' },
+ { name: 'Laughing Shadow', desc: 'A mobile one-hand-free striker.' },
+ { name: 'Sparkling Targe', desc: 'A sword-and-board magus with arcane shielding.' },
+ { name: 'Starlit Span', desc: 'A ranged Spellstrike sniper.' },
+ { name: 'Twisting Tree', desc: 'A staff/finesse martial artist.' },
+ ],
+ Oracle: [
+ { name: 'Ash', desc: 'Fire and ruin; the more cursed, the fiercer.' },
+ { name: 'Battle', desc: 'A martial oracle who fights up close.' },
+ { name: 'Bones', desc: 'Command death and the undead.' },
+ { name: 'Cosmos', desc: 'Stars, gravity, and the void.' },
+ { name: 'Flames', desc: 'Searing fire and light.' },
+ { name: 'Life', desc: 'Healing magic at a mounting cost.' },
+ { name: 'Tempest', desc: 'Storms, lightning, and the sea.' },
+ ],
+ Psychic: [
+ { name: 'Conscious Mind: Infinite Eye', desc: 'Divination and perception amps.' },
+ { name: 'Conscious Mind: Oscillating Wave', desc: 'Fire and cold blasting amps.' },
+ { name: 'Conscious Mind: Silent Whisper', desc: 'Mind-affecting force amps.' },
+ { name: 'Subconscious Mind', desc: 'Your psyche source (Distant Grasp, Tangible Dream, …).' },
+ ],
+ Ranger: [
+ { name: "Flurry Edge", desc: 'Reduce multiple-attack penalty against your prey.' },
+ { name: "Precision Edge", desc: 'Bonus damage on your first hit each round vs prey.' },
+ { name: "Outwit Edge", desc: 'Skill and defensive bonuses against your prey.' },
+ ],
+ Rogue: [
+ { name: 'Ruffian', desc: 'A bruising rogue who sneak-attacks with simple weapons.' },
+ { name: 'Scoundrel', desc: 'A face who feints foes Off-Guard.' },
+ { name: 'Thief', desc: 'Adds Dexterity to damage; the classic nimble rogue.' },
+ { name: 'Mastermind', desc: 'Recall Knowledge to make foes Off-Guard.' },
+ { name: 'Eldritch Trickster', desc: 'A spellcasting multiclass rogue.' },
+ ],
+ Sorcerer: [
+ { name: 'Aberrant', desc: 'Occult magic of alien minds.' },
+ { name: 'Angelic', desc: 'Divine magic of the heavens.' },
+ { name: 'Demonic', desc: 'Divine magic of the Abyss.' },
+ { name: 'Diabolic', desc: 'Divine magic of Hell — fire and contracts.' },
+ { name: 'Draconic', desc: 'Arcane magic and a chosen dragon’s breath.' },
+ { name: 'Elemental', desc: 'Primal magic of an element.' },
+ { name: 'Fey', desc: 'Primal magic of the First World.' },
+ { name: 'Imperial', desc: 'Arcane magic of mortal bloodlines.' },
+ { name: 'Undead', desc: 'Divine magic touched by death.' },
+ ],
+ Summoner: [
+ { name: 'Beast Eidolon', desc: 'A bestial companion of fang and claw.' },
+ { name: 'Construct Eidolon', desc: 'A made being of metal or stone.' },
+ { name: 'Dragon Eidolon', desc: 'A draconic ally with a breath weapon.' },
+ { name: 'Fey Eidolon', desc: 'A capricious First World spirit.' },
+ { name: 'Plant Eidolon', desc: 'A verdant guardian.' },
+ { name: 'Celestial / Fiend / Psychopomp', desc: 'An outsider bound to your soul.' },
+ ],
+ Swashbuckler: [
+ { name: 'Battledancer', desc: 'Gain panache by Performing.' },
+ { name: 'Braggart', desc: 'Gain panache by Demoralizing foes.' },
+ { name: 'Fencer', desc: 'Gain panache by Feinting.' },
+ { name: 'Gymnast', desc: 'Gain panache by Tumbling and grappling.' },
+ { name: 'Wit', desc: 'Gain panache by Bon Mot and clever talk.' },
+ ],
+ Thaumaturge: [
+ { name: 'Amulet', desc: 'Ward yourself and allies from harm.' },
+ { name: 'Bell', desc: 'Disrupt and frighten foes with sound.' },
+ { name: 'Chalice', desc: 'Heal and sustain by drinking from it.' },
+ { name: 'Lantern', desc: 'Reveal the hidden and the invisible.' },
+ { name: 'Mirror', desc: 'Send a duplicate to flank and confound.' },
+ { name: 'Regalia', desc: 'Command presence that bolsters allies.' },
+ { name: 'Tome', desc: 'Recall esoteric lore to exploit weaknesses.' },
+ { name: 'Wand', desc: 'Blast foes with a stored spell.' },
+ { name: 'Weapon', desc: 'A martial implement for esoteric strikes.' },
+ ],
+ Witch: [
+ { name: 'Patron: Faith’s Flamekeeper', desc: 'Divine lessons and fire.' },
+ { name: 'Patron: Silence in Snow', desc: 'Primal cold lessons.' },
+ { name: 'Patron: Spinner of Threads', desc: 'Occult fate lessons.' },
+ { name: 'Patron: Starless Shadow', desc: 'Occult shadow lessons.' },
+ { name: 'Patron: Wilding Steward', desc: 'Primal nature lessons.' },
+ { name: 'Patron: The Resentment', desc: 'Occult curses and misfortune.' },
+ ],
+ Wizard: [
+ { name: 'Thesis: Improved Familiar Attunement', desc: 'A stronger familiar that grows with you.' },
+ { name: 'Thesis: Metamagical Experimentation', desc: 'Swap in metamagic feats each day.' },
+ { name: 'Thesis: Spell Blending', desc: 'Trade lower slots for higher-rank casting.' },
+ { name: 'Thesis: Spell Substitution', desc: 'Re-prepare spells on the fly.' },
+ { name: 'Thesis: Experimental Spellshaping', desc: 'Reshape spell areas and effects.' },
+ ],
+};