Adds import support from our other project.

This commit is contained in:
2026-09-07 21:57:59 -07:00
parent a3d1a139e9
commit a3dace36de
150 changed files with 17716 additions and 21 deletions

View File

@@ -0,0 +1,57 @@
{
"$schema": "../../../schema/entity-definition.json",
"id": "armor",
"name": "Armor",
"description": "Armor entity extending base item schema with AC base, dex cap, strength requirement, and stealth penalties.",
"ruleset_id": "open5e",
"version": "1.0.0",
"schema": {
"type": "object",
"required": ["name", "armor_category", "ac_base"],
"allOf": [
{
"$ref": "item.json#/schema"
},
{
"type": "object",
"properties": {
"armor_category": {
"type": "string",
"enum": ["light", "medium", "heavy"],
"description": "Classification of armor determining weight class and dexterity application."
},
"ac_base": {
"type": "integer",
"minimum": 0,
"description": "Base Armor Class provided by this armor (e.g. 11 for Leather, 14 for Breastplate, 18 for Plate)."
},
"ac_modifier": {
"type": "integer",
"description": "Armor Class modifier over the base 10 (e.g. +1 for Leather, +4 for Breastplate, +8 for Plate)."
},
"dex_cap": {
"type": ["integer", "null"],
"description": "Maximum dexterity modifier bonus allowed. null = uncapped (light armor), 2 = medium armor cap (+2), 0 = heavy armor (no dex bonus)."
},
"strength_requirement": {
"type": ["integer", "null"],
"description": "Minimum Strength score needed to wear without speed penalty. null or 0 if none."
},
"stealth_disadvantage": {
"type": "boolean",
"default": false,
"description": "Whether wearing this armor imposes disadvantage on Dexterity (Stealth) checks."
}
}
}
]
},
"default_values": {
"weight": 10,
"quantity": 1,
"equipped": false,
"rarity": "common",
"stealth_disadvantage": false,
"strength_requirement": null
}
}

View File

@@ -0,0 +1,375 @@
{
"$schema": "../../../schema/entity-definition.json",
"id": "character",
"name": "Player Character",
"description": "5e Player Character entity with ability scores, proficiencies, hit points, and inventory.",
"ruleset_id": "open5e",
"version": "1.0.0",
"default_template": "character_sheet",
"allowed_templates": [
"character_sheet",
"character_sheet_alt",
"spellcasting_sheet",
"stat_block",
"character_card",
"character_list_item"
],
"schema": {
"type": "object",
"required": ["level", "abilities", "hit_points"],
"properties": {
"level": {
"type": "integer",
"minimum": 1,
"maximum": 20,
"description": "Total character level."
},
"class": {
"type": "string",
"description": "Primary class of the character."
},
"race": {
"type": "string",
"description": "Character race / species."
},
"background": {
"type": "string",
"description": "Character background."
},
"alignment": {
"type": "string",
"description": "Ethical and moral alignment."
},
"experience_points": {
"type": "integer",
"minimum": 0
},
"armor_class": {
"type": ["integer", "object"],
"description": "Armor class value or extended property with shield/dex/item modifiers."
},
"equipped_armor": {
"type": ["object", "null"],
"description": "Currently equipped body armor item snapshot or reference. null if unarmored.",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"armor_category": {
"type": "string",
"enum": ["light", "medium", "heavy"]
},
"ac_base": { "type": "integer" },
"dex_cap": { "type": ["integer", "null"] },
"stealth_disadvantage": { "type": "boolean" },
"strength_requirement": { "type": ["integer", "null"] }
}
},
"equipped_shield": {
"type": ["object", "null"],
"description": "Currently equipped shield item snapshot or reference. null if no shield is equipped.",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"ac_bonus": { "type": "integer", "default": 2 },
"stealth_disadvantage": { "type": "boolean" },
"strength_requirement": { "type": ["integer", "null"] }
}
},
"speed": {
"type": "integer",
"description": "Base walking speed in feet."
},
"hit_points": {
"type": "object",
"required": ["current", "max"],
"properties": {
"current": { "type": "integer" },
"max": { "type": "integer" },
"temp": { "type": "integer", "default": 0 }
}
},
"abilities": {
"type": "object",
"description": "Core 5e ability scores.",
"required": [
"strength",
"dexterity",
"constitution",
"intelligence",
"wisdom",
"charisma"
],
"properties": {
"strength": { "type": ["integer", "object"] },
"dexterity": { "type": ["integer", "object"] },
"constitution": { "type": ["integer", "object"] },
"intelligence": { "type": ["integer", "object"] },
"wisdom": { "type": ["integer", "object"] },
"charisma": { "type": ["integer", "object"] }
}
},
"player_name": {
"type": "string",
"description": "Name of the player controlling this character."
},
"subclass": {
"type": "string",
"description": "Character archetype or subclass (e.g. Hunter, Champion, Evoker)."
},
"inspiration": {
"type": ["boolean", "integer"],
"default": false,
"description": "Heroic inspiration state or count."
},
"initiative": {
"type": ["integer", "object"],
"description": "Initiative modifier (typically dexterity modifier plus bonuses)."
},
"hit_dice": {
"type": "object",
"properties": {
"total": { "type": ["integer", "string"] },
"current": { "type": ["integer", "string"] },
"dice": { "type": "string" }
}
},
"death_saves": {
"type": "object",
"properties": {
"successes": {
"type": "integer",
"minimum": 0,
"maximum": 3,
"default": 0
},
"failures": {
"type": "integer",
"minimum": 0,
"maximum": 3,
"default": 0
}
}
},
"saving_throws": {
"type": ["array", "object"],
"description": "Saving throw proficiencies and bonuses."
},
"skills": {
"type": ["array", "object"],
"description": "Skill proficiencies and bonuses across the 18 standard 5e skills."
},
"passive_perception": {
"type": ["integer", "object"],
"description": "Passive Wisdom (Perception) score: typically 10 + perception bonus."
},
"other_proficiencies_and_languages": {
"type": "object",
"properties": {
"armor": { "type": "array", "items": { "type": "string" } },
"weapons": { "type": "array", "items": { "type": "string" } },
"tools": { "type": "array", "items": { "type": "string" } },
"languages": { "type": "array", "items": { "type": "string" } }
}
},
"attacks": {
"type": "array",
"description": "Weapons and spell attacks for the Attacks & Spellcasting section.",
"items": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" },
"atk_bonus": { "type": ["string", "integer"] },
"damage": { "type": "string" },
"damage_type": { "type": "string" },
"range": { "type": "string" },
"notes": { "type": "string" }
}
}
},
"currency": {
"type": "object",
"properties": {
"cp": { "type": "integer", "default": 0 },
"sp": { "type": "integer", "default": 0 },
"ep": { "type": "integer", "default": 0 },
"gp": { "type": "integer", "default": 0 },
"pp": { "type": "integer", "default": 0 }
}
},
"personality_traits": { "type": ["string", "array"] },
"ideals": { "type": ["string", "array"] },
"bonds": { "type": ["string", "array"] },
"flaws": { "type": ["string", "array"] },
"features_and_traits": {
"type": "array",
"description": "Class, racial, and background features.",
"items": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" },
"source": { "type": "string" },
"desc": { "type": "string" }
}
}
},
"character_details": {
"type": "object",
"properties": {
"age": { "type": ["integer", "string"] },
"height": { "type": "string" },
"weight": { "type": "string" },
"eyes": { "type": "string" },
"skin": { "type": "string" },
"hair": { "type": "string" },
"appearance": { "type": "string" },
"backstory": { "type": "string" },
"allies_and_organizations": { "type": ["string", "array"] },
"treasure": { "type": ["string", "array"] }
}
},
"spellcasting": {
"type": "object",
"properties": {
"class": { "type": "string" },
"ability": { "type": "string" },
"save_dc": { "type": ["integer", "object"] },
"attack_bonus": { "type": ["integer", "object"] },
"cantrips": {
"type": "array",
"items": { "type": ["string", "object"] }
},
"slots": {
"type": "object",
"description": "Spell slot counts per spell level 1-9.",
"additionalProperties": {
"type": "object",
"properties": {
"total": { "type": "integer" },
"expended": { "type": "integer", "default": 0 }
}
}
},
"spells": {
"type": "array",
"items": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" },
"level": { "type": "integer", "minimum": 0, "maximum": 9 },
"prepared": { "type": "boolean", "default": false },
"desc": { "type": "string" }
}
}
}
}
},
"inventory": {
"type": "array",
"description": "List of carried items.",
"items": {
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string" },
"entity_ref": {
"type": "string",
"description": "Optional relative path or identifier of the base item entity."
},
"name": { "type": "string" },
"quantity": { "type": "integer", "default": 1 },
"weight": { "type": "number", "default": 0 },
"equipped": { "type": "boolean", "default": false },
"ac_base": { "type": "integer" },
"dex_cap": { "type": ["integer", "null"] },
"ac_bonus": { "type": "integer" },
"grants_modifiers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"target": { "type": "string" },
"value": { "type": ["number", "string"] },
"type": { "type": "string" },
"source": { "type": "string" }
}
}
}
},
"additionalProperties": true
}
}
}
},
"calculated_properties": {
"armor_class": {
"expr": "10 + (equipped_armor ? (equipped_armor.ac_modifier !== undefined ? equipped_armor.ac_modifier : equipped_armor.ac_base - 10) : 0) + (equipped_armor && equipped_armor.dex_cap !== null ? (equipped_armor.dex_cap === 0 ? min(0, abilities.dexterity.modifier) : min(equipped_armor.dex_cap, abilities.dexterity.modifier)) : abilities.dexterity.modifier) + (equipped_shield ? equipped_shield.ac_bonus : 0) + sum(filter(armor_class.modifiers, m => m.active && m.type !== 'armor' && m.type !== 'ability'))",
"type": "integer",
"dependencies": [
"equipped_armor",
"equipped_shield",
"abilities.dexterity.modifier",
"armor_class.modifiers"
],
"description": "5e Armor Class calculation: base 10 + armor modifier (e.g. +1 for leather, +8 for plate) + dexterity modifier (capped by armor; heavy armor caps positive dex to 0 but allows negative penalties) + shield bonus + active external modifiers."
},
"proficiency_bonus": {
"expr": "floor((level - 1) / 4) + 2",
"type": "integer",
"dependencies": ["level"],
"description": "Standard 5e proficiency bonus calculation."
},
"ability_modifiers.strength": {
"expr": "floor((abilities.strength - 10) / 2)",
"type": "integer",
"dependencies": ["abilities.strength"]
},
"ability_modifiers.dexterity": {
"expr": "floor((abilities.dexterity - 10) / 2)",
"type": "integer",
"dependencies": ["abilities.dexterity"]
},
"ability_modifiers.constitution": {
"expr": "floor((abilities.constitution - 10) / 2)",
"type": "integer",
"dependencies": ["abilities.constitution"]
},
"ability_modifiers.intelligence": {
"expr": "floor((abilities.intelligence - 10) / 2)",
"type": "integer",
"dependencies": ["abilities.intelligence"]
},
"ability_modifiers.wisdom": {
"expr": "floor((abilities.wisdom - 10) / 2)",
"type": "integer",
"dependencies": ["abilities.wisdom"]
},
"ability_modifiers.charisma": {
"expr": "floor((abilities.charisma - 10) / 2)",
"type": "integer",
"dependencies": ["abilities.charisma"]
}
},
"default_values": {
"level": 1,
"equipped_armor": null,
"equipped_shield": null,
"abilities": {
"strength": 10,
"dexterity": 10,
"constitution": 10,
"intelligence": 10,
"wisdom": 10,
"charisma": 10
},
"hit_points": {
"current": 10,
"max": 10,
"temp": 0
},
"speed": 30,
"inventory": []
}
}

View File

@@ -0,0 +1,100 @@
{
"$schema": "../../../schema/entity-definition.json",
"id": "item",
"name": "Item / Gear",
"description": "Base entity schema for generic items, equipment, and gear in 5e.",
"ruleset_id": "open5e",
"version": "1.0.0",
"schema": {
"type": "object",
"required": ["name"],
"properties": {
"name": {
"type": "string",
"description": "The name of the item."
},
"description": {
"type": "string",
"description": "A description of the item."
},
"cost": {
"description": "The market value or cost of the item.",
"type": "object",
"properties": {
"quantity": { "type": "number", "minimum": 0 },
"unit": {
"type": "string",
"enum": ["cp", "sp", "ep", "gp", "pp"],
"default": "gp"
}
}
},
"weight": {
"type": "number",
"minimum": 0,
"description": "Weight in pounds (lbs)."
},
"rarity": {
"type": "string",
"enum": [
"common",
"uncommon",
"rare",
"very rare",
"legendary",
"artifact"
],
"default": "common"
},
"attunement": {
"type": "boolean",
"default": false,
"description": "Whether this item requires attunement."
},
"equipped": {
"type": "boolean",
"default": false,
"description": "Whether this item is currently equipped or worn."
},
"quantity": {
"type": "integer",
"minimum": 1,
"default": 1,
"description": "Number of copies carried in inventory."
},
"grants_modifiers": {
"type": "array",
"description": "Modifiers applied to the character when this item is equipped.",
"items": {
"type": "object",
"required": ["target", "value"],
"properties": {
"target": {
"type": "string",
"description": "The property path modified on the wearer (e.g. 'armor_class', 'saving_throws.all')."
},
"value": {
"type": ["number", "string"],
"description": "Modifier bonus value or expression."
},
"type": {
"type": "string",
"description": "Modifier type (e.g. 'bonus', 'base', 'override', 'advantage')."
},
"source": {
"type": "string",
"description": "Name or origin of the bonus."
}
}
}
}
}
},
"default_values": {
"weight": 0,
"quantity": 1,
"equipped": false,
"rarity": "common",
"attunement": false
}
}

View File

@@ -0,0 +1,45 @@
{
"$schema": "../../../schema/entity-definition.json",
"id": "shield",
"name": "Shield",
"description": "Shield entity extending base item schema with AC bonus and strength/stealth properties.",
"ruleset_id": "open5e",
"version": "1.0.0",
"schema": {
"type": "object",
"required": ["name", "ac_bonus"],
"allOf": [
{
"$ref": "item.json#/schema"
},
{
"type": "object",
"properties": {
"ac_bonus": {
"type": "integer",
"default": 2,
"description": "Armor Class bonus granted by wielding this shield (standard 5e shield is +2)."
},
"strength_requirement": {
"type": ["integer", "null"],
"description": "Minimum Strength score required to use without penalty."
},
"stealth_disadvantage": {
"type": "boolean",
"default": false,
"description": "Whether wielding this shield imposes disadvantage on Dexterity (Stealth) checks."
}
}
}
]
},
"default_values": {
"ac_bonus": 2,
"weight": 6,
"quantity": 1,
"equipped": false,
"rarity": "common",
"stealth_disadvantage": false,
"strength_requirement": null
}
}

View File

@@ -0,0 +1,92 @@
{
"$schema": "../../../schema/entity-definition.json",
"id": "spell",
"name": "Spell",
"description": "5e Spell entity definition declaring level, school, casting time, range, components, duration, and spell description.",
"ruleset_id": "open5e",
"version": "1.0.0",
"schema": {
"type": "object",
"required": [
"level",
"school",
"casting_time",
"range",
"duration",
"description"
],
"properties": {
"level": {
"type": "integer",
"minimum": 0,
"maximum": 9,
"description": "Spell level from 0 (cantrip) up to 9."
},
"school": {
"type": "string",
"enum": [
"abjuration",
"conjuration",
"divination",
"enchantment",
"evocation",
"illusion",
"necromancy",
"transmutation"
]
},
"casting_time": {
"type": "string",
"description": "e.g. 1 action, 1 bonus action, 1 reaction, 10 minutes."
},
"range": {
"type": "string",
"description": "e.g. Self, Touch, 30 feet, 60 feet, 120 feet."
},
"components": {
"type": "object",
"properties": {
"verbal": { "type": "boolean", "default": true },
"somatic": { "type": "boolean", "default": true },
"material": { "type": "boolean", "default": false },
"material_specified": { "type": "string" }
}
},
"duration": {
"type": "string",
"description": "e.g. Instantaneous, 1 round, 1 hour, Concentration up to 1 minute."
},
"concentration": {
"type": "boolean",
"default": false
},
"ritual": {
"type": "boolean",
"default": false
},
"description": {
"type": "string",
"description": "Rules text of the spell effect."
},
"higher_levels": {
"type": "string",
"description": "Rules text for casting at higher slot levels."
}
}
},
"default_values": {
"level": 0,
"school": "evocation",
"casting_time": "1 action",
"range": "60 feet",
"components": {
"verbal": true,
"somatic": true,
"material": false
},
"duration": "Instantaneous",
"concentration": false,
"ritual": false,
"description": ""
}
}

View File

@@ -0,0 +1,82 @@
{
"$schema": "../../../schema/entity-definition.json",
"id": "weapon",
"name": "Weapon",
"description": "5e Weapon entity definition inheriting from base item, specifying damage dice, damage type, and weapon properties.",
"ruleset_id": "open5e",
"version": "1.0.0",
"schema": {
"allOf": [
{
"$ref": "item.json#/schema"
},
{
"type": "object",
"required": ["damage_dice", "damage_type"],
"properties": {
"weapon_category": {
"type": "string",
"enum": ["simple", "martial"]
},
"weapon_range": {
"type": "string",
"enum": ["melee", "ranged"],
"default": "melee"
},
"damage_dice": {
"type": "string",
"description": "Damage formula e.g. 1d6, 1d8, 2d6."
},
"damage_type": {
"type": "string",
"enum": ["bludgeoning", "piercing", "slashing"]
},
"properties": {
"type": "array",
"description": "Standard 5e weapon properties.",
"items": {
"type": "string",
"enum": [
"ammunition",
"finesse",
"heavy",
"light",
"loading",
"range",
"reach",
"special",
"thrown",
"two-handed",
"versatile"
]
}
},
"range_normal": {
"type": ["integer", "null"],
"description": "Standard effective range in feet."
},
"range_long": {
"type": ["integer", "null"],
"description": "Long range with disadvantage in feet."
},
"versatile_dice": {
"type": ["string", "null"],
"description": "Two-handed damage dice when used versatile (e.g. 1d10 for a longsword)."
}
}
}
]
},
"default_values": {
"cost": {
"quantity": 10,
"unit": "gp"
},
"weight": 2.0,
"weapon_category": "simple",
"weapon_range": "melee",
"damage_dice": "1d6",
"damage_type": "slashing",
"properties": []
}
}

View File

@@ -0,0 +1,51 @@
{
"$schema": "../../../../schema/entity.json",
"uuid": "f6a83d72-b94e-4e5c-a5c2-f39b6b52d202",
"ruleset_id": "open5e",
"entity_type": "character",
"display_name": "Acolyte",
"description": "Medium humanoid (any race), any alignment.",
"version": "1.0.0",
"template_id": "stat_block",
"tags": ["npc", "humanoid", "spellcaster", "cr-1/4"],
"properties": {
"size": "medium",
"type": "Humanoid",
"subtype": "any race",
"alignment": "any alignment",
"armor_class": 10,
"hit_points": {
"current": 9,
"max": 9,
"formula": "2d8 + 0",
"hit_dice": "2d8"
},
"speed": 30,
"speed_desc": "30 ft.",
"challenge_rating": "1/4 (50 XP)",
"cr": "1/4",
"abilities": {
"strength": 10,
"dexterity": 10,
"constitution": 10,
"intelligence": 10,
"wisdom": 14,
"charisma": 11
},
"skills": ["medicine +4", "religion +2"],
"senses": "passive Perception 12",
"languages": ["any one language (usually Common)"],
"traits": [
{
"name": "Spellcasting",
"desc": "The acolyte is a 1st-level spellcaster. Its spellcasting ability is Wisdom (spell save DC 12, +4 to hit with spell attacks). The acolyte has following cleric spells prepared:\n\n* Cantrips (at will): _light, sacred flame, thaumaturgy_\n* 1st level (3 slots): _bless, cure wounds, sanctuary_"
}
],
"actions": [
{
"name": "Club",
"desc": "_Melee Weapon Attack:_ +2 to hit, reach 5 ft., one target. _Hit:_ 2 (1d4) bludgeoning damage."
}
]
}
}

View File

@@ -0,0 +1,99 @@
{
"$schema": "../../../../schema/entity.json",
"uuid": "b8a91c34-d07f-4f8a-92c1-d41a8a91e404",
"ruleset_id": "open5e",
"entity_type": "character",
"display_name": "Adult Black Dragon",
"description": "Huge dragon, chaotic evil.",
"version": "1.0.0",
"template_id": "stat_block",
"tags": ["monster", "dragon", "legendary", "cr-14"],
"properties": {
"size": "huge",
"type": "Dragon",
"alignment": "chaotic evil",
"armor_class": 19,
"armor_description": "natural armor",
"hit_points": {
"current": 196,
"max": 196,
"formula": "17d12 + 85",
"hit_dice": "17d12"
},
"speed": 40,
"speed_desc": "40 ft., fly 80 ft., swim 40 ft.",
"challenge_rating": "14 (11,500 XP)",
"cr": "14",
"abilities": {
"strength": 23,
"dexterity": 14,
"constitution": 21,
"intelligence": 14,
"wisdom": 13,
"charisma": 17
},
"saving_throws": [
"Dexterity +7",
"Constitution +10",
"Wisdom +6",
"Charisma +8"
],
"skills": ["perception +16", "stealth +11"],
"damage_immunities": ["acid"],
"senses": "darkvision 120 ft., blindsight 60 ft., passive Perception 11",
"languages": ["Common", "Draconic"],
"traits": [
{
"name": "Amphibious",
"desc": "The dragon can breathe air and water."
},
{
"name": "Legendary Resistance (3/Day)",
"desc": "If the dragon fails a saving throw, it can choose to succeed instead."
}
],
"actions": [
{
"name": "Multiattack",
"desc": "The dragon can use its Frightful Presence. It then makes three attacks: one with its bite and two with its claws."
},
{
"name": "Bite",
"desc": "_Melee Weapon Attack:_ +11 to hit, reach 10 ft., one target. _Hit:_ 17 (2d10 + 6) piercing damage plus 4 (1d8) acid damage."
},
{
"name": "Claw",
"desc": "_Melee Weapon Attack:_ +11 to hit, reach 5 ft., one target. _Hit:_ 13 (2d6 + 6) slashing damage."
},
{
"name": "Tail",
"desc": "_Melee Weapon Attack:_ +11 to hit, reach 15 ft., one target. _Hit:_ 15 (2d8 + 6) bludgeoning damage."
},
{
"name": "Frightful Presence",
"desc": "Each creature of the dragon's choice that is within 120 feet of the dragon and aware of it must succeed on a DC 16 Wisdom saving throw or become frightened for 1 minute. A creature can repeat the saving throw at the end of each of its turns, ending the effect on itself on a success. If a creature's saving throw is successful or the effect ends for it, the creature is immune to the dragon's Frightful Presence for the next 24 hours."
},
{
"name": "Acid Breath (Recharge 5-6)",
"desc": "The dragon exhales acid in a 60-foot line that is 5 feet wide. Each creature in that line must make a DC 18 Dexterity saving throw, taking 54 (12d8) acid damage on a failed save, or half as much damage on a successful one."
}
],
"legendary_actions": {
"description": "The dragon can take 3 legendary actions, choosing from the options below. Only one legendary action option can be used at a time and only at the end of another creature's turn. The dragon regains spent legendary actions at the start of its turn.",
"actions": [
{
"name": "Detect",
"desc": "The dragon makes a Wisdom (Perception) check."
},
{
"name": "Tail Attack",
"desc": "The dragon makes a tail attack."
},
{
"name": "Wing Attack (Costs 2 Actions)",
"desc": "The dragon beats its wings. Each creature within 10 ft. of the dragon must succeed on a DC 19 Dexterity saving throw or take 13 (2d6 + 6) bludgeoning damage and be knocked prone. The dragon can then fly up to half its flying speed."
}
]
}
}
}

View File

@@ -0,0 +1,261 @@
{
"$schema": "../../../schema/entity.json",
"uuid": "4c8f3824-3453-48bf-9a0e-a4c336b3bcf2",
"ruleset_id": "open5e",
"entity_type": "character",
"display_name": "Arannis",
"description": "Elven Ranger exploring ancient ruins.",
"version": "1.0.0",
"created_at": "2026-09-06T14:00:00Z",
"updated_at": "2026-09-06T18:00:00Z",
"tags": ["pc", "ranger", "elf"],
"properties": {
"player_name": "Tom",
"level": 3,
"class": "Ranger",
"subclass": "Hunter",
"race": "Elf",
"size": "medium",
"type": "Humanoid",
"subtype": "Elf",
"background": "Outlander",
"alignment": "Chaotic Good",
"experience_points": 900,
"inspiration": true,
"proficiency_bonus": 2,
"initiative": 3,
"speed": 35,
"speed_desc": "35 ft.",
"hit_points": {
"current": 24,
"max": 24,
"formula": "3d10 + 6",
"temp": 0
},
"hit_dice": {
"total": 3,
"current": 3,
"dice": "1d10"
},
"death_saves": {
"successes": 0,
"failures": 0
},
"saving_throws": ["Strength +3", "Dexterity +5"],
"advantages": ["wis"],
"skills": ["Athletics +3", "Perception +5", "Stealth +5", "Survival +5"],
"passive_perception": 15,
"senses": "darkvision 60 ft., passive Perception 15",
"languages": ["Common", "Elvish", "Sylvan"],
"other_proficiencies_and_languages": {
"armor": ["Light armor", "Medium armor", "Shields"],
"weapons": ["Simple weapons", "Martial weapons"],
"tools": ["Herbalism kit", "Flute"],
"languages": ["Common", "Elvish", "Sylvan"]
},
"attacks": [
{
"name": "Longbow",
"atk_bonus": "+7",
"damage": "1d8 + 3",
"damage_type": "Piercing",
"range": "150/600 ft."
},
{
"name": "Shortsword",
"atk_bonus": "+5",
"damage": "1d6 + 3",
"damage_type": "Piercing",
"range": "5 ft."
}
],
"currency": {
"cp": 15,
"sp": 40,
"ep": 0,
"gp": 25,
"pp": 0
},
"personality_traits": "I feel far more comfortable among the ancient trees and wild beasts than in bustling cities.",
"ideals": "Nature: The natural world is more important than the constructs of civilization.",
"bonds": "An ancient ruined shrine in the northern forest holds a secret I have sworn to protect.",
"flaws": "I am slow to trust others and prefer solitude even in times of danger.",
"features_and_traits": [
{
"name": "Favored Enemy (Undead)",
"source": "Ranger 1",
"desc": "You have advantage on Wisdom (Survival) checks to track your favored enemies, as well as on Intelligence checks to recall information about them."
},
{
"name": "Natural Explorer (Forest)",
"source": "Ranger 1",
"desc": "When making an Intelligence or Wisdom check related to your favored terrain, your proficiency bonus is doubled if you are using a skill you're proficient in."
},
{
"name": "Colossus Slayer",
"source": "Hunter Archetype",
"desc": "Your tenacity can wear down the most potent foes. Once per turn when you hit a creature with a weapon attack, the creature takes an extra 1d8 damage if it's below its hit point maximum."
}
],
"spellcasting": {
"class": "Ranger",
"ability": "WISDOM",
"save_dc": 13,
"attack_bonus": 5,
"cantrips": [],
"slots": {
"1": {
"total": 3,
"expended": 1
}
},
"spells": [
{
"name": "Hunter's Mark",
"level": 1,
"prepared": true,
"desc": "You choose a creature you can see within range and mystically mark it as your quarry."
},
{
"name": "Cure Wounds",
"level": 1,
"prepared": true,
"desc": "A creature you touch regains a number of hit points equal to 1d8 + your spellcasting ability modifier."
}
]
},
"traits": [
{
"name": "Fey Ancestry",
"desc": "Arannis has advantage on saving throws against being charmed, and magic cannot put her to sleep."
},
{
"name": "Natural Explorer",
"desc": "Difficult terrain does not slow Arannis's group while traveling."
}
],
"actions": [
{
"name": "Longbow",
"desc": "_Ranged Weapon Attack:_ +7 to hit, range 150/600 ft., one target. _Hit:_ 7 (1d8 + 3) piercing damage."
},
{
"name": "Shortsword",
"desc": "_Melee Weapon Attack:_ +5 to hit, reach 5 ft., one target. _Hit:_ 6 (1d6 + 3) piercing damage."
}
],
"equipped_armor": {
"id": "armor_leather",
"name": "Leather Armor",
"armor_category": "light",
"ac_base": 11,
"ac_modifier": 1,
"dex_cap": null,
"stealth_disadvantage": false,
"strength_requirement": null
},
"equipped_shield": null,
"armor_class": {
"value": 15,
"base": 10,
"expr": "base + sum(modifiers)",
"modifiers": [
{
"source": "Leather Armor",
"value": 1,
"type": "armor"
},
{
"source": "Dexterity Modifier",
"value": 3,
"type": "ability"
},
{
"source": "Ring of Protection +1",
"value": 1,
"type": "bonus",
"active": true
}
]
},
"abilities": {
"strength": 12,
"dexterity": {
"value": 16,
"base": 14,
"modifier": 3,
"modifiers": [
{
"source": "Elven Agility",
"value": 2,
"type": "bonus"
}
]
},
"constitution": 14,
"intelligence": 10,
"wisdom": 15,
"charisma": 8
},
"inventory": [
{
"id": "armor_leather",
"entity_ref": "entities/armor.json",
"name": "Leather Armor",
"quantity": 1,
"weight": 10.0,
"equipped": true,
"ac_base": 11,
"dex_cap": null
},
{
"id": "ring_protection",
"entity_ref": "entities/item.json",
"name": "Ring of Protection +1",
"quantity": 1,
"weight": 0.1,
"equipped": true,
"grants_modifiers": [
{
"target": "armor_class",
"value": 1,
"type": "bonus",
"source": "Ring of Protection +1"
}
]
},
{
"id": "longbow",
"entity_ref": "entities/weapon.json",
"name": "Longbow",
"quantity": 1,
"weight": 2.0,
"equipped": true
},
{
"id": "shortsword",
"entity_ref": "entities/weapon.json",
"name": "Shortsword",
"quantity": 1,
"weight": 2.0,
"equipped": true
},
{
"id": "arrow",
"entity_ref": "entities/item.json",
"name": "Arrow",
"quantity": 20,
"weight": 1.0,
"equipped": false
},
{
"id": "potion_healing",
"entity_ref": "entities/item.json",
"name": "Potion of Healing",
"quantity": 2,
"weight": 1.0,
"equipped": false
}
]
}
}

View File

@@ -0,0 +1,56 @@
{
"$schema": "../../../../schema/entity.json",
"uuid": "e5b72e61-a83d-4c3d-b4b1-e28a5a41c101",
"ruleset_id": "open5e",
"entity_type": "character",
"display_name": "Goblin",
"description": "Small humanoid (goblinoid), neutral evil.",
"version": "1.0.0",
"template_id": "stat_block",
"tags": ["monster", "goblinoid", "humanoid", "cr-1/4"],
"properties": {
"size": "small",
"type": "Humanoid",
"subtype": "goblinoid",
"alignment": "neutral evil",
"armor_class": 15,
"armor_description": "leather armor, shield",
"hit_points": {
"current": 7,
"max": 7,
"formula": "2d6 + 0",
"hit_dice": "2d6"
},
"speed": 30,
"speed_desc": "30 ft.",
"challenge_rating": "1/4 (50 XP)",
"cr": "1/4",
"abilities": {
"strength": 8,
"dexterity": 14,
"constitution": 10,
"intelligence": 10,
"wisdom": 8,
"charisma": 8
},
"skills": ["stealth +6"],
"senses": "darkvision 60 ft., passive Perception 9",
"languages": ["Common", "Goblin"],
"traits": [
{
"name": "Nimble Escape",
"desc": "The goblin can take the Disengage or Hide action as a bonus action on each of its turns."
}
],
"actions": [
{
"name": "Scimitar",
"desc": "_Melee Weapon Attack:_ +4 to hit, reach 5 ft., one target. _Hit:_ 5 (1d6 + 2) slashing damage."
},
{
"name": "Shortbow",
"desc": "_Ranged Weapon Attack:_ +4 to hit, range 80/320 ft., one target. _Hit:_ 5 (1d6 + 2) piercing damage."
}
]
}
}

View File

@@ -0,0 +1,492 @@
# Open5e Ruleset — Import & Export Specification Guide
This document is the technical integration and schema guide for third-party applications, services, and developers who want to **import** or **export** Open5e (D&D 5th Edition SRD) entities—including **Player Characters / Creatures**, **Weapons**, **Armor**, **Shields**, **Spells**, and **Generic Items**.
---
## Table of Contents
1. [Overview & Architecture](#1-overview--architecture)
2. [What Files to Distribute to External Apps](#2-what-files-to-distribute-to-external-apps)
3. [The Universal Entity Envelope (`entity.json`)](#3-the-universal-entity-envelope-entityjson)
4. [Entity Specifications & Payloads](#4-entity-specifications--payloads)
- [4.1 Character (`character.json`)](#41-character-entity-characterjson)
- [4.2 Base Item (`item.json`)](#42-base-item-entity-itemjson)
- [4.3 Weapon (`weapon.json`)](#43-weapon-entity-weaponjson)
- [4.4 Armor (`armor.json`)](#44-armor-entity-armorjson)
- [4.5 Shield (`shield.json`)](#45-shield-entity-shieldjson)
- [4.6 Spell (`spell.json`)](#46-spell-entity-spelljson)
5. [Validation & Conformance Rules](#5-validation--conformance-rules)
6. [Import & Export Implementation Checklist](#6-import--export-implementation-checklist)
---
## 1. Overview & Architecture
In our system, all data entities (characters, items, spells, etc.) follow a two-tier JSON architecture:
1. **The Universal Envelope (`entity.json`)**: Provides standard metadata required across all rulesets and systems (`uuid`, `ruleset_id`, `entity_type`, `display_name`, `version`, `tags`, etc.).
2. **The Entity Properties Payload (`properties`)**: Houses ruleset-specific and entity-specific fields defined by the entity schema (e.g. ability scores, spell slots, armor classes, damage dice).
```
┌─────────────────────────────────────────────────────────────┐
│ Universal Envelope (schema/entity.json) │
│ ├─ uuid: "4c8f3824-3453-48bf-9a0e-a4c336b3bcf2" │
│ ├─ ruleset_id: "open5e" │
│ ├─ entity_type: "character" | "weapon" | "spell" | ... │
│ ├─ display_name: "Arannis" │
│ └─ properties: { ... } <── Validated by Entity Schema │
└─────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ Entity-Specific Schema (entities/{type}.json) │
│ ├─ character.json │
│ ├─ weapon.json (inherits from item.json) │
│ ├─ armor.json (inherits from item.json) │
│ ├─ shield.json (inherits from item.json) │
│ ├─ spell.json │
│ └─ item.json │
└──────────────────────────────────────────────────┘
```
---
## 2. What Files to Distribute to External Apps
To give an external application complete import and export capability, package and provide the following files:
### A. Root Schemas (`schema/`)
1. **[`schema/entity.json`](../../schema/entity.json)**: The universal envelope schema. Every exported file must validate against this schema.
2. **[`schema/entity-definition.json`](../../schema/entity-definition.json)**: The meta-schema describing how entity types and schemas are declared.
### B. Ruleset Manifest (`rulesets/open5e/`)
3. **[`rulesets/open5e/manifest.json`](manifest.json)**: Identifies the ruleset (`open5e`), version, author, and all registered entities and templates.
### C. Entity Property Schemas (`rulesets/open5e/entities/`)
4. **[`character.json`](entities/character.json)**: 5e Character and creature schema (attributes, vitals, proficiencies, attacks, spells, inventory).
5. **[`item.json`](entities/item.json)**: Base schema for equipment, gear, and magic items.
6. **[`weapon.json`](entities/weapon.json)**: Weapon schema (damage dice, damage types, ranges, weapon properties; inherits from `item.json`).
7. **[`armor.json`](entities/armor.json)**: Armor schema (AC base, Dex caps, stealth penalty, strength requirement; inherits from `item.json`).
8. **[`shield.json`](entities/shield.json)**: Shield schema (AC bonus, stealth penalty; inherits from `item.json`).
9. **[`spell.json`](entities/spell.json)**: Spell schema (spell level 0–9, school, casting time, range, components, duration).
> **Integration Note on `$ref` Resolution**:
> `weapon.json`, `armor.json`, and `shield.json` use JSON Schema `$ref: "item.json#/schema"`. When integrating with schema validators (such as Ajv, jsonschema, or Newtonsoft.Json), ensure all schema files are preloaded in the schema registry or resolved relative to their folder.
---
## 3. The Universal Entity Envelope (`entity.json`)
Every exported file **must** include these top-level fields:
| Field | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| `$schema` | `string` | Recommended | URI or relative path to schema (`"../../../schema/entity.json"`). |
| `uuid` | `string` | **Yes** | Unique identifier (standard UUID v4 format recommended). |
| `ruleset_id` | `string` | **Yes** | Must be `"open5e"` for this ruleset. |
| `entity_type` | `string` | **Yes** | One of: `"character"`, `"item"`, `"weapon"`, `"armor"`, `"shield"`, `"spell"`. |
| `display_name` | `string` | **Yes** | Human-readable primary label (e.g. `"Arannis"`, `"Longsword +1"`). |
| `description` | `string` | No | Short synopsis or narrative summary. |
| `version` | `string` | No | Data version format (default `"1.0.0"`). |
| `created_at` | `string` | No | ISO 8601 timestamp (e.g. `"2026-09-07T12:00:00Z"`). |
| `updated_at` | `string` | No | ISO 8601 timestamp of last modification. |
| `tags` | `array<string>`| No | Filter and categorization tags (e.g. `["pc", "ranger", "elf"]`). |
| `template_id` | `string` | No | Optional UI template override (e.g. `"character_sheet"`). |
| `properties` | `object` | **Yes** | The entity's payload data (detailed below). |
---
## 4. Entity Specifications & Payloads
### 4.1 Character Entity (`character.json`)
Used for **Player Characters (PCs)**, **Non-Player Characters (NPCs)**, and **Monsters / Creatures**.
#### Required Fields in `properties`:
- `level` (`integer`, 1–20)
- `abilities` (`object` containing `strength`, `dexterity`, `constitution`, `intelligence`, `wisdom`, `charisma`)
- `hit_points` (`object` containing `current` and `max`)
#### Full Property Breakdown:
| Group | Field | Type | Description / Example |
| :--- | :--- | :--- | :--- |
| **Identity** | `player_name` | `string` | Player name (optional for NPCs/monsters). |
| | `class` | `string` | Primary class (e.g. `"Ranger"`). |
| | `subclass` | `string` | Archetype / subclass (e.g. `"Hunter"`). |
| | `race` | `string` | Character race (e.g. `"Elf"`). |
| | `background` | `string` | Background (e.g. `"Outlander"`). |
| | `alignment` | `string` | Moral alignment (e.g. `"Chaotic Good"`). |
| | `experience_points` | `integer` | Current total XP. |
| | `inspiration` | `boolean` | Inspiration point toggle. |
| **Abilities** | `abilities` | `object` | 6 scores. Can be raw integers or score objects: `{"strength": 12, "dexterity": 16, "constitution": 14, "intelligence": 10, "wisdom": 15, "charisma": 8}`. |
| | `proficiency_bonus` | `integer` | Bonus added to proficient rolls (auto-calculated from level or explicit). |
| | `saving_throws` | `array<string>` | Formatted saves (e.g. `["Strength +3", "Dexterity +5"]`). |
| | `skills` | `array<string>` | Formatted skill proficiencies (e.g. `["Athletics +3", "Stealth +5"]`). |
| | `passive_perception` | `integer` | Passive Wisdom (Perception) score (e.g. `15`). |
| **Combat** | `hit_points` | `object` | `{"current": 24, "max": 24, "temp": 0, "formula": "3d10 + 6"}`. |
| | `hit_dice` | `object` | `{"total": 3, "current": 3, "dice": "1d10"}`. |
| | `death_saves` | `object` | `{"successes": 0, "failures": 0}` (0–3 each). |
| | `speed` | `integer` | Walking speed in feet (e.g. `35`). |
| | `speed_desc` | `string` | Display string including flight/swim (e.g. `"30 ft., fly 60 ft."`). |
| | `initiative` | `integer` | Initiative modifier (usually Dex modifier). |
| | `armor_class` | `integer\|object` | Total AC (e.g. `15`). |
| | `equipped_armor` | `object\|null` | Snapshot: `{"name": "Studded Leather", "ac_base": 12, "dex_cap": null}`. |
| | `equipped_shield` | `object\|null` | Snapshot: `{"name": "Shield", "ac_bonus": 2}`. |
| | `attacks` | `array<object>` | Weapon/spell attack rows: `[{"name": "Longbow", "atk_bonus": "+7", "damage": "1d8 + 3", "damage_type": "Piercing", "range": "150/600 ft."}]`. |
| **Inventory & Wealth** | `currency` | `object` | `{"cp": 15, "sp": 40, "ep": 0, "gp": 25, "pp": 0}`. |
| | `inventory` | `array<object>` | Carried items matching `item.json` schema. |
| **Roleplay** | `personality_traits` | `string` | Personality trait text. |
| | `ideals` | `string` | Character ideals. |
| | `bonds` | `string` | Character bonds. |
| | `flaws` | `string` | Character flaws. |
| | `features_and_traits` | `array<object>` | Class/racial features: `[{"name": "Colossus Slayer", "source": "Hunter Archetype", "desc": "..."}]`. |
| **Spellcasting** | `spellcasting` | `object` | `{"class": "Ranger", "ability": "Wisdom", "save_dc": 13, "attack_bonus": 5, "cantrips": [], "slots": {"1": {"total": 3, "expended": 1}}, "spells": [...]}`. |
| **NPC / Monster** | `challenge_rating` | `number\|string` | CR (e.g. `0.25`, `"1/4"`, `14`). |
| | `actions` | `array<object>` | Monster stat block actions: `[{"name": "Scimitar", "desc": "Melee Weapon Attack..."}]`. |
| | `reactions` | `array<object>` | Creature reactions. |
| | `legendary_actions` | `array<object>` | Creature legendary actions. |
#### Complete Export/Import Example (`character`):
```json
{
"$schema": "../../../schema/entity.json",
"uuid": "4c8f3824-3453-48bf-9a0e-a4c336b3bcf2",
"ruleset_id": "open5e",
"entity_type": "character",
"display_name": "Arannis",
"description": "Elven Ranger exploring ancient ruins.",
"version": "1.0.0",
"tags": ["pc", "ranger", "elf"],
"properties": {
"player_name": "Tom",
"level": 3,
"class": "Ranger",
"subclass": "Hunter",
"race": "Elf",
"background": "Outlander",
"alignment": "Chaotic Good",
"experience_points": 900,
"inspiration": true,
"proficiency_bonus": 2,
"initiative": 3,
"speed": 35,
"hit_points": {
"current": 24,
"max": 24,
"temp": 0,
"formula": "3d10 + 6"
},
"hit_dice": {
"total": 3,
"current": 3,
"dice": "1d10"
},
"death_saves": {
"successes": 0,
"failures": 0
},
"armor_class": 15,
"abilities": {
"strength": 12,
"dexterity": 16,
"constitution": 14,
"intelligence": 10,
"wisdom": 15,
"charisma": 8
},
"saving_throws": ["Strength +3", "Dexterity +5"],
"skills": ["Athletics +3", "Perception +5", "Stealth +5", "Survival +5"],
"passive_perception": 15,
"languages": ["Common", "Elvish", "Sylvan"],
"attacks": [
{
"name": "Longbow",
"atk_bonus": "+7",
"damage": "1d8 + 3",
"damage_type": "Piercing",
"range": "150/600 ft."
}
],
"currency": {
"cp": 15,
"sp": 40,
"ep": 0,
"gp": 25,
"pp": 0
}
}
}
```
---
### 4.2 Base Item Entity (`item.json`)
Used for general adventuring gear, tools, containers, consumables, and magic items.
#### Required Fields in `properties`:
- `name` (`string`)
#### Property Fields:
| Field | Type | Default | Description / Enum |
| :--- | :--- | :--- | :--- |
| `name` | `string` | — | Item name (e.g. `"Rope, Hempen (50 feet)"`). |
| `description` | `string` | `""` | Detailed description or rules text. |
| `cost` | `object` | `{"quantity": 1, "unit": "gp"}` | Value (`quantity`: number, `unit`: `"cp"`, `"sp"`, `"ep"`, `"gp"`, `"pp"`). |
| `weight` | `number` | `0` | Weight in pounds (lbs). |
| `rarity` | `string` | `"common"` | `"common"`, `"uncommon"`, `"rare"`, `"very rare"`, `"legendary"`, `"artifact"`. |
| `attunement` | `boolean` | `false` | Whether the item requires attunement. |
| `equipped` | `boolean` | `false` | Equipped/worn status. |
| `quantity` | `integer` | `1` | Count carried. |
| `grants_modifiers`| `array<object>` | `[]` | Stat bonuses granted when equipped (e.g. `[{"target": "armor_class", "value": 1, "type": "bonus", "source": "Cloak of Protection"}]`). |
#### Example Payload (`item`):
```json
{
"$schema": "../../../schema/entity.json",
"uuid": "8b5123d1-419b-4351-92b0-9b34e56711aa",
"ruleset_id": "open5e",
"entity_type": "item",
"display_name": "Potion of Healing",
"description": "A magical red fluid that restores hit points when consumed.",
"properties": {
"name": "Potion of Healing",
"description": "You regain 2d4 + 2 hit points when you drink this potion.",
"cost": { "quantity": 50, "unit": "gp" },
"weight": 0.5,
"rarity": "common",
"attunement": false,
"equipped": false,
"quantity": 2
}
}
```
---
### 4.3 Weapon Entity (`weapon.json`)
Inherits all fields from `item.json` plus combat damage specifications.
#### Required Fields in `properties`:
- `name` (`string`)
- `damage_dice` (`string`, e.g. `"1d8"`, `"2d6"`)
- `damage_type` (`string`: `"bludgeoning"`, `"piercing"`, or `"slashing"`)
#### Additional Property Fields:
| Field | Type | Default | Description / Enum |
| :--- | :--- | :--- | :--- |
| `weapon_category` | `string` | `"simple"` | `"simple"` or `"martial"`. |
| `weapon_range` | `string` | `"melee"` | `"melee"` or `"ranged"`. |
| `damage_dice` | `string` | `"1d6"` | Damage roll formula (e.g. `"1d8"`, `"2d6"`). |
| `damage_type` | `string` | `"slashing"` | `"bludgeoning"`, `"piercing"`, `"slashing"`. |
| `properties` | `array<string>` | `[]` | 5e weapon properties: `"ammunition"`, `"finesse"`, `"heavy"`, `"light"`, `"loading"`, `"range"`, `"reach"`, `"special"`, `"thrown"`, `"two-handed"`, `"versatile"`. |
| `range_normal` | `integer\|null` | `null` | Normal range in feet (e.g. `20` for dagger, `150` for longbow). |
| `range_long` | `integer\|null` | `null` | Disadvantage range in feet (e.g. `60` for dagger, `600` for longbow). |
| `versatile_dice`| `string\|null` | `null` | Alternate two-handed damage die (e.g. `"1d10"` for longsword/battleaxe). |
#### Example Payload (`weapon`):
```json
{
"$schema": "../../../schema/entity.json",
"uuid": "7f139d42-26cb-4029-a1b7-6ec7e34ef399",
"ruleset_id": "open5e",
"entity_type": "weapon",
"display_name": "Longsword",
"description": "A versatile martial melee blade.",
"properties": {
"name": "Longsword",
"description": "A classic double-edged straight blade with a cruciform hilt.",
"cost": { "quantity": 15, "unit": "gp" },
"weight": 3.0,
"rarity": "common",
"weapon_category": "martial",
"weapon_range": "melee",
"damage_dice": "1d8",
"damage_type": "slashing",
"properties": ["versatile"],
"versatile_dice": "1d10",
"equipped": true,
"quantity": 1
}
}
```
---
### 4.4 Armor Entity (`armor.json`)
Inherits all fields from `item.json` plus protection and armor class modifiers.
#### Required Fields in `properties`:
- `name` (`string`)
- `armor_category` (`string`: `"light"`, `"medium"`, or `"heavy"`)
- `ac_base` (`integer`, minimum `0`)
#### Additional Property Fields:
| Field | Type | Description |
| :--- | :--- | :--- |
| `armor_category` | `string` | `"light"`, `"medium"`, or `"heavy"`. |
| `ac_base` | `integer` | Base AC provided (e.g. `11` for Leather, `14` for Scale Mail, `18` for Plate). |
| `ac_modifier` | `integer` | Bonus over base 10 (e.g. `+1` for Leather, `+4` for Scale Mail, `+8` for Plate). |
| `dex_cap` | `integer\|null` | Max Dex modifier allowed: `null` (uncapped, light armor), `2` (medium armor cap), `0` (heavy armor, no Dex). |
| `strength_requirement` | `integer\|null` | Minimum Strength score required without -10 speed penalty (e.g. `13` or `15`). `null` if none. |
| `stealth_disadvantage` | `boolean` | `true` if wearing this armor imposes disadvantage on Stealth checks. |
#### Example Payload (`armor`):
```json
{
"$schema": "../../../schema/entity.json",
"uuid": "21950d81-817b-40fa-9861-bbd51381aa01",
"ruleset_id": "open5e",
"entity_type": "armor",
"display_name": "Breastplate",
"description": "Fitted metal chest armor with leather backing.",
"properties": {
"name": "Breastplate",
"description": "Protects the wearer's torso while leaving limbs free.",
"cost": { "quantity": 400, "unit": "gp" },
"weight": 20.0,
"rarity": "common",
"armor_category": "medium",
"ac_base": 14,
"dex_cap": 2,
"strength_requirement": null,
"stealth_disadvantage": false,
"equipped": true,
"quantity": 1
}
}
```
---
### 4.5 Shield Entity (`shield.json`)
Inherits all fields from `item.json` plus shield-specific AC bonus and penalties.
#### Required Fields in `properties`:
- `name` (`string`)
- `ac_bonus` (`integer`, standard 5e value is `2`)
#### Additional Property Fields:
| Field | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `ac_bonus` | `integer` | `2` | Armor Class bonus added when wielded. |
| `strength_requirement` | `integer\|null` | `null` | Minimum Strength required to wield without penalty. |
| `stealth_disadvantage` | `boolean` | `false` | Imposes disadvantage on Stealth rolls if `true`. |
#### Example Payload (`shield`):
```json
{
"$schema": "../../../schema/entity.json",
"uuid": "43e2f901-b841-47fa-89fa-948123da6711",
"ruleset_id": "open5e",
"entity_type": "shield",
"display_name": "Shield",
"description": "A wooden or metal shield carried in one hand.",
"properties": {
"name": "Shield",
"description": "Wielding a shield increases your Armor Class by 2.",
"cost": { "quantity": 10, "unit": "gp" },
"weight": 6.0,
"rarity": "common",
"ac_bonus": 2,
"strength_requirement": null,
"stealth_disadvantage": false,
"equipped": true,
"quantity": 1
}
}
```
---
### 4.6 Spell Entity (`spell.json`)
Defines complete 5e spells, cantrips, and rituals.
#### Required Fields in `properties`:
- `level` (`integer`, 0–9; 0 represents cantrips)
- `school` (`string`: abjuration, conjuration, divination, enchantment, evocation, illusion, necromancy, transmutation)
- `casting_time` (`string`, e.g. `"1 action"`, `"1 bonus action"`, `"1 reaction"`)
- `range` (`string`, e.g. `"Self"`, `"Touch"`, `"60 feet"`, `"120 feet"`)
- `duration` (`string`, e.g. `"Instantaneous"`, `"Concentration, up to 1 minute"`)
- `description` (`string`, the full rules text)
#### Additional Property Fields:
| Field | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `components` | `object` | `{"verbal": true, "somatic": true, "material": false}` | Component breakdown: `verbal` (`bool`), `somatic` (`bool`), `material` (`bool`), `material_specified` (`string`). |
| `concentration` | `boolean` | `false` | Requires spellcaster concentration. |
| `ritual` | `boolean` | `false` | Can be cast as a 10-minute ritual without expending a spell slot. |
| `higher_levels` | `string` | `""` | Additional damage or target effects when upcast with higher level slots. |
#### Example Payload (`spell`):
```json
{
"$schema": "../../../schema/entity.json",
"uuid": "e58123f0-49a1-4322-9fa1-01f4218aef82",
"ruleset_id": "open5e",
"entity_type": "spell",
"display_name": "Fireball",
"description": "A bright streak that blossoms with a low roar into an explosion of flame.",
"properties": {
"level": 3,
"school": "evocation",
"casting_time": "1 action",
"range": "150 feet",
"duration": "Instantaneous",
"components": {
"verbal": true,
"somatic": true,
"material": true,
"material_specified": "a tiny ball of bat guano and sulfur"
},
"concentration": false,
"ritual": false,
"description": "Each creature in a 20-foot-radius sphere centered on that point must make a Dexterity saving throw. A target takes 8d6 fire damage on a failed save, or half as much damage on a successful one.",
"higher_levels": "When you cast this spell using a spell slot of 4th level or higher, the damage increases by 1d6 for each slot level above 3rd."
}
}
```
---
## 5. Validation & Conformance Rules
When parsing or validating imported entities, external apps should follow these rules:
1. **Top-Level Envelope Validation**:
- Verify `ruleset_id === "open5e"`.
- Ensure `entity_type` matches one of the 6 registered entity types (`character`, `item`, `weapon`, `armor`, `shield`, `spell`).
- Ensure `uuid` is a valid unique string or UUID v4.
2. **Sub-Schema Dispatch**:
- Extract `properties` and validate against the corresponding `entities/{entity_type}.json` schema.
3. **Graceful Defaults**:
- If an optional field is missing (such as `temp` HP or `currency.ep`), assume default 0 or empty structures rather than throwing a validation exception.
4. **Extensibility**:
- Extra custom fields in `properties` are permitted (`additionalProperties: true` in base schemas) to allow apps to preserve their own proprietary metadata (e.g. inventory IDs, campaign notes, asset URLs) without breaking import/export compatibility.
---
## 6. Import & Export Implementation Checklist
When implementing an integration adapter in another application:
- [ ] **Export Checklist**:
- [ ] Generate or preserve a stable `uuid` (UUID v4).
- [ ] Set `"ruleset_id": "open5e"`.
- [ ] Set `"entity_type"` to the exact entity type slug.
- [ ] Populate `"display_name"` with the character/item name.
- [ ] Place all game attributes inside the `"properties"` root object.
- [ ] Export numbers as JSON numbers (not stringified quotes, except for dice formulas like `"1d8 + 3"`).
- [ ] Format ISO 8601 dates for `created_at` and `updated_at`.
- [ ] **Import Checklist**:
- [ ] Check `"ruleset_id" === "open5e"`.
- [ ] Inspect `"entity_type"` to determine character vs. item vs. spell importer.
- [ ] Read `properties.abilities` (handle both raw numeric maps and modifier objects).
- [ ] Safely read `properties.hit_points` (`current`, `max`, `temp`).
- [ ] Safely import or map `properties.attacks` and `properties.currency`.
- [ ] If importing items or weapons into a character inventory, unpack nested items into the character's equipment list.

View File

@@ -0,0 +1,127 @@
{
"$schema": "../../schema/manifest.json",
"id": "open5e",
"uuid": "7a35e4d2-f67b-4890-a292-6a7593c72b21",
"name": "Open5e (D&D 5th Edition SRD)",
"version": "1.0.0",
"description": "5th Edition System Reference Document ruleset providing entity schemas and sheet templates.",
"license": "OGL 1.0a",
"author": {
"name": "Open5e Community",
"url": "https://open5e.com"
},
"entities": [
{
"id": "character",
"name": "Player Character",
"description": "A player character with attributes, proficiencies, classes, equipment, and spells.",
"schema": "entities/character.json",
"default_template": "character_sheet",
"templates": [
"character_sheet",
"character_sheet_alt",
"spellcasting_sheet",
"stat_block",
"character_card",
"character_list_item"
]
},
{
"id": "item",
"name": "Item / Gear",
"description": "Generic items, equipment, adventuring gear, and magical items.",
"schema": "entities/item.json"
},
{
"id": "weapon",
"name": "Weapon",
"description": "Simple and martial melee and ranged weapons.",
"schema": "entities/weapon.json"
},
{
"id": "armor",
"name": "Armor",
"description": "Wearable armor with base AC, dex caps, and strength/stealth rules.",
"schema": "entities/armor.json"
},
{
"id": "shield",
"name": "Shield",
"description": "Equippable shields providing armor class bonuses.",
"schema": "entities/shield.json"
},
{
"id": "spell",
"name": "Spell",
"description": "5e Spells with level, school, casting time, range, and components.",
"schema": "entities/spell.json"
}
],
"templates": [
{
"id": "character_sheet",
"name": "Standard Character Sheet",
"description": "Full interactive 5e character sheet matching the official 3-column print layout.",
"entity_types": ["character"],
"mode": "sheet",
"engine": "html",
"html": "templates/character_sheet.html",
"css": ["templates/character_sheet.css"],
"js": ["templates/character_sheet.js"]
},
{
"id": "character_sheet_alt",
"name": "Alternative Character Sheet",
"description": "5e character sheet with saving throws and skills grouped by ability score.",
"entity_types": ["character"],
"mode": "sheet",
"engine": "html",
"html": "templates/character_sheet_alt.html",
"css": ["templates/character_sheet_alt.css"]
},
{
"id": "spellcasting_sheet",
"name": "Spellcasting Sheet",
"description": "Dedicated 5e spellcasting sheet with cantrips, spell slots 1-9, and spell save DC.",
"entity_types": ["character"],
"mode": "sheet",
"engine": "html",
"html": "templates/spellcasting_sheet.html",
"css": ["templates/spellcasting_sheet.css"]
},
{
"id": "stat_block",
"name": "Creature Stat Block",
"description": "Monster manual style stat block with full actions, traits, and combat statistics.",
"entity_types": ["character"],
"mode": "view",
"engine": "html",
"html": "templates/stat_block.html",
"css": ["templates/stat_block.css"]
},
{
"id": "character_card",
"name": "NPC Character Card",
"description": "Compact NPC card with 8 stat boxes and quick action summaries.",
"entity_types": ["character"],
"mode": "card",
"engine": "html",
"html": "templates/character_card.html",
"css": ["templates/character_card.css"]
},
{
"id": "character_list_item",
"name": "Character List Item",
"description": "Uniform fixed-height quick-reference list item card with non-wrapping horizontal action row.",
"entity_types": ["character"],
"mode": "card",
"engine": "html",
"html": "templates/character_list_item.html",
"css": ["templates/character_list_item.css"]
}
],
"assets": {
"path": "assets",
"images": []
}
}

View File

@@ -0,0 +1,155 @@
.card-page-wrapper {
margin: 0;
padding: 20px;
background-color: #f4f6f8;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
display: flex;
justify-content: center;
}
.npc-card {
width: 100%;
max-width: 580px;
background: #ffffff;
border: 1.5px solid #922610;
border-radius: 4px;
padding: 16px 18px 18px;
box-sizing: border-box;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
.card-top {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.card-name {
margin: 0;
font-size: 22px;
font-weight: 700;
color: #922610;
line-height: 1.15;
}
.card-cr {
font-size: 18px;
font-weight: 500;
color: #555;
white-space: nowrap;
}
.card-subtitle {
font-style: italic;
font-size: 13.5px;
color: #666;
margin: 4px 0 14px 0;
}
.stat-boxes-row {
display: flex;
gap: 6px;
justify-content: space-between;
margin-bottom: 14px;
}
.stat-box {
flex: 1;
position: relative;
border: 1.5px solid #222;
border-radius: 2px;
padding: 6px 2px 4px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 48px;
background: #ffffff;
box-sizing: border-box;
}
.badge-advantage {
position: absolute;
top: 1.5px;
left: 2.5px;
font-size: 8.5px;
font-weight: 800;
color: #111;
line-height: 1;
}
.badge-proficiency {
position: absolute;
top: 1.5px;
right: 2.5px;
font-size: 8.5px;
font-weight: 800;
color: #111;
line-height: 1;
}
.stat-mod {
font-size: 15px;
font-weight: 700;
color: #111;
line-height: 1.1;
}
.stat-code {
font-size: 9.5px;
font-weight: 600;
color: #333;
margin-top: 3px;
text-transform: uppercase;
}
.action-boxes-grid {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.action-card-box {
flex: 1 1 calc(50% - 8px);
min-width: 140px;
border: 1px solid #333;
border-radius: 2px;
padding: 8px 10px;
background: #ffffff;
box-sizing: border-box;
}
.action-box-title {
font-size: 13.5px;
font-weight: 700;
color: #222;
margin-bottom: 4px;
}
.action-box-desc {
font-size: 11.5px;
line-height: 1.35;
color: #444;
}
/* Single action takes full width */
.action-boxes-grid > .action-card-box:only-child {
flex: 1 1 100%;
}
@media (max-width: 440px) {
.card-page-wrapper {
padding: 8px;
}
.stat-boxes-row {
gap: 3px;
}
.stat-mod {
font-size: 13px;
}
.action-card-box {
flex: 1 1 100%;
}
}

View File

@@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{display_name}} - Character Card</title>
<link rel="stylesheet" href="character_card.css">
</head>
<body class="card-page-wrapper">
<div class="npc-card">
<!-- Top Bar: Name & Challenge/Level -->
<div class="card-top">
<h2 class="card-name">{{display_name}}</h2>
<div class="card-cr">
{{#if properties.challenge_rating}}
CR {{properties.challenge_rating}}
{{else if properties.cr}}
CR {{properties.cr}}
{{else if properties.level}}
LVL {{properties.level}}
{{else}}
CR 1
{{/if}}
</div>
</div>
<!-- Subtitle: Size, Type, Subtype, Alignment -->
<div class="card-subtitle">
{{#if properties.size}}{{properties.size}}{{else}}medium{{/if}}
{{#if properties.type}}{{properties.type}}{{else}}Humanoid{{/if}}{{#if properties.subtype}} ({{properties.subtype}}){{else if properties.race}} ({{properties.race}}){{/if}},
{{#if properties.alignment}}{{properties.alignment}}{{else}}unaligned{{/if}}
</div>
<!-- 8 Stat Boxes Row: S, D, C, I, W, Ch, AC, HP -->
<div class="stat-boxes-row">
<!-- STR -->
<div class="stat-box">
<span class="badge-advantage">{{advBadge properties "str"}}</span>
<span class="badge-proficiency">{{profBadge properties.saving_throws "str"}}</span>
<span class="stat-mod">{{modText properties.abilities.strength}}</span>
<span class="stat-code">S</span>
</div>
<!-- DEX -->
<div class="stat-box">
<span class="badge-advantage">{{advBadge properties "dex"}}</span>
<span class="badge-proficiency">{{profBadge properties.saving_throws "dex"}}</span>
<span class="stat-mod">{{modText properties.abilities.dexterity}}</span>
<span class="stat-code">D</span>
</div>
<!-- CON -->
<div class="stat-box">
<span class="badge-advantage">{{advBadge properties "con"}}</span>
<span class="badge-proficiency">{{profBadge properties.saving_throws "con"}}</span>
<span class="stat-mod">{{modText properties.abilities.constitution}}</span>
<span class="stat-code">C</span>
</div>
<!-- INT -->
<div class="stat-box">
<span class="badge-advantage">{{advBadge properties "int"}}</span>
<span class="badge-proficiency">{{profBadge properties.saving_throws "int"}}</span>
<span class="stat-mod">{{modText properties.abilities.intelligence}}</span>
<span class="stat-code">I</span>
</div>
<!-- WIS -->
<div class="stat-box">
<span class="badge-advantage">{{advBadge properties "wis"}}</span>
<span class="badge-proficiency">{{profBadge properties.saving_throws "wis"}}</span>
<span class="stat-mod">{{modText properties.abilities.wisdom}}</span>
<span class="stat-code">W</span>
</div>
<!-- CHA -->
<div class="stat-box">
<span class="badge-advantage">{{advBadge properties "cha"}}</span>
<span class="badge-proficiency">{{profBadge properties.saving_throws "cha"}}</span>
<span class="stat-mod">{{modText properties.abilities.charisma}}</span>
<span class="stat-code">Ch</span>
</div>
<!-- AC -->
<div class="stat-box">
<span class="stat-mod">{{val properties.armor_class}}</span>
<span class="stat-code">AC</span>
</div>
<!-- HP -->
<div class="stat-box">
<span class="stat-mod">
{{#if properties.hit_points.current}}
{{properties.hit_points.current}}
{{else if properties.hit_points.max}}
{{properties.hit_points.max}}
{{else}}
{{val properties.hit_points}}
{{/if}}
</span>
<span class="stat-code">HP</span>
</div>
</div>
<!-- Action Summary Boxes -->
{{#if properties.actions}}
<div class="action-boxes-grid">
{{#each properties.actions}}
<div class="action-card-box">
<div class="action-box-title">{{this.name}}</div>
<div class="action-box-desc">{{formatText this.desc}}</div>
</div>
{{/each}}
</div>
{{/if}}
</div>
</body>
</html>

View File

@@ -0,0 +1,245 @@
/* Character List Item Template - Uniform Fixed Height with Non-Wrapping Horizontal Actions */
:root {
--card-border: #922610;
--card-title: #922610;
--box-border: #222222;
--bg-page: #f4f6f8;
--bg-card: #ffffff;
--bg-box: #ffffff;
--text-main: #111111;
--text-muted: #666666;
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
body.list-item-page-wrapper {
margin: 0;
padding: 16px;
background-color: var(--bg-page);
font-family: var(--font-sans);
display: flex;
justify-content: center;
}
/* Card has fixed height so all list cards are identical height */
.npc-list-card {
width: 100%;
max-width: 580px;
height: 236px;
background: var(--bg-card);
border: 1.5px solid var(--card-border);
border-radius: 4px;
padding: 12px 14px;
box-sizing: border-box;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Header */
.card-top {
display: flex;
justify-content: space-between;
align-items: baseline;
flex-shrink: 0;
}
.card-name {
margin: 0;
font-size: 20px;
font-weight: 700;
color: var(--card-title);
line-height: 1.15;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-cr {
font-size: 16px;
font-weight: 500;
color: #555;
white-space: nowrap;
margin-left: 8px;
}
.card-subtitle {
font-style: italic;
font-size: 12px;
color: var(--text-muted);
margin: 2px 0 10px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 0;
}
/* 8 Stat Boxes */
.stat-boxes-row {
display: flex;
gap: 5px;
justify-content: space-between;
margin-bottom: 10px;
flex-shrink: 0;
}
.stat-box {
flex: 1;
position: relative;
border: 1.2px solid var(--box-border);
border-radius: 2px;
padding: 4px 1px 3px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 44px;
background: var(--bg-box);
box-sizing: border-box;
}
.badge-advantage {
position: absolute;
top: 1px;
left: 2px;
font-size: 8px;
font-weight: 800;
color: #111;
line-height: 1;
}
.badge-proficiency {
position: absolute;
top: 1px;
right: 2px;
font-size: 8px;
font-weight: 800;
color: #111;
line-height: 1;
}
.stat-mod {
font-size: 14px;
font-weight: 700;
color: var(--text-main);
line-height: 1.1;
}
.stat-code {
font-size: 9px;
font-weight: 600;
color: #333;
margin-top: 2px;
text-transform: uppercase;
}
/* Actions Row: Single Non-Wrapping Row with Horizontal Scrolling */
.action-row-container {
flex: 1;
display: flex;
min-height: 0; /* Allows container to shrink inside flex parent */
overflow: hidden;
}
.action-boxes-row {
display: flex;
flex-wrap: nowrap;
gap: 8px;
width: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
scrollbar-color: #ccc transparent;
-webkit-overflow-scrolling: touch;
padding-bottom: 2px;
}
.action-boxes-row::-webkit-scrollbar {
height: 4px;
}
.action-boxes-row::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 2px;
}
.action-boxes-row::-webkit-scrollbar-track {
background: transparent;
}
/* Individual Action Card */
.action-card-box {
flex: 0 0 170px;
max-width: 240px;
border: 1px solid #333;
border-radius: 2px;
padding: 6px 8px;
background: #ffffff;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* When only 1 action exists (e.g. Acolyte Club), expand full width */
.action-boxes-row > .action-card-box:only-child {
flex: 1 0 100%;
max-width: 100%;
}
.action-box-title {
font-size: 12.5px;
font-weight: 700;
color: #222;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 0;
}
.action-box-desc {
font-size: 10.5px;
line-height: 1.25;
color: #444;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
}
/* Empty State (e.g. Unnamed Monster) */
.no-actions-box {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #ccc;
border-radius: 2px;
background: #fafafa;
}
.no-actions-text {
font-size: 11px;
color: #999;
font-style: italic;
}
/* Responsive */
@media (max-width: 440px) {
.list-item-page-wrapper {
padding: 6px;
}
.stat-boxes-row {
gap: 3px;
}
.stat-mod {
font-size: 12px;
}
.action-card-box {
flex: 0 0 145px;
}
}

View File

@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="/rulesets/open5e/templates/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{display_name}} - List Item</title>
<link rel="stylesheet" href="character_list_item.css">
</head>
<body class="list-item-page-wrapper">
<div class="npc-list-card">
<!-- Header: Name & Challenge/Level -->
<div class="card-top">
<h2 class="card-name">{{display_name}}</h2>
<div class="card-cr">
{{#if properties.challenge_rating}}
CR {{properties.challenge_rating}}
{{else if properties.cr}}
CR {{properties.cr}}
{{else if properties.level}}
LVL {{properties.level}}
{{else}}
CR 1
{{/if}}
</div>
</div>
<!-- Subtitle: Size, Type, Subtype, Alignment -->
<div class="card-subtitle">
{{#if properties.size}}{{properties.size}}{{else}}medium{{/if}}
{{#if properties.type}}{{properties.type}}{{else}}Humanoid{{/if}}{{#if properties.subtype}} ({{properties.subtype}}){{else if properties.race}} ({{properties.race}}){{/if}},
{{#if properties.alignment}}{{properties.alignment}}{{else}}unaligned{{/if}}
</div>
<!-- 8 Stat Boxes Row: S, D, C, I, W, Ch, AC, HP -->
<div class="stat-boxes-row">
<!-- STR -->
<div class="stat-box">
<span class="badge-advantage">{{advBadge properties "str"}}</span>
<span class="badge-proficiency">{{profBadge properties.saving_throws "str"}}</span>
<span class="stat-mod">{{modText properties.abilities.strength}}</span>
<span class="stat-code">S</span>
</div>
<!-- DEX -->
<div class="stat-box">
<span class="badge-advantage">{{advBadge properties "dex"}}</span>
<span class="badge-proficiency">{{profBadge properties.saving_throws "dex"}}</span>
<span class="stat-mod">{{modText properties.abilities.dexterity}}</span>
<span class="stat-code">D</span>
</div>
<!-- CON -->
<div class="stat-box">
<span class="badge-advantage">{{advBadge properties "con"}}</span>
<span class="badge-proficiency">{{profBadge properties.saving_throws "con"}}</span>
<span class="stat-mod">{{modText properties.abilities.constitution}}</span>
<span class="stat-code">C</span>
</div>
<!-- INT -->
<div class="stat-box">
<span class="badge-advantage">{{advBadge properties "int"}}</span>
<span class="badge-proficiency">{{profBadge properties.saving_throws "int"}}</span>
<span class="stat-mod">{{modText properties.abilities.intelligence}}</span>
<span class="stat-code">I</span>
</div>
<!-- WIS -->
<div class="stat-box">
<span class="badge-advantage">{{advBadge properties "wis"}}</span>
<span class="badge-proficiency">{{profBadge properties.saving_throws "wis"}}</span>
<span class="stat-mod">{{modText properties.abilities.wisdom}}</span>
<span class="stat-code">W</span>
</div>
<!-- CHA -->
<div class="stat-box">
<span class="badge-advantage">{{advBadge properties "cha"}}</span>
<span class="badge-proficiency">{{profBadge properties.saving_throws "cha"}}</span>
<span class="stat-mod">{{modText properties.abilities.charisma}}</span>
<span class="stat-code">Ch</span>
</div>
<!-- AC -->
<div class="stat-box">
<span class="stat-mod">{{val properties.armor_class}}</span>
<span class="stat-code">AC</span>
</div>
<!-- HP -->
<div class="stat-box">
<span class="stat-mod">
{{#if properties.hit_points.current}}
{{properties.hit_points.current}}
{{else if properties.hit_points.max}}
{{properties.hit_points.max}}
{{else}}
{{val properties.hit_points}}
{{/if}}
</span>
<span class="stat-code">HP</span>
</div>
</div>
<!-- Actions Row: Single Non-Wrapping Row with Horizontal Scroll -->
<div class="action-row-container">
{{#if properties.actions}}
<div class="action-boxes-row">
{{#each properties.actions}}
<div class="action-card-box">
<div class="action-box-title">{{this.name}}</div>
<div class="action-box-desc">{{formatText this.desc}}</div>
</div>
{{/each}}
</div>
{{else}}
<div class="no-actions-box">
<span class="no-actions-text">&mdash; No listed actions &mdash;</span>
</div>
{{/if}}
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,585 @@
/* D&D 5e Standard Character Sheet Styles */
:root {
--border-color: #2b2b2b;
--accent-red: #922610;
--bg-page: #eaedf0;
--bg-sheet: #ffffff;
--bg-box: #f9fbfd;
--text-main: #1e1e1e;
--text-muted: #666666;
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--font-serif: "Libre Baskerville", "Georgia", serif;
}
body.sheet-page-wrapper {
margin: 0;
padding: 24px;
background-color: var(--bg-page);
font-family: var(--font-sans);
color: var(--text-main);
display: flex;
justify-content: center;
box-sizing: border-box;
}
.character-sheet-container {
width: 100%;
max-width: 1050px;
background: var(--bg-sheet);
border: 2px solid var(--border-color);
border-radius: 6px;
padding: 20px 24px 24px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
box-sizing: border-box;
}
/* ================= HEADER ================= */
.sheet-header {
display: grid;
grid-template-columns: 320px 1fr;
gap: 16px;
align-items: stretch;
border-bottom: 2px solid var(--border-color);
padding-bottom: 16px;
margin-bottom: 20px;
}
.header-name-box {
border: 1.5px solid var(--border-color);
border-radius: 4px;
padding: 12px 16px;
display: flex;
flex-direction: column;
justify-content: flex-end;
background: var(--bg-box);
}
.character-name {
font-family: var(--font-serif);
font-size: 26px;
color: var(--accent-red);
margin: 0 0 6px 0;
line-height: 1.1;
word-break: break-word;
}
.header-label {
font-size: 9.5px;
font-weight: bold;
color: var(--text-muted);
letter-spacing: 0.5px;
}
.header-details-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 12px;
border: 1.5px solid var(--border-color);
border-radius: 4px;
padding: 10px 14px;
background: var(--bg-box);
}
.detail-cell {
display: flex;
flex-direction: column;
justify-content: flex-end;
border-bottom: 1px solid #ccc;
padding-bottom: 3px;
}
.detail-val {
font-size: 13.5px;
font-weight: 600;
color: var(--text-main);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 2px;
}
/* ================= 3-COLUMN MAIN GRID ================= */
.sheet-main-grid {
display: grid;
grid-template-columns: 320px 340px 1fr;
gap: 16px;
}
.sheet-col {
display: flex;
flex-direction: column;
gap: 12px;
}
/* Generic Section Box */
.section-box {
border: 1.5px solid var(--border-color);
border-radius: 4px;
padding: 10px 12px;
background: #ffffff;
position: relative;
box-sizing: border-box;
}
.box-bottom-label {
text-align: center;
font-size: 9.5px;
font-weight: bold;
color: var(--text-muted);
letter-spacing: 0.5px;
margin-top: 6px;
padding-top: 4px;
border-top: 1px solid #eee;
}
/* ================= LEFT COLUMN ================= */
.abilities-and-skills-row {
display: grid;
grid-template-columns: 88px 1fr;
gap: 12px;
}
/* 6 Abilities */
.abilities-stack {
display: flex;
flex-direction: column;
gap: 10px;
}
.ability-box {
border: 1.5px solid var(--border-color);
border-radius: 6px;
padding: 6px 4px 4px;
display: flex;
flex-direction: column;
align-items: center;
background: var(--bg-box);
text-align: center;
}
.ab-title {
font-size: 8.5px;
font-weight: bold;
color: var(--accent-red);
letter-spacing: 0.5px;
}
.ab-modifier {
font-size: 20px;
font-weight: bold;
color: var(--text-main);
margin: 2px 0;
line-height: 1;
}
.ab-score-pill {
font-size: 11px;
font-weight: 600;
color: #333;
border: 1px solid #999;
border-radius: 10px;
padding: 1px 8px;
background: #ffffff;
}
/* Skills & Saves Stack */
.skills-and-saves-stack {
display: flex;
flex-direction: column;
gap: 10px;
}
.top-stat-pill {
border: 1.5px solid var(--border-color);
border-radius: 16px;
padding: 4px 10px;
display: flex;
align-items: center;
gap: 8px;
background: var(--bg-box);
}
.pill-checkbox {
width: 14px;
height: 14px;
border: 1.5px solid var(--border-color);
border-radius: 50%;
display: inline-block;
}
.pill-checkbox.checked {
background-color: var(--accent-red);
}
.pill-value {
font-size: 13px;
font-weight: bold;
color: var(--text-main);
border-right: 1px solid #ccc;
padding-right: 8px;
}
.pill-label {
font-size: 9px;
font-weight: bold;
color: var(--text-muted);
}
/* Proficiency Lists (Saves & Skills) */
.prof-list {
list-style: none;
margin: 0;
padding: 0;
}
.prof-item {
display: flex;
align-items: center;
font-size: 11.5px;
margin-bottom: 3.5px;
gap: 6px;
line-height: 1.2;
}
.prof-item .dot {
width: 8px;
height: 8px;
border: 1.2px solid var(--border-color);
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
}
.prof-item .dot.filled {
background-color: var(--accent-red);
border-color: var(--accent-red);
}
.prof-bonus {
font-weight: 700;
width: 22px;
text-align: right;
flex-shrink: 0;
}
.prof-name {
color: #333;
}
.skill-attr {
color: #888;
font-size: 10px;
}
/* Passive Perception */
.passive-perception-box {
border: 1.5px solid var(--border-color);
border-radius: 18px;
padding: 6px 12px;
display: flex;
align-items: center;
gap: 10px;
background: var(--bg-box);
}
.passive-score {
font-size: 15px;
font-weight: bold;
color: var(--text-main);
border: 1px solid var(--border-color);
border-radius: 50%;
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
background: #ffffff;
flex-shrink: 0;
}
.passive-label {
font-size: 9.5px;
font-weight: bold;
color: var(--text-muted);
}
.prof-block-content {
font-size: 11.5px;
line-height: 1.45;
}
.prof-line {
margin-bottom: 4px;
}
/* ================= MIDDLE COLUMN ================= */
.combat-top-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.combat-badge {
border: 1.5px solid var(--border-color);
border-radius: 6px;
padding: 8px 4px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: var(--bg-box);
}
.badge-ac {
border-color: var(--accent-red);
}
.badge-value {
font-size: 22px;
font-weight: bold;
color: var(--text-main);
line-height: 1.1;
}
.badge-label {
font-size: 8.5px;
font-weight: bold;
color: var(--text-muted);
margin-top: 3px;
}
/* HP Boxes */
.hp-max-line {
font-size: 10.5px;
color: #666;
border-bottom: 1px solid #eee;
padding-bottom: 4px;
}
.hp-current-val, .temp-hp-val {
font-size: 28px;
font-weight: bold;
text-align: center;
padding: 8px 0;
color: var(--text-main);
}
.hd-and-death-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.half-box {
padding: 6px 8px;
}
.hd-total-line {
font-size: 9.5px;
color: #666;
}
.hd-current-val {
font-size: 18px;
font-weight: bold;
text-align: center;
padding: 4px 0;
}
.death-save-line {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 9px;
font-weight: bold;
margin: 3px 0;
}
.ds-bubbles {
display: flex;
gap: 4px;
}
.ds-bubbles .bubble {
width: 9px;
height: 9px;
border: 1.2px solid var(--border-color);
border-radius: 50%;
display: inline-block;
}
.ds-bubbles .bubble.filled {
background-color: var(--accent-red);
}
/* Attacks Table */
.attacks-table {
width: 100%;
border-collapse: collapse;
font-size: 11px;
margin-bottom: 8px;
}
.attacks-table th {
font-size: 8.5px;
font-weight: bold;
color: var(--text-muted);
border-bottom: 1px solid #ccc;
padding: 2px 4px;
text-align: left;
}
.attacks-table td {
padding: 5px 4px;
border-bottom: 1px solid #eee;
}
.atk-name {
font-weight: 600;
}
.atk-bonus {
font-weight: 700;
color: var(--accent-red);
}
.attack-notes {
font-size: 10.5px;
line-height: 1.35;
color: #444;
margin-top: 6px;
max-height: 120px;
overflow-y: auto;
}
.attack-note-item {
margin: 4px 0;
}
/* Equipment */
.equipment-layout {
display: grid;
grid-template-columns: 56px 1fr;
gap: 8px;
}
.coins-stack {
display: flex;
flex-direction: column;
gap: 4px;
}
.coin-row {
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 2px 4px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 10px;
background: var(--bg-box);
}
.coin-tag {
font-weight: bold;
font-size: 8.5px;
color: var(--accent-red);
}
.coin-val {
font-weight: 600;
}
.items-list-container {
max-height: 180px;
overflow-y: auto;
}
.items-list {
list-style: none;
margin: 0;
padding: 0;
font-size: 11px;
}
.item-row {
display: flex;
justify-content: space-between;
padding: 3px 0;
border-bottom: 1px solid #eee;
}
.equipped-tag {
color: var(--accent-red);
font-weight: bold;
}
.item-meta {
color: #777;
font-size: 10px;
}
/* ================= RIGHT COLUMN ================= */
.roleplay-box {
min-height: 64px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.box-content {
font-size: 11.5px;
line-height: 1.4;
color: #333;
}
.box-content p {
margin: 0;
}
.empty-hint {
color: #999;
font-style: italic;
font-size: 11px;
}
.features-box {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 250px;
}
.features-content {
font-size: 11.5px;
line-height: 1.4;
}
.feature-item {
margin-bottom: 8px;
}
.feature-name {
color: var(--accent-red);
display: block;
margin-bottom: 2px;
}
.feature-desc {
margin: 0;
}
/* Responsive Styles */
@media (max-width: 900px) {
.sheet-header {
grid-template-columns: 1fr;
}
.sheet-main-grid {
grid-template-columns: 1fr;
}
.abilities-and-skills-row {
grid-template-columns: 90px 1fr;
}
}

View File

@@ -0,0 +1,511 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="/rulesets/open5e/templates/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{display_name}} - Character Sheet</title>
<link rel="stylesheet" href="character_sheet.css">
</head>
<body class="sheet-page-wrapper">
<div class="character-sheet-container">
<!-- Top Header Banner -->
<header class="sheet-header">
<div class="header-name-box">
<h1 class="character-name">{{display_name}}</h1>
<label class="header-label">CHARACTER NAME</label>
</div>
<div class="header-details-grid">
<div class="detail-cell">
<div class="detail-val">{{properties.class}}{{#if properties.subclass}} ({{properties.subclass}}){{/if}} {{properties.level}}</div>
<label class="header-label">CLASS &amp; LEVEL</label>
</div>
<div class="detail-cell">
<div class="detail-val">{{#if properties.background}}{{properties.background}}{{else}}&mdash;{{/if}}</div>
<label class="header-label">BACKGROUND</label>
</div>
<div class="detail-cell">
<div class="detail-val">{{#if properties.player_name}}{{properties.player_name}}{{else}}&mdash;{{/if}}</div>
<label class="header-label">PLAYER NAME</label>
</div>
<div class="detail-cell">
<div class="detail-val">{{properties.race}}</div>
<label class="header-label">RACE</label>
</div>
<div class="detail-cell">
<div class="detail-val">{{properties.alignment}}</div>
<label class="header-label">ALIGNMENT</label>
</div>
<div class="detail-cell">
<div class="detail-val">{{#if properties.experience_points}}{{properties.experience_points}}{{else}}0{{/if}} XP</div>
<label class="header-label">EXPERIENCE POINTS</label>
</div>
</div>
</header>
<!-- Main 3-Column Body -->
<main class="sheet-main-grid">
<!-- ================= LEFT COLUMN ================= -->
<section class="sheet-col col-left">
<div class="abilities-and-skills-row">
<!-- 6 Ability Score Boxes -->
<div class="abilities-stack">
<!-- STR -->
<div class="ability-box">
<span class="ab-title">STRENGTH</span>
<span class="ab-modifier">{{modText properties.abilities.strength}}</span>
<div class="ab-score-pill">{{val properties.abilities.strength}}</div>
</div>
<!-- DEX -->
<div class="ability-box">
<span class="ab-title">DEXTERITY</span>
<span class="ab-modifier">{{modText properties.abilities.dexterity}}</span>
<div class="ab-score-pill">{{val properties.abilities.dexterity}}</div>
</div>
<!-- CON -->
<div class="ability-box">
<span class="ab-title">CONSTITUTION</span>
<span class="ab-modifier">{{modText properties.abilities.constitution}}</span>
<div class="ab-score-pill">{{val properties.abilities.constitution}}</div>
</div>
<!-- INT -->
<div class="ability-box">
<span class="ab-title">INTELLIGENCE</span>
<span class="ab-modifier">{{modText properties.abilities.intelligence}}</span>
<div class="ab-score-pill">{{val properties.abilities.intelligence}}</div>
</div>
<!-- WIS -->
<div class="ability-box">
<span class="ab-title">WISDOM</span>
<span class="ab-modifier">{{modText properties.abilities.wisdom}}</span>
<div class="ab-score-pill">{{val properties.abilities.wisdom}}</div>
</div>
<!-- CHA -->
<div class="ability-box">
<span class="ab-title">CHARISMA</span>
<span class="ab-modifier">{{modText properties.abilities.charisma}}</span>
<div class="ab-score-pill">{{val properties.abilities.charisma}}</div>
</div>
</div>
<!-- Proficiency, Inspiration, Saves & Skills -->
<div class="skills-and-saves-stack">
<!-- Inspiration & Proficiency Bonus -->
<div class="top-stat-pill">
<span class="pill-checkbox {{#if properties.inspiration}}checked{{/if}}"></span>
<span class="pill-label">INSPIRATION</span>
</div>
<div class="top-stat-pill">
<span class="pill-value">+{{calcProf properties.level properties.cr}}</span>
<span class="pill-label">PROFICIENCY BONUS</span>
</div>
<!-- Saving Throws Box -->
<div class="section-box saves-box">
<ul class="prof-list">
<li class="prof-item">
<span class="dot {{#if (hasSave properties.saving_throws 'str')}}filled{{/if}}"></span>
<span class="prof-bonus">{{saveBonus properties 'strength'}}</span>
<span class="prof-name">Strength</span>
</li>
<li class="prof-item">
<span class="dot {{#if (hasSave properties.saving_throws 'dex')}}filled{{/if}}"></span>
<span class="prof-bonus">{{saveBonus properties 'dexterity'}}</span>
<span class="prof-name">Dexterity</span>
</li>
<li class="prof-item">
<span class="dot {{#if (hasSave properties.saving_throws 'con')}}filled{{/if}}"></span>
<span class="prof-bonus">{{saveBonus properties 'constitution'}}</span>
<span class="prof-name">Constitution</span>
</li>
<li class="prof-item">
<span class="dot {{#if (hasSave properties.saving_throws 'int')}}filled{{/if}}"></span>
<span class="prof-bonus">{{saveBonus properties 'intelligence'}}</span>
<span class="prof-name">Intelligence</span>
</li>
<li class="prof-item">
<span class="dot {{#if (hasSave properties.saving_throws 'wis')}}filled{{/if}}"></span>
<span class="prof-bonus">{{saveBonus properties 'wisdom'}}</span>
<span class="prof-name">Wisdom</span>
</li>
<li class="prof-item">
<span class="dot {{#if (hasSave properties.saving_throws 'cha')}}filled{{/if}}"></span>
<span class="prof-bonus">{{saveBonus properties 'charisma'}}</span>
<span class="prof-name">Charisma</span>
</li>
</ul>
<div class="box-bottom-label">SAVING THROWS</div>
</div>
<!-- Skills Box (Alphabetical 18 Skills) -->
<div class="section-box skills-box">
<ul class="prof-list">
<li class="prof-item">
<span class="dot {{#if (isSkillProf properties.skills 'acrobatics')}}filled{{/if}}"></span>
<span class="prof-bonus">{{skillBonus properties 'acrobatics' 'dexterity'}}</span>
<span class="prof-name">Acrobatics <small class="skill-attr">(Dex)</small></span>
</li>
<li class="prof-item">
<span class="dot {{#if (isSkillProf properties.skills 'animal_handling')}}filled{{/if}}"></span>
<span class="prof-bonus">{{skillBonus properties 'animal_handling' 'wisdom'}}</span>
<span class="prof-name">Animal Handling <small class="skill-attr">(Wis)</small></span>
</li>
<li class="prof-item">
<span class="dot {{#if (isSkillProf properties.skills 'arcana')}}filled{{/if}}"></span>
<span class="prof-bonus">{{skillBonus properties 'arcana' 'intelligence'}}</span>
<span class="prof-name">Arcana <small class="skill-attr">(Int)</small></span>
</li>
<li class="prof-item">
<span class="dot {{#if (isSkillProf properties.skills 'athletics')}}filled{{/if}}"></span>
<span class="prof-bonus">{{skillBonus properties 'athletics' 'strength'}}</span>
<span class="prof-name">Athletics <small class="skill-attr">(Str)</small></span>
</li>
<li class="prof-item">
<span class="dot {{#if (isSkillProf properties.skills 'deception')}}filled{{/if}}"></span>
<span class="prof-bonus">{{skillBonus properties 'deception' 'charisma'}}</span>
<span class="prof-name">Deception <small class="skill-attr">(Cha)</small></span>
</li>
<li class="prof-item">
<span class="dot {{#if (isSkillProf properties.skills 'history')}}filled{{/if}}"></span>
<span class="prof-bonus">{{skillBonus properties 'history' 'intelligence'}}</span>
<span class="prof-name">History <small class="skill-attr">(Int)</small></span>
</li>
<li class="prof-item">
<span class="dot {{#if (isSkillProf properties.skills 'insight')}}filled{{/if}}"></span>
<span class="prof-bonus">{{skillBonus properties 'insight' 'wisdom'}}</span>
<span class="prof-name">Insight <small class="skill-attr">(Wis)</small></span>
</li>
<li class="prof-item">
<span class="dot {{#if (isSkillProf properties.skills 'intimidation')}}filled{{/if}}"></span>
<span class="prof-bonus">{{skillBonus properties 'intimidation' 'charisma'}}</span>
<span class="prof-name">Intimidation <small class="skill-attr">(Cha)</small></span>
</li>
<li class="prof-item">
<span class="dot {{#if (isSkillProf properties.skills 'investigation')}}filled{{/if}}"></span>
<span class="prof-bonus">{{skillBonus properties 'investigation' 'intelligence'}}</span>
<span class="prof-name">Investigation <small class="skill-attr">(Int)</small></span>
</li>
<li class="prof-item">
<span class="dot {{#if (isSkillProf properties.skills 'medicine')}}filled{{/if}}"></span>
<span class="prof-bonus">{{skillBonus properties 'medicine' 'wisdom'}}</span>
<span class="prof-name">Medicine <small class="skill-attr">(Wis)</small></span>
</li>
<li class="prof-item">
<span class="dot {{#if (isSkillProf properties.skills 'nature')}}filled{{/if}}"></span>
<span class="prof-bonus">{{skillBonus properties 'nature' 'intelligence'}}</span>
<span class="prof-name">Nature <small class="skill-attr">(Int)</small></span>
</li>
<li class="prof-item">
<span class="dot {{#if (isSkillProf properties.skills 'perception')}}filled{{/if}}"></span>
<span class="prof-bonus">{{skillBonus properties 'perception' 'wisdom'}}</span>
<span class="prof-name">Perception <small class="skill-attr">(Wis)</small></span>
</li>
<li class="prof-item">
<span class="dot {{#if (isSkillProf properties.skills 'performance')}}filled{{/if}}"></span>
<span class="prof-bonus">{{skillBonus properties 'performance' 'charisma'}}</span>
<span class="prof-name">Performance <small class="skill-attr">(Cha)</small></span>
</li>
<li class="prof-item">
<span class="dot {{#if (isSkillProf properties.skills 'persuasion')}}filled{{/if}}"></span>
<span class="prof-bonus">{{skillBonus properties 'persuasion' 'charisma'}}</span>
<span class="prof-name">Persuasion <small class="skill-attr">(Cha)</small></span>
</li>
<li class="prof-item">
<span class="dot {{#if (isSkillProf properties.skills 'religion')}}filled{{/if}}"></span>
<span class="prof-bonus">{{skillBonus properties 'religion' 'intelligence'}}</span>
<span class="prof-name">Religion <small class="skill-attr">(Int)</small></span>
</li>
<li class="prof-item">
<span class="dot {{#if (isSkillProf properties.skills 'sleight_of_hand')}}filled{{/if}}"></span>
<span class="prof-bonus">{{skillBonus properties 'sleight_of_hand' 'dexterity'}}</span>
<span class="prof-name">Sleight of Hand <small class="skill-attr">(Dex)</small></span>
</li>
<li class="prof-item">
<span class="dot {{#if (isSkillProf properties.skills 'stealth')}}filled{{/if}}"></span>
<span class="prof-bonus">{{skillBonus properties 'stealth' 'dexterity'}}</span>
<span class="prof-name">Stealth <small class="skill-attr">(Dex)</small></span>
</li>
<li class="prof-item">
<span class="dot {{#if (isSkillProf properties.skills 'survival')}}filled{{/if}}"></span>
<span class="prof-bonus">{{skillBonus properties 'survival' 'wisdom'}}</span>
<span class="prof-name">Survival <small class="skill-attr">(Wis)</small></span>
</li>
</ul>
<div class="box-bottom-label">SKILLS</div>
</div>
</div>
</div>
<!-- Passive Perception Box -->
<div class="passive-perception-box">
<span class="passive-score">{{passivePerception properties}}</span>
<span class="passive-label">PASSIVE WISDOM (PERCEPTION)</span>
</div>
<!-- Other Proficiencies & Languages Box -->
<div class="section-box proficiencies-box">
<div class="prof-block-content">
{{#if properties.other_proficiencies_and_languages.armor}}
<div class="prof-line"><strong>Armor:</strong> {{join properties.other_proficiencies_and_languages.armor}}</div>
{{/if}}
{{#if properties.other_proficiencies_and_languages.weapons}}
<div class="prof-line"><strong>Weapons:</strong> {{join properties.other_proficiencies_and_languages.weapons}}</div>
{{/if}}
{{#if properties.other_proficiencies_and_languages.tools}}
<div class="prof-line"><strong>Tools:</strong> {{join properties.other_proficiencies_and_languages.tools}}</div>
{{/if}}
{{#if properties.languages}}
<div class="prof-line"><strong>Languages:</strong> {{join properties.languages}}</div>
{{else if properties.other_proficiencies_and_languages.languages}}
<div class="prof-line"><strong>Languages:</strong> {{join properties.other_proficiencies_and_languages.languages}}</div>
{{/if}}
</div>
<div class="box-bottom-label">OTHER PROFICIENCIES &amp; LANGUAGES</div>
</div>
</section>
<!-- ================= MIDDLE COLUMN ================= -->
<section class="sheet-col col-middle">
<!-- Combat Top Row: AC, Initiative, Speed -->
<div class="combat-top-row">
<div class="combat-badge badge-ac">
<span class="badge-value">{{val properties.armor_class}}</span>
<span class="badge-label">ARMOR CLASS</span>
</div>
<div class="combat-badge badge-init">
<span class="badge-value">{{#if properties.initiative}}{{modSign properties.initiative}}{{else}}{{modText properties.abilities.dexterity}}{{/if}}</span>
<span class="badge-label">INITIATIVE</span>
</div>
<div class="combat-badge badge-speed">
<span class="badge-value">{{#if properties.speed}}{{properties.speed}} ft.{{else}}30 ft.{{/if}}</span>
<span class="badge-label">SPEED</span>
</div>
</div>
<!-- Current Hit Points -->
<div class="section-box hp-box">
<div class="hp-max-line">
Hit Point Maximum: <strong>{{properties.hit_points.max}}</strong>
</div>
<div class="hp-current-val">
{{properties.hit_points.current}}
</div>
<div class="box-bottom-label">CURRENT HIT POINTS</div>
</div>
<!-- Temporary Hit Points -->
<div class="section-box temp-hp-box">
<div class="temp-hp-val">
{{#if properties.hit_points.temp}}{{properties.hit_points.temp}}{{else}}--{{/if}}
</div>
<div class="box-bottom-label">TEMPORARY HIT POINTS</div>
</div>
<!-- Hit Dice & Death Saves Row -->
<div class="hd-and-death-row">
<!-- Hit Dice -->
<div class="section-box half-box hd-box">
<div class="hd-total-line">Total: {{#if properties.hit_dice.total}}{{properties.hit_dice.total}}{{else}}{{properties.level}}{{/if}}</div>
<div class="hd-current-val">
{{#if properties.hit_dice.dice}}{{properties.hit_dice.dice}}{{else if properties.hit_points.hit_dice}}{{properties.hit_points.hit_dice}}{{else}}1d10{{/if}}
</div>
<div class="box-bottom-label">HIT DICE</div>
</div>
<!-- Death Saves -->
<div class="section-box half-box death-saves-box">
<div class="death-save-line">
<span class="ds-title">SUCCESSES</span>
<span class="ds-bubbles">
<span class="bubble {{#if (gte properties.death_saves.successes 1)}}filled{{/if}}"></span>
<span class="bubble {{#if (gte properties.death_saves.successes 2)}}filled{{/if}}"></span>
<span class="bubble {{#if (gte properties.death_saves.successes 3)}}filled{{/if}}"></span>
</span>
</div>
<div class="death-save-line">
<span class="ds-title">FAILURES</span>
<span class="ds-bubbles">
<span class="bubble {{#if (gte properties.death_saves.failures 1)}}filled{{/if}}"></span>
<span class="bubble {{#if (gte properties.death_saves.failures 2)}}filled{{/if}}"></span>
<span class="bubble {{#if (gte properties.death_saves.failures 3)}}filled{{/if}}"></span>
</span>
</div>
<div class="box-bottom-label">DEATH SAVES</div>
</div>
</div>
<!-- Attacks & Spellcasting -->
<div class="section-box attacks-box">
<table class="attacks-table">
<thead>
<tr>
<th>NAME</th>
<th>ATK BONUS</th>
<th>DAMAGE/TYPE</th>
</tr>
</thead>
<tbody>
{{#if properties.attacks}}
{{#each properties.attacks}}
<tr>
<td class="atk-name">{{this.name}}</td>
<td class="atk-bonus">{{#if this.atk_bonus}}{{this.atk_bonus}}{{else}}+0{{/if}}</td>
<td class="atk-damage">{{this.damage}} {{this.damage_type}}</td>
</tr>
{{/each}}
{{else if properties.actions}}
{{#each properties.actions}}
<tr>
<td class="atk-name">{{this.name}}</td>
<td class="atk-bonus">+5</td>
<td class="atk-damage">{{this.desc}}</td>
</tr>
{{/each}}
{{else}}
<tr><td colspan="3" class="empty-hint">No attacks equipped</td></tr>
{{/if}}
</tbody>
</table>
{{#if properties.actions}}
<div class="attack-notes">
{{#each properties.actions}}
<p class="attack-note-item"><em><strong>{{this.name}}:</strong></em> {{formatText this.desc}}</p>
{{/each}}
</div>
{{/if}}
<div class="box-bottom-label">ATTACKS &amp; SPELLCASTING</div>
</div>
<!-- Equipment & Coins -->
<div class="section-box equipment-box">
<div class="equipment-layout">
<!-- Coins Column -->
<div class="coins-stack">
<div class="coin-row">
<span class="coin-tag">CP</span>
<span class="coin-val">{{#if properties.currency.cp}}{{properties.currency.cp}}{{else}}0{{/if}}</span>
</div>
<div class="coin-row">
<span class="coin-tag">SP</span>
<span class="coin-val">{{#if properties.currency.sp}}{{properties.currency.sp}}{{else}}0{{/if}}</span>
</div>
<div class="coin-row">
<span class="coin-tag">EP</span>
<span class="coin-val">{{#if properties.currency.ep}}{{properties.currency.ep}}{{else}}0{{/if}}</span>
</div>
<div class="coin-row">
<span class="coin-tag">GP</span>
<span class="coin-val">{{#if properties.currency.gp}}{{properties.currency.gp}}{{else}}0{{/if}}</span>
</div>
<div class="coin-row">
<span class="coin-tag">PP</span>
<span class="coin-val">{{#if properties.currency.pp}}{{properties.currency.pp}}{{else}}0{{/if}}</span>
</div>
</div>
<!-- Equipment List -->
<div class="items-list-container">
<ul class="items-list">
{{#each properties.inventory}}
<li class="item-row">
<span class="item-name">{{this.name}}{{#if this.equipped}} <small class="equipped-tag">(E)</small>{{/if}}</span>
<span class="item-meta">x{{this.quantity}}{{#if this.weight}} &bull; {{this.weight}} lb{{/if}}</span>
</li>
{{/each}}
</ul>
</div>
</div>
<div class="box-bottom-label">EQUIPMENT</div>
</div>
</section>
<!-- ================= RIGHT COLUMN ================= -->
<section class="sheet-col col-right">
<!-- Personality Traits -->
<div class="section-box roleplay-box">
<div class="box-content">
{{#if properties.personality_traits}}
<p>{{formatText properties.personality_traits}}</p>
{{else}}
<span class="empty-hint">None</span>
{{/if}}
</div>
<div class="box-bottom-label">PERSONALITY TRAITS</div>
</div>
<!-- Ideals -->
<div class="section-box roleplay-box">
<div class="box-content">
{{#if properties.ideals}}
<p>{{formatText properties.ideals}}</p>
{{else}}
<span class="empty-hint">None</span>
{{/if}}
</div>
<div class="box-bottom-label">IDEALS</div>
</div>
<!-- Bonds -->
<div class="section-box roleplay-box">
<div class="box-content">
{{#if properties.bonds}}
<p>{{formatText properties.bonds}}</p>
{{else}}
<span class="empty-hint">None</span>
{{/if}}
</div>
<div class="box-bottom-label">BONDS</div>
</div>
<!-- Flaws -->
<div class="section-box roleplay-box">
<div class="box-content">
{{#if properties.flaws}}
<p>{{formatText properties.flaws}}</p>
{{else}}
<span class="empty-hint">None</span>
{{/if}}
</div>
<div class="box-bottom-label">FLAWS</div>
</div>
<!-- Features & Traits (Large Box) -->
<div class="section-box features-box">
<div class="features-content">
{{#if properties.features_and_traits}}
{{#each properties.features_and_traits}}
<div class="feature-item">
<strong class="feature-name">{{this.name}}</strong>
<p class="feature-desc">{{formatText this.desc}}</p>
</div>
{{/each}}
{{else if properties.traits}}
{{#each properties.traits}}
<div class="feature-item">
<strong class="feature-name">{{this.name}}</strong>
<p class="feature-desc">{{formatText this.desc}}</p>
</div>
{{/each}}
{{/if}}
</div>
<div class="box-bottom-label">FEATURES &amp; TRAITS</div>
</div>
</section>
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,5 @@
// Starter client-side script for open5e character sheet
window.addEventListener('DOMContentLoaded', () => {
console.log('Open5e Character Sheet template initialized.');
});

View File

@@ -0,0 +1,487 @@
/* D&D 5e Alternative (Ability-Grouped) Character Sheet Styles */
:root {
--border-color: #2b2b2b;
--accent-red: #922610;
--bg-page: #eaedf0;
--bg-sheet: #ffffff;
--bg-box: #f9fbfd;
--text-main: #1e1e1e;
--text-muted: #666666;
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--font-serif: "Libre Baskerville", "Georgia", serif;
}
body.sheet-page-wrapper {
margin: 0;
padding: 24px;
background-color: var(--bg-page);
font-family: var(--font-sans);
color: var(--text-main);
display: flex;
justify-content: center;
box-sizing: border-box;
}
.character-sheet-container {
width: 100%;
max-width: 1050px;
background: var(--bg-sheet);
border: 2px solid var(--border-color);
border-radius: 6px;
padding: 20px 24px 24px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
box-sizing: border-box;
}
/* Header */
.sheet-header {
display: grid;
grid-template-columns: 320px 1fr;
gap: 16px;
align-items: stretch;
border-bottom: 2px solid var(--border-color);
padding-bottom: 16px;
margin-bottom: 20px;
}
.header-name-box {
border: 1.5px solid var(--border-color);
border-radius: 4px;
padding: 12px 16px;
display: flex;
flex-direction: column;
justify-content: flex-end;
background: var(--bg-box);
}
.character-name {
font-family: var(--font-serif);
font-size: 26px;
color: var(--accent-red);
margin: 0 0 6px 0;
line-height: 1.1;
word-break: break-word;
}
.header-label {
font-size: 9.5px;
font-weight: bold;
color: var(--text-muted);
letter-spacing: 0.5px;
}
.header-details-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 12px;
border: 1.5px solid var(--border-color);
border-radius: 4px;
padding: 10px 14px;
background: var(--bg-box);
}
.detail-cell {
display: flex;
flex-direction: column;
justify-content: flex-end;
border-bottom: 1px solid #ccc;
padding-bottom: 3px;
}
.detail-val {
font-size: 13.5px;
font-weight: 600;
color: var(--text-main);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 2px;
}
/* 3-Column Grid */
.sheet-main-grid {
display: grid;
grid-template-columns: 320px 340px 1fr;
gap: 16px;
margin-bottom: 16px;
}
.sheet-col {
display: flex;
flex-direction: column;
gap: 12px;
}
.section-box {
border: 1.5px solid var(--border-color);
border-radius: 4px;
padding: 10px 12px;
background: #ffffff;
position: relative;
box-sizing: border-box;
}
.box-bottom-label {
text-align: center;
font-size: 9.5px;
font-weight: bold;
color: var(--text-muted);
letter-spacing: 0.5px;
margin-top: 6px;
padding-top: 4px;
border-top: 1px solid #eee;
}
/* Ability-Grouped Box Styles */
.alt-top-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.alt-stat-pill {
border: 1.5px solid var(--border-color);
border-radius: 16px;
padding: 5px 10px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
background: var(--bg-box);
}
.pill-value {
font-size: 14px;
font-weight: bold;
color: var(--accent-red);
}
.pill-checkbox {
width: 14px;
height: 14px;
border: 1.5px solid var(--border-color);
border-radius: 50%;
display: inline-block;
}
.pill-checkbox.checked {
background-color: var(--accent-red);
}
.pill-label {
font-size: 8.5px;
font-weight: bold;
color: var(--text-muted);
}
.ability-group-box {
border: 1.5px solid var(--border-color);
border-radius: 6px;
padding: 8px 10px;
display: grid;
grid-template-columns: 78px 1fr;
gap: 10px;
align-items: center;
background: #ffffff;
}
.ag-score-badge {
border: 1.2px solid var(--border-color);
border-radius: 4px;
padding: 4px 2px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
background: var(--bg-box);
}
.ag-title {
font-size: 8px;
font-weight: bold;
color: var(--accent-red);
}
.ag-mod {
font-size: 18px;
font-weight: bold;
color: var(--text-main);
line-height: 1.1;
}
.ag-score {
font-size: 10px;
color: #555;
border: 1px solid #ccc;
border-radius: 8px;
padding: 0 6px;
background: #ffffff;
}
.ag-sublist {
list-style: none;
margin: 0;
padding: 0;
}
.ag-subitem {
display: flex;
align-items: center;
font-size: 11px;
gap: 6px;
margin: 2px 0;
line-height: 1.2;
}
.ag-subitem .dot {
width: 8px;
height: 8px;
border: 1.2px solid var(--border-color);
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
}
.ag-subitem .dot.filled {
background-color: var(--accent-red);
border-color: var(--accent-red);
}
.ag-bonus {
font-weight: 700;
width: 20px;
text-align: right;
flex-shrink: 0;
}
.ag-name {
color: #333;
}
/* Passive Perception */
.passive-perception-box {
border: 1.5px solid var(--border-color);
border-radius: 18px;
padding: 6px 12px;
display: flex;
align-items: center;
gap: 10px;
background: var(--bg-box);
}
.passive-score {
font-size: 15px;
font-weight: bold;
color: var(--text-main);
border: 1px solid var(--border-color);
border-radius: 50%;
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
background: #ffffff;
flex-shrink: 0;
}
.passive-label {
font-size: 9.5px;
font-weight: bold;
color: var(--text-muted);
}
/* Combat Stats */
.combat-top-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.combat-badge {
border: 1.5px solid var(--border-color);
border-radius: 6px;
padding: 8px 4px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: var(--bg-box);
}
.badge-ac { border-color: var(--accent-red); }
.badge-value {
font-size: 22px;
font-weight: bold;
color: var(--text-main);
line-height: 1.1;
}
.badge-label {
font-size: 8.5px;
font-weight: bold;
color: var(--text-muted);
margin-top: 3px;
}
.hp-max-line {
font-size: 10.5px;
color: #666;
border-bottom: 1px solid #eee;
padding-bottom: 4px;
}
.hp-current-val, .temp-hp-val {
font-size: 28px;
font-weight: bold;
text-align: center;
padding: 8px 0;
color: var(--text-main);
}
.hd-and-death-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.half-box { padding: 6px 8px; }
.hd-total-line { font-size: 9.5px; color: #666; }
.hd-current-val {
font-size: 18px;
font-weight: bold;
text-align: center;
padding: 4px 0;
}
.death-save-line {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 9px;
font-weight: bold;
margin: 3px 0;
}
.ds-bubbles { display: flex; gap: 4px; }
.ds-bubbles .bubble {
width: 9px;
height: 9px;
border: 1.2px solid var(--border-color);
border-radius: 50%;
display: inline-block;
}
.ds-bubbles .bubble.filled { background-color: var(--accent-red); }
/* Attacks Table */
.attacks-table {
width: 100%;
border-collapse: collapse;
font-size: 11px;
margin-bottom: 8px;
}
.attacks-table th {
font-size: 8.5px;
font-weight: bold;
color: var(--text-muted);
border-bottom: 1px solid #ccc;
padding: 2px 4px;
text-align: left;
}
.attacks-table td {
padding: 5px 4px;
border-bottom: 1px solid #eee;
}
.atk-name { font-weight: 600; }
.atk-bonus { font-weight: 700; color: var(--accent-red); }
.attack-notes {
font-size: 10.5px;
line-height: 1.35;
color: #444;
margin-top: 6px;
max-height: 120px;
overflow-y: auto;
}
.attack-note-item { margin: 4px 0; }
/* Right Column Roleplay Boxes */
.roleplay-box {
min-height: 64px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.box-content { font-size: 11.5px; line-height: 1.4; color: #333; }
.box-content p { margin: 0; }
.empty-hint { color: #999; font-style: italic; font-size: 11px; }
.features-box {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 250px;
}
.features-content { font-size: 11.5px; line-height: 1.4; }
.feature-item { margin-bottom: 8px; }
.feature-name { color: var(--accent-red); display: block; margin-bottom: 2px; }
.feature-desc { margin: 0; }
/* Bottom Wide Row */
.sheet-bottom-row {
display: grid;
grid-template-columns: 320px 1fr;
gap: 16px;
}
.prof-block-content { font-size: 11.5px; line-height: 1.45; }
.prof-line { margin-bottom: 4px; }
.equipment-layout {
display: grid;
grid-template-columns: 56px 1fr;
gap: 12px;
}
.coins-stack { display: flex; flex-direction: column; gap: 4px; }
.coin-row {
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 2px 4px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 10px;
background: var(--bg-box);
}
.coin-tag { font-weight: bold; font-size: 8.5px; color: var(--accent-red); }
.coin-val { font-weight: 600; }
.items-list-container { max-height: 180px; overflow-y: auto; }
.items-list { list-style: none; margin: 0; padding: 0; font-size: 11px; }
.item-row { display: flex; justify-content: space-between; padding: 3px 0; border-bottom: 1px solid #eee; }
.equipped-tag { color: var(--accent-red); font-weight: bold; }
.item-meta { color: #777; font-size: 10px; }
/* Responsive Styles */
@media (max-width: 900px) {
.sheet-header { grid-template-columns: 1fr; }
.sheet-main-grid { grid-template-columns: 1fr; }
.sheet-bottom-row { grid-template-columns: 1fr; }
}

View File

@@ -0,0 +1,472 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="/rulesets/open5e/templates/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{display_name}} - Alternative Character Sheet</title>
<link rel="stylesheet" href="character_sheet_alt.css">
</head>
<body class="sheet-page-wrapper">
<div class="character-sheet-container">
<!-- Top Header Banner -->
<header class="sheet-header">
<div class="header-name-box">
<h1 class="character-name">{{display_name}}</h1>
<label class="header-label">CHARACTER NAME</label>
</div>
<div class="header-details-grid">
<div class="detail-cell">
<div class="detail-val">{{properties.class}}{{#if properties.subclass}} ({{properties.subclass}}){{/if}} {{properties.level}}</div>
<label class="header-label">CLASS &amp; LEVEL</label>
</div>
<div class="detail-cell">
<div class="detail-val">{{#if properties.background}}{{properties.background}}{{else}}&mdash;{{/if}}</div>
<label class="header-label">BACKGROUND</label>
</div>
<div class="detail-cell">
<div class="detail-val">{{#if properties.player_name}}{{properties.player_name}}{{else}}&mdash;{{/if}}</div>
<label class="header-label">PLAYER NAME</label>
</div>
<div class="detail-cell">
<div class="detail-val">{{properties.race}}</div>
<label class="header-label">RACE</label>
</div>
<div class="detail-cell">
<div class="detail-val">{{properties.alignment}}</div>
<label class="header-label">ALIGNMENT</label>
</div>
<div class="detail-cell">
<div class="detail-val">{{#if properties.experience_points}}{{properties.experience_points}}{{else}}0{{/if}} XP</div>
<label class="header-label">EXPERIENCE POINTS</label>
</div>
</div>
</header>
<!-- Main 3-Column Layout -->
<main class="sheet-main-grid">
<!-- ================= LEFT COLUMN: ABILITY-GROUPED STATS ================= -->
<section class="sheet-col col-left">
<!-- Top Controls: Proficiency Bonus & Inspiration -->
<div class="alt-top-row">
<div class="alt-stat-pill">
<span class="pill-value">+{{calcProf properties.level properties.cr}}</span>
<span class="pill-label">PROFICIENCY BONUS</span>
</div>
<div class="alt-stat-pill">
<span class="pill-checkbox {{#if properties.inspiration}}checked{{/if}}"></span>
<span class="pill-label">INSPIRATION</span>
</div>
</div>
<!-- STRENGTH GROUP -->
<div class="ability-group-box">
<div class="ag-score-badge">
<span class="ag-title">STRENGTH</span>
<span class="ag-mod">{{modText properties.abilities.strength}}</span>
<span class="ag-score">{{val properties.abilities.strength}}</span>
</div>
<ul class="ag-sublist">
<li class="ag-subitem">
<span class="dot {{#if (hasSave properties.saving_throws 'str')}}filled{{/if}}"></span>
<span class="ag-bonus">{{saveBonus properties 'strength'}}</span>
<span class="ag-name">SAVING THROWS</span>
</li>
<li class="ag-subitem">
<span class="dot {{#if (isSkillProf properties.skills 'athletics')}}filled{{/if}}"></span>
<span class="ag-bonus">{{skillBonus properties 'athletics' 'strength'}}</span>
<span class="ag-name">Athletics</span>
</li>
</ul>
</div>
<!-- DEXTERITY GROUP -->
<div class="ability-group-box">
<div class="ag-score-badge">
<span class="ag-title">DEXTERITY</span>
<span class="ag-mod">{{modText properties.abilities.dexterity}}</span>
<span class="ag-score">{{val properties.abilities.dexterity}}</span>
</div>
<ul class="ag-sublist">
<li class="ag-subitem">
<span class="dot {{#if (hasSave properties.saving_throws 'dex')}}filled{{/if}}"></span>
<span class="ag-bonus">{{saveBonus properties 'dexterity'}}</span>
<span class="ag-name">SAVING THROWS</span>
</li>
<li class="ag-subitem">
<span class="dot {{#if (isSkillProf properties.skills 'acrobatics')}}filled{{/if}}"></span>
<span class="ag-bonus">{{skillBonus properties 'acrobatics' 'dexterity'}}</span>
<span class="ag-name">Acrobatics</span>
</li>
<li class="ag-subitem">
<span class="dot {{#if (isSkillProf properties.skills 'sleight_of_hand')}}filled{{/if}}"></span>
<span class="ag-bonus">{{skillBonus properties 'sleight_of_hand' 'dexterity'}}</span>
<span class="ag-name">Sleight of Hand</span>
</li>
<li class="ag-subitem">
<span class="dot {{#if (isSkillProf properties.skills 'stealth')}}filled{{/if}}"></span>
<span class="ag-bonus">{{skillBonus properties 'stealth' 'dexterity'}}</span>
<span class="ag-name">Stealth</span>
</li>
</ul>
</div>
<!-- CONSTITUTION GROUP -->
<div class="ability-group-box">
<div class="ag-score-badge">
<span class="ag-title">CONSTITUTION</span>
<span class="ag-mod">{{modText properties.abilities.constitution}}</span>
<span class="ag-score">{{val properties.abilities.constitution}}</span>
</div>
<ul class="ag-sublist">
<li class="ag-subitem">
<span class="dot {{#if (hasSave properties.saving_throws 'con')}}filled{{/if}}"></span>
<span class="ag-bonus">{{saveBonus properties 'constitution'}}</span>
<span class="ag-name">SAVING THROWS</span>
</li>
</ul>
</div>
<!-- INTELLIGENCE GROUP -->
<div class="ability-group-box">
<div class="ag-score-badge">
<span class="ag-title">INTELLIGENCE</span>
<span class="ag-mod">{{modText properties.abilities.intelligence}}</span>
<span class="ag-score">{{val properties.abilities.intelligence}}</span>
</div>
<ul class="ag-sublist">
<li class="ag-subitem">
<span class="dot {{#if (hasSave properties.saving_throws 'int')}}filled{{/if}}"></span>
<span class="ag-bonus">{{saveBonus properties 'intelligence'}}</span>
<span class="ag-name">SAVING THROWS</span>
</li>
<li class="ag-subitem">
<span class="dot {{#if (isSkillProf properties.skills 'arcana')}}filled{{/if}}"></span>
<span class="ag-bonus">{{skillBonus properties 'arcana' 'intelligence'}}</span>
<span class="ag-name">Arcana</span>
</li>
<li class="ag-subitem">
<span class="dot {{#if (isSkillProf properties.skills 'history')}}filled{{/if}}"></span>
<span class="ag-bonus">{{skillBonus properties 'history' 'intelligence'}}</span>
<span class="ag-name">History</span>
</li>
<li class="ag-subitem">
<span class="dot {{#if (isSkillProf properties.skills 'investigation')}}filled{{/if}}"></span>
<span class="ag-bonus">{{skillBonus properties 'investigation' 'intelligence'}}</span>
<span class="ag-name">Investigation</span>
</li>
<li class="ag-subitem">
<span class="dot {{#if (isSkillProf properties.skills 'nature')}}filled{{/if}}"></span>
<span class="ag-bonus">{{skillBonus properties 'nature' 'intelligence'}}</span>
<span class="ag-name">Nature</span>
</li>
<li class="ag-subitem">
<span class="dot {{#if (isSkillProf properties.skills 'religion')}}filled{{/if}}"></span>
<span class="ag-bonus">{{skillBonus properties 'religion' 'intelligence'}}</span>
<span class="ag-name">Religion</span>
</li>
</ul>
</div>
<!-- WISDOM GROUP -->
<div class="ability-group-box">
<div class="ag-score-badge">
<span class="ag-title">WISDOM</span>
<span class="ag-mod">{{modText properties.abilities.wisdom}}</span>
<span class="ag-score">{{val properties.abilities.wisdom}}</span>
</div>
<ul class="ag-sublist">
<li class="ag-subitem">
<span class="dot {{#if (hasSave properties.saving_throws 'wis')}}filled{{/if}}"></span>
<span class="ag-bonus">{{saveBonus properties 'wisdom'}}</span>
<span class="ag-name">SAVING THROWS</span>
</li>
<li class="ag-subitem">
<span class="dot {{#if (isSkillProf properties.skills 'animal_handling')}}filled{{/if}}"></span>
<span class="ag-bonus">{{skillBonus properties 'animal_handling' 'wisdom'}}</span>
<span class="ag-name">Animal Handling</span>
</li>
<li class="ag-subitem">
<span class="dot {{#if (isSkillProf properties.skills 'insight')}}filled{{/if}}"></span>
<span class="ag-bonus">{{skillBonus properties 'insight' 'wisdom'}}</span>
<span class="ag-name">Insight</span>
</li>
<li class="ag-subitem">
<span class="dot {{#if (isSkillProf properties.skills 'medicine')}}filled{{/if}}"></span>
<span class="ag-bonus">{{skillBonus properties 'medicine' 'wisdom'}}</span>
<span class="ag-name">Medicine</span>
</li>
<li class="ag-subitem">
<span class="dot {{#if (isSkillProf properties.skills 'perception')}}filled{{/if}}"></span>
<span class="ag-bonus">{{skillBonus properties 'perception' 'wisdom'}}</span>
<span class="ag-name">Perception</span>
</li>
<li class="ag-subitem">
<span class="dot {{#if (isSkillProf properties.skills 'survival')}}filled{{/if}}"></span>
<span class="ag-bonus">{{skillBonus properties 'survival' 'wisdom'}}</span>
<span class="ag-name">Survival</span>
</li>
</ul>
</div>
<!-- CHARISMA GROUP -->
<div class="ability-group-box">
<div class="ag-score-badge">
<span class="ag-title">CHARISMA</span>
<span class="ag-mod">{{modText properties.abilities.charisma}}</span>
<span class="ag-score">{{val properties.abilities.charisma}}</span>
</div>
<ul class="ag-sublist">
<li class="ag-subitem">
<span class="dot {{#if (hasSave properties.saving_throws 'cha')}}filled{{/if}}"></span>
<span class="ag-bonus">{{saveBonus properties 'charisma'}}</span>
<span class="ag-name">SAVING THROWS</span>
</li>
<li class="ag-subitem">
<span class="dot {{#if (isSkillProf properties.skills 'deception')}}filled{{/if}}"></span>
<span class="ag-bonus">{{skillBonus properties 'deception' 'charisma'}}</span>
<span class="ag-name">Deception</span>
</li>
<li class="ag-subitem">
<span class="dot {{#if (isSkillProf properties.skills 'intimidation')}}filled{{/if}}"></span>
<span class="ag-bonus">{{skillBonus properties 'intimidation' 'charisma'}}</span>
<span class="ag-name">Intimidation</span>
</li>
<li class="ag-subitem">
<span class="dot {{#if (isSkillProf properties.skills 'performance')}}filled{{/if}}"></span>
<span class="ag-bonus">{{skillBonus properties 'performance' 'charisma'}}</span>
<span class="ag-name">Performance</span>
</li>
<li class="ag-subitem">
<span class="dot {{#if (isSkillProf properties.skills 'persuasion')}}filled{{/if}}"></span>
<span class="ag-bonus">{{skillBonus properties 'persuasion' 'charisma'}}</span>
<span class="ag-name">Persuasion</span>
</li>
</ul>
</div>
<!-- Passive Perception Box -->
<div class="passive-perception-box">
<span class="passive-score">{{passivePerception properties}}</span>
<span class="passive-label">PASSIVE WISDOM (PERCEPTION)</span>
</div>
</section>
<!-- ================= MIDDLE COLUMN ================= -->
<section class="sheet-col col-middle">
<!-- Combat Top Row -->
<div class="combat-top-row">
<div class="combat-badge badge-ac">
<span class="badge-value">{{val properties.armor_class}}</span>
<span class="badge-label">ARMOR CLASS</span>
</div>
<div class="combat-badge badge-init">
<span class="badge-value">{{#if properties.initiative}}{{modSign properties.initiative}}{{else}}{{modText properties.abilities.dexterity}}{{/if}}</span>
<span class="badge-label">INITIATIVE</span>
</div>
<div class="combat-badge badge-speed">
<span class="badge-value">{{#if properties.speed}}{{properties.speed}} ft.{{else}}30 ft.{{/if}}</span>
<span class="badge-label">SPEED</span>
</div>
</div>
<!-- HP Box -->
<div class="section-box hp-box">
<div class="hp-max-line">
Hit Point Maximum: <strong>{{properties.hit_points.max}}</strong>
</div>
<div class="hp-current-val">{{properties.hit_points.current}}</div>
<div class="box-bottom-label">CURRENT HIT POINTS</div>
</div>
<!-- Temp HP -->
<div class="section-box temp-hp-box">
<div class="temp-hp-val">{{#if properties.hit_points.temp}}{{properties.hit_points.temp}}{{else}}--{{/if}}</div>
<div class="box-bottom-label">TEMPORARY HIT POINTS</div>
</div>
<!-- Hit Dice & Death Saves -->
<div class="hd-and-death-row">
<div class="section-box half-box hd-box">
<div class="hd-total-line">Total: {{#if properties.hit_dice.total}}{{properties.hit_dice.total}}{{else}}{{properties.level}}{{/if}}</div>
<div class="hd-current-val">
{{#if properties.hit_dice.dice}}{{properties.hit_dice.dice}}{{else if properties.hit_points.hit_dice}}{{properties.hit_points.hit_dice}}{{else}}1d10{{/if}}
</div>
<div class="box-bottom-label">HIT DICE</div>
</div>
<div class="section-box half-box death-saves-box">
<div class="death-save-line">
<span class="ds-title">SUCCESSES</span>
<span class="ds-bubbles">
<span class="bubble {{#if (gte properties.death_saves.successes 1)}}filled{{/if}}"></span>
<span class="bubble {{#if (gte properties.death_saves.successes 2)}}filled{{/if}}"></span>
<span class="bubble {{#if (gte properties.death_saves.successes 3)}}filled{{/if}}"></span>
</span>
</div>
<div class="death-save-line">
<span class="ds-title">FAILURES</span>
<span class="ds-bubbles">
<span class="bubble {{#if (gte properties.death_saves.failures 1)}}filled{{/if}}"></span>
<span class="bubble {{#if (gte properties.death_saves.failures 2)}}filled{{/if}}"></span>
<span class="bubble {{#if (gte properties.death_saves.failures 3)}}filled{{/if}}"></span>
</span>
</div>
<div class="box-bottom-label">DEATH SAVES</div>
</div>
</div>
<!-- Attacks & Spellcasting -->
<div class="section-box attacks-box">
<table class="attacks-table">
<thead>
<tr>
<th>NAME</th>
<th>ATK BONUS</th>
<th>DAMAGE/TYPE</th>
</tr>
</thead>
<tbody>
{{#if properties.attacks}}
{{#each properties.attacks}}
<tr>
<td class="atk-name">{{this.name}}</td>
<td class="atk-bonus">{{#if this.atk_bonus}}{{this.atk_bonus}}{{else}}+0{{/if}}</td>
<td class="atk-damage">{{this.damage}} {{this.damage_type}}</td>
</tr>
{{/each}}
{{else if properties.actions}}
{{#each properties.actions}}
<tr>
<td class="atk-name">{{this.name}}</td>
<td class="atk-bonus">+5</td>
<td class="atk-damage">{{this.desc}}</td>
</tr>
{{/each}}
{{else}}
<tr><td colspan="3" class="empty-hint">No attacks equipped</td></tr>
{{/if}}
</tbody>
</table>
{{#if properties.actions}}
<div class="attack-notes">
{{#each properties.actions}}
<p class="attack-note-item"><em><strong>{{this.name}}:</strong></em> {{formatText this.desc}}</p>
{{/each}}
</div>
{{/if}}
<div class="box-bottom-label">ATTACKS &amp; SPELLCASTING</div>
</div>
</section>
<!-- ================= RIGHT COLUMN ================= -->
<section class="sheet-col col-right">
<!-- Personality Traits -->
<div class="section-box roleplay-box">
<div class="box-content">{{#if properties.personality_traits}}<p>{{formatText properties.personality_traits}}</p>{{else}}<span class="empty-hint">None</span>{{/if}}</div>
<div class="box-bottom-label">PERSONALITY TRAITS</div>
</div>
<!-- Ideals -->
<div class="section-box roleplay-box">
<div class="box-content">{{#if properties.ideals}}<p>{{formatText properties.ideals}}</p>{{else}}<span class="empty-hint">None</span>{{/if}}</div>
<div class="box-bottom-label">IDEALS</div>
</div>
<!-- Bonds -->
<div class="section-box roleplay-box">
<div class="box-content">{{#if properties.bonds}}<p>{{formatText properties.bonds}}</p>{{else}}<span class="empty-hint">None</span>{{/if}}</div>
<div class="box-bottom-label">BONDS</div>
</div>
<!-- Flaws -->
<div class="section-box roleplay-box">
<div class="box-content">{{#if properties.flaws}}<p>{{formatText properties.flaws}}</p>{{else}}<span class="empty-hint">None</span>{{/if}}</div>
<div class="box-bottom-label">FLAWS</div>
</div>
<!-- Features & Traits -->
<div class="section-box features-box">
<div class="features-content">
{{#if properties.features_and_traits}}
{{#each properties.features_and_traits}}
<div class="feature-item">
<strong class="feature-name">{{this.name}}</strong>
<p class="feature-desc">{{formatText this.desc}}</p>
</div>
{{/each}}
{{else if properties.traits}}
{{#each properties.traits}}
<div class="feature-item">
<strong class="feature-name">{{this.name}}</strong>
<p class="feature-desc">{{formatText this.desc}}</p>
</div>
{{/each}}
{{/if}}
</div>
<div class="box-bottom-label">FEATURES &amp; TRAITS</div>
</div>
</section>
</main>
<!-- Bottom Wide Row: Proficiencies & Equipment/Notes -->
<div class="sheet-bottom-row">
<!-- Other Proficiencies & Languages -->
<div class="section-box proficiencies-box">
<div class="prof-block-content">
{{#if properties.other_proficiencies_and_languages.armor}}
<div class="prof-line"><strong>Armor:</strong> {{join properties.other_proficiencies_and_languages.armor}}</div>
{{/if}}
{{#if properties.other_proficiencies_and_languages.weapons}}
<div class="prof-line"><strong>Weapons:</strong> {{join properties.other_proficiencies_and_languages.weapons}}</div>
{{/if}}
{{#if properties.other_proficiencies_and_languages.tools}}
<div class="prof-line"><strong>Tools:</strong> {{join properties.other_proficiencies_and_languages.tools}}</div>
{{/if}}
{{#if properties.languages}}
<div class="prof-line"><strong>Languages:</strong> {{join properties.languages}}</div>
{{else if properties.other_proficiencies_and_languages.languages}}
<div class="prof-line"><strong>Languages:</strong> {{join properties.other_proficiencies_and_languages.languages}}</div>
{{/if}}
</div>
<div class="box-bottom-label">OTHER PROFICIENCIES &amp; LANGUAGES</div>
</div>
<!-- Equipment & Notes -->
<div class="section-box equipment-box">
<div class="equipment-layout">
<div class="coins-stack">
<div class="coin-row"><span class="coin-tag">CP</span><span class="coin-val">{{#if properties.currency.cp}}{{properties.currency.cp}}{{else}}0{{/if}}</span></div>
<div class="coin-row"><span class="coin-tag">SP</span><span class="coin-val">{{#if properties.currency.sp}}{{properties.currency.sp}}{{else}}0{{/if}}</span></div>
<div class="coin-row"><span class="coin-tag">EP</span><span class="coin-val">{{#if properties.currency.ep}}{{properties.currency.ep}}{{else}}0{{/if}}</span></div>
<div class="coin-row"><span class="coin-tag">GP</span><span class="coin-val">{{#if properties.currency.gp}}{{properties.currency.gp}}{{else}}0{{/if}}</span></div>
<div class="coin-row"><span class="coin-tag">PP</span><span class="coin-val">{{#if properties.currency.pp}}{{properties.currency.pp}}{{else}}0{{/if}}</span></div>
</div>
<div class="items-list-container">
<ul class="items-list">
{{#each properties.inventory}}
<li class="item-row">
<span class="item-name">{{this.name}}{{#if this.equipped}} <small class="equipped-tag">(E)</small>{{/if}}</span>
<span class="item-meta">x{{this.quantity}}{{#if this.weight}} &bull; {{this.weight}} lb{{/if}}</span>
</li>
{{/each}}
</ul>
</div>
</div>
<div class="box-bottom-label">EQUIPMENT &amp; CHARACTER NOTES</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,221 @@
/* D&D 5e Spellcasting Sheet Styles */
:root {
--border-color: #2b2b2b;
--accent-red: #922610;
--bg-page: #eaedf0;
--bg-sheet: #ffffff;
--bg-box: #f9fbfd;
--text-main: #1e1e1e;
--text-muted: #666666;
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--font-serif: "Libre Baskerville", "Georgia", serif;
}
body.sheet-page-wrapper {
margin: 0;
padding: 24px;
background-color: var(--bg-page);
font-family: var(--font-sans);
color: var(--text-main);
display: flex;
justify-content: center;
box-sizing: border-box;
}
.spellcasting-sheet-container {
width: 100%;
max-width: 1050px;
background: var(--bg-sheet);
border: 2px solid var(--border-color);
border-radius: 6px;
padding: 20px 24px 24px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
box-sizing: border-box;
}
/* Header */
.spell-header {
display: grid;
grid-template-columns: 320px 1fr;
gap: 16px;
border-bottom: 2px solid var(--border-color);
padding-bottom: 16px;
margin-bottom: 20px;
}
.spell-header-class-box {
border: 1.5px solid var(--border-color);
border-radius: 4px;
padding: 12px 16px;
display: flex;
flex-direction: column;
justify-content: flex-end;
background: var(--bg-box);
}
.spell-class-name {
font-family: var(--font-serif);
font-size: 26px;
color: var(--accent-red);
margin: 0 0 6px 0;
line-height: 1.1;
}
.spell-header-label {
font-size: 9px;
font-weight: bold;
color: var(--text-muted);
letter-spacing: 0.5px;
}
.spell-header-stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
border: 1.5px solid var(--border-color);
border-radius: 4px;
padding: 10px 14px;
background: var(--bg-box);
}
.spell-stat-cell {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-right: 1px solid #ccc;
}
.spell-stat-cell:last-child {
border-right: none;
}
.spell-stat-val {
font-size: 20px;
font-weight: bold;
color: var(--text-main);
margin-bottom: 4px;
}
/* 3-Column Layout */
.spells-main-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.spells-column {
display: flex;
flex-direction: column;
gap: 14px;
}
.spell-level-box {
border: 1.5px solid var(--border-color);
border-radius: 6px;
background: #ffffff;
overflow: hidden;
}
.spell-level-header {
background: var(--bg-box);
border-bottom: 1.5px solid var(--border-color);
padding: 6px 10px;
display: flex;
align-items: center;
gap: 8px;
}
.level-num {
font-size: 16px;
font-weight: bold;
color: #ffffff;
background: var(--accent-red);
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.level-title {
font-size: 11px;
font-weight: bold;
color: var(--text-main);
letter-spacing: 0.5px;
}
.spell-level-header.has-slots {
justify-content: space-between;
}
.slot-counters {
display: flex;
gap: 10px;
font-size: 10.5px;
color: #555;
}
.slot-field strong {
color: var(--text-main);
}
.spell-items-list {
list-style: none;
margin: 0;
padding: 6px 10px;
min-height: 80px;
}
.spell-item-row {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
border-bottom: 1px solid #f0f0f0;
font-size: 12px;
}
.spell-item-row:last-child {
border-bottom: none;
}
.spell-prep-circle {
width: 10px;
height: 10px;
border: 1.2px solid var(--border-color);
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
}
.spell-prep-circle.prepared {
background: var(--accent-red);
border-color: var(--accent-red);
}
.spell-prep-circle.invisible {
opacity: 0;
}
.spell-name {
color: var(--text-main);
}
.spell-item-row.empty {
color: #bbb;
justify-content: center;
}
/* Responsive */
@media (max-width: 900px) {
.spell-header {
grid-template-columns: 1fr;
}
.spells-main-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,277 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="/rulesets/open5e/templates/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{display_name}} - Spellcasting Sheet</title>
<link rel="stylesheet" href="spellcasting_sheet.css">
</head>
<body class="sheet-page-wrapper">
<div class="spellcasting-sheet-container">
<!-- Top Spellcasting Header -->
<header class="spell-header">
<div class="spell-header-class-box">
<h1 class="spell-class-name">
{{#if properties.spellcasting.class}}
{{properties.spellcasting.class}}
{{else if properties.class}}
{{properties.class}}
{{else}}
Spellcaster
{{/if}}
</h1>
<label class="spell-header-label">SPELLCASTING CLASS</label>
</div>
<div class="spell-header-stats-grid">
<div class="spell-stat-cell">
<span class="spell-stat-val">
{{#if properties.spellcasting.ability}}
{{properties.spellcasting.ability}}
{{else}}
WISDOM
{{/if}}
</span>
<label class="spell-header-label">SPELLCASTING ABILITY</label>
</div>
<div class="spell-stat-cell">
<span class="spell-stat-val">
{{#if properties.spellcasting.save_dc}}
{{properties.spellcasting.save_dc}}
{{else}}
13
{{/if}}
</span>
<label class="spell-header-label">SPELL SAVE DC</label>
</div>
<div class="spell-stat-cell">
<span class="spell-stat-val">
{{#if properties.spellcasting.attack_bonus}}
{{modSign properties.spellcasting.attack_bonus}}
{{else}}
+5
{{/if}}
</span>
<label class="spell-header-label">SPELL ATTACK BONUS</label>
</div>
</div>
</header>
<!-- 3-Column Spells Grid -->
<main class="spells-main-grid">
<!-- ================= COLUMN 1: Cantrips, Level 1, Level 2 ================= -->
<section class="spells-column">
<!-- Cantrips (Level 0) -->
<div class="spell-level-box">
<div class="spell-level-header">
<span class="level-num">0</span>
<span class="level-title">CANTRIPS</span>
</div>
<ul class="spell-items-list">
{{#if properties.spellcasting.cantrips}}
{{#each properties.spellcasting.cantrips}}
<li class="spell-item-row">
<span class="spell-prep-circle invisible"></span>
<span class="spell-name">{{#if this.name}}{{this.name}}{{else}}{{this}}{{/if}}</span>
</li>
{{/each}}
{{else}}
<li class="spell-item-row empty"><span class="spell-name">&mdash;</span></li>
{{/if}}
</ul>
</div>
<!-- Level 1 Spells -->
<div class="spell-level-box">
<div class="spell-level-header has-slots">
<span class="level-num">1</span>
<div class="slot-counters">
<span class="slot-field">Total: <strong>{{slotTotal ../properties.spellcasting.slots 1}}</strong></span>
<span class="slot-field">Expended: <strong>{{slotExpended ../properties.spellcasting.slots 1}}</strong></span>
</div>
</div>
<ul class="spell-items-list">
{{#each (filterSpells properties.spellcasting.spells 1)}}
<li class="spell-item-row">
<span class="spell-prep-circle {{#if this.prepared}}prepared{{/if}}"></span>
<span class="spell-name">{{this.name}}</span>
</li>
{{/each}}
</ul>
</div>
<!-- Level 2 Spells -->
<div class="spell-level-box">
<div class="spell-level-header has-slots">
<span class="level-num">2</span>
<div class="slot-counters">
<span class="slot-field">Total: <strong>{{slotTotal ../properties.spellcasting.slots 2}}</strong></span>
<span class="slot-field">Expended: <strong>{{slotExpended ../properties.spellcasting.slots 2}}</strong></span>
</div>
</div>
<ul class="spell-items-list">
{{#each (filterSpells properties.spellcasting.spells 2)}}
<li class="spell-item-row">
<span class="spell-prep-circle {{#if this.prepared}}prepared{{/if}}"></span>
<span class="spell-name">{{this.name}}</span>
</li>
{{/each}}
</ul>
</div>
</section>
<!-- ================= COLUMN 2: Level 3, Level 4, Level 5 ================= -->
<section class="spells-column">
<!-- Level 3 Spells -->
<div class="spell-level-box">
<div class="spell-level-header has-slots">
<span class="level-num">3</span>
<div class="slot-counters">
<span class="slot-field">Total: <strong>{{slotTotal ../properties.spellcasting.slots 3}}</strong></span>
<span class="slot-field">Expended: <strong>{{slotExpended ../properties.spellcasting.slots 3}}</strong></span>
</div>
</div>
<ul class="spell-items-list">
{{#each (filterSpells properties.spellcasting.spells 3)}}
<li class="spell-item-row">
<span class="spell-prep-circle {{#if this.prepared}}prepared{{/if}}"></span>
<span class="spell-name">{{this.name}}</span>
</li>
{{/each}}
</ul>
</div>
<!-- Level 4 Spells -->
<div class="spell-level-box">
<div class="spell-level-header has-slots">
<span class="level-num">4</span>
<div class="slot-counters">
<span class="slot-field">Total: <strong>{{slotTotal ../properties.spellcasting.slots 4}}</strong></span>
<span class="slot-field">Expended: <strong>{{slotExpended ../properties.spellcasting.slots 4}}</strong></span>
</div>
</div>
<ul class="spell-items-list">
{{#each (filterSpells properties.spellcasting.spells 4)}}
<li class="spell-item-row">
<span class="spell-prep-circle {{#if this.prepared}}prepared{{/if}}"></span>
<span class="spell-name">{{this.name}}</span>
</li>
{{/each}}
</ul>
</div>
<!-- Level 5 Spells -->
<div class="spell-level-box">
<div class="spell-level-header has-slots">
<span class="level-num">5</span>
<div class="slot-counters">
<span class="slot-field">Total: <strong>{{slotTotal ../properties.spellcasting.slots 5}}</strong></span>
<span class="slot-field">Expended: <strong>{{slotExpended ../properties.spellcasting.slots 5}}</strong></span>
</div>
</div>
<ul class="spell-items-list">
{{#each (filterSpells properties.spellcasting.spells 5)}}
<li class="spell-item-row">
<span class="spell-prep-circle {{#if this.prepared}}prepared{{/if}}"></span>
<span class="spell-name">{{this.name}}</span>
</li>
{{/each}}
</ul>
</div>
</section>
<!-- ================= COLUMN 3: Level 6, 7, 8, 9 ================= -->
<section class="spells-column">
<!-- Level 6 Spells -->
<div class="spell-level-box">
<div class="spell-level-header has-slots">
<span class="level-num">6</span>
<div class="slot-counters">
<span class="slot-field">Total: <strong>{{slotTotal ../properties.spellcasting.slots 6}}</strong></span>
<span class="slot-field">Expended: <strong>{{slotExpended ../properties.spellcasting.slots 6}}</strong></span>
</div>
</div>
<ul class="spell-items-list">
{{#each (filterSpells properties.spellcasting.spells 6)}}
<li class="spell-item-row">
<span class="spell-prep-circle {{#if this.prepared}}prepared{{/if}}"></span>
<span class="spell-name">{{this.name}}</span>
</li>
{{/each}}
</ul>
</div>
<!-- Level 7 Spells -->
<div class="spell-level-box">
<div class="spell-level-header has-slots">
<span class="level-num">7</span>
<div class="slot-counters">
<span class="slot-field">Total: <strong>{{slotTotal ../properties.spellcasting.slots 7}}</strong></span>
<span class="slot-field">Expended: <strong>{{slotExpended ../properties.spellcasting.slots 7}}</strong></span>
</div>
</div>
<ul class="spell-items-list">
{{#each (filterSpells properties.spellcasting.spells 7)}}
<li class="spell-item-row">
<span class="spell-prep-circle {{#if this.prepared}}prepared{{/if}}"></span>
<span class="spell-name">{{this.name}}</span>
</li>
{{/each}}
</ul>
</div>
<!-- Level 8 Spells -->
<div class="spell-level-box">
<div class="spell-level-header has-slots">
<span class="level-num">8</span>
<div class="slot-counters">
<span class="slot-field">Total: <strong>{{slotTotal ../properties.spellcasting.slots 8}}</strong></span>
<span class="slot-field">Expended: <strong>{{slotExpended ../properties.spellcasting.slots 8}}</strong></span>
</div>
</div>
<ul class="spell-items-list">
{{#each (filterSpells properties.spellcasting.spells 8)}}
<li class="spell-item-row">
<span class="spell-prep-circle {{#if this.prepared}}prepared{{/if}}"></span>
<span class="spell-name">{{this.name}}</span>
</li>
{{/each}}
</ul>
</div>
<!-- Level 9 Spells -->
<div class="spell-level-box">
<div class="spell-level-header has-slots">
<span class="level-num">9</span>
<div class="slot-counters">
<span class="slot-field">Total: <strong>{{slotTotal ../properties.spellcasting.slots 9}}</strong></span>
<span class="slot-field">Expended: <strong>{{slotExpended ../properties.spellcasting.slots 9}}</strong></span>
</div>
</div>
<ul class="spell-items-list">
{{#each (filterSpells properties.spellcasting.spells 9)}}
<li class="spell-item-row">
<span class="spell-prep-circle {{#if this.prepared}}prepared{{/if}}"></span>
<span class="spell-name">{{this.name}}</span>
</li>
{{/each}}
</ul>
</div>
</section>
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,142 @@
.stat-block-wrapper {
margin: 0;
padding: 16px;
background-color: #f0f2f5;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: #222;
display: flex;
justify-content: center;
}
.stat-block {
width: 100%;
max-width: 650px;
background: #ffffff;
padding: 24px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border-radius: 4px;
border-top: 6px solid #922610;
box-sizing: border-box;
}
.sb-header {
margin-bottom: 8px;
}
.sb-name {
font-family: "Libre Baskerville", "Georgia", serif;
font-size: 28px;
font-weight: bold;
color: #922610;
margin: 0 0 4px 0;
line-height: 1.15;
}
.sb-type {
font-style: italic;
font-size: 14px;
color: #555;
margin-bottom: 6px;
}
.red-bar {
height: 3px;
background-color: #922610;
margin: 10px 0;
border-radius: 1px;
}
.sb-vitals, .sb-meta, .sb-traits {
font-size: 14px;
line-height: 1.45;
color: #333;
}
.vital-row, .meta-row {
margin: 4px 0;
}
.sb-abilities {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 4px;
text-align: center;
padding: 6px 0;
}
.sb-ability {
display: flex;
flex-direction: column;
}
.ab-label {
font-weight: bold;
font-size: 12px;
color: #922610;
}
.ab-val {
font-size: 13.5px;
margin-top: 2px;
color: #222;
}
.sb-traits {
margin-top: 10px;
}
.trait-block, .action-block {
font-size: 14px;
line-height: 1.45;
margin: 8px 0;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
text-align: justify;
}
.trait-block:last-child, .action-block:last-child {
border-bottom: none;
padding-bottom: 0;
}
.sb-section {
margin-top: 16px;
}
.section-title {
font-family: "Libre Baskerville", "Georgia", serif;
font-size: 20px;
font-weight: bold;
color: #922610;
margin: 0 0 4px 0;
}
.section-divider {
height: 1.5px;
background-color: #922610;
margin-bottom: 8px;
}
.legendary-preamble {
font-size: 13.5px;
font-style: italic;
line-height: 1.4;
margin: 6px 0 10px 0;
}
/* Responsive adjustments for phones */
@media (max-width: 480px) {
.stat-block-wrapper {
padding: 8px;
}
.stat-block {
padding: 14px;
}
.sb-name {
font-size: 24px;
}
.ab-val {
font-size: 12px;
}
}

View File

@@ -0,0 +1,234 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{display_name}} - Stat Block</title>
<link rel="stylesheet" href="stat_block.css">
</head>
<body class="stat-block-wrapper">
<div class="stat-block">
<!-- Header -->
<header class="sb-header">
<h1 class="sb-name">{{display_name}}</h1>
<div class="sb-type">
{{#if properties.size}}{{properties.size}}{{else}}Medium{{/if}}
{{#if properties.type}}{{properties.type}}{{else}}Humanoid{{/if}}{{#if properties.subtype}} ({{properties.subtype}}){{/if}},
{{#if properties.alignment}}{{properties.alignment}}{{else}}unaligned{{/if}}
</div>
</header>
<div class="red-bar"></div>
<!-- Core Vitals -->
<div class="sb-vitals">
<div class="vital-row">
<strong>Armor Class</strong> {{val properties.armor_class}}{{#if properties.armor_description}} ({{properties.armor_description}}){{/if}}
</div>
<div class="vital-row">
<strong>Hit Points</strong>
{{#if properties.hit_points.current}}
{{properties.hit_points.current}}
{{else if properties.hit_points.max}}
{{properties.hit_points.max}}
{{else}}
{{val properties.hit_points}}
{{/if}}
{{#if properties.hit_points.formula}}
({{properties.hit_points.formula}})
{{else if properties.hit_points.hit_dice}}
({{properties.hit_points.hit_dice}})
{{/if}}
</div>
<div class="vital-row">
<strong>Speed</strong>
{{#if properties.speed_desc}}
{{properties.speed_desc}}
{{else if properties.speed}}
{{properties.speed}} ft.
{{/if}}
</div>
</div>
<div class="red-bar"></div>
<!-- Ability Scores Table -->
<div class="sb-abilities">
<div class="sb-ability">
<span class="ab-label">STR</span>
<span class="ab-val">{{val properties.abilities.strength}} ({{modText properties.abilities.strength}})</span>
</div>
<div class="sb-ability">
<span class="ab-label">DEX</span>
<span class="ab-val">{{val properties.abilities.dexterity}} ({{modText properties.abilities.dexterity}})</span>
</div>
<div class="sb-ability">
<span class="ab-label">CON</span>
<span class="ab-val">{{val properties.abilities.constitution}} ({{modText properties.abilities.constitution}})</span>
</div>
<div class="sb-ability">
<span class="ab-label">INT</span>
<span class="ab-val">{{val properties.abilities.intelligence}} ({{modText properties.abilities.intelligence}})</span>
</div>
<div class="sb-ability">
<span class="ab-label">WIS</span>
<span class="ab-val">{{val properties.abilities.wisdom}} ({{modText properties.abilities.wisdom}})</span>
</div>
<div class="sb-ability">
<span class="ab-label">CHA</span>
<span class="ab-val">{{val properties.abilities.charisma}} ({{modText properties.abilities.charisma}})</span>
</div>
</div>
<div class="red-bar"></div>
<!-- Optional Monster Meta -->
<div class="sb-meta">
{{#if properties.saving_throws}}
<div class="meta-row">
<strong>Saving Throws</strong> {{join properties.saving_throws}}
</div>
{{/if}}
{{#if properties.skills}}
<div class="meta-row">
<strong>Skills</strong> {{join properties.skills}}
</div>
{{/if}}
{{#if properties.damage_vulnerabilities}}
<div class="meta-row">
<strong>Damage Vulnerabilities</strong> {{join properties.damage_vulnerabilities}}
</div>
{{/if}}
{{#if properties.damage_resistances}}
<div class="meta-row">
<strong>Damage Resistances</strong> {{join properties.damage_resistances}}
</div>
{{/if}}
{{#if properties.damage_immunities}}
<div class="meta-row">
<strong>Damage Immunities</strong> {{join properties.damage_immunities}}
</div>
{{/if}}
{{#if properties.condition_immunities}}
<div class="meta-row">
<strong>Condition Immunities</strong> {{join properties.condition_immunities}}
</div>
{{/if}}
{{#if properties.senses}}
<div class="meta-row">
<strong>Senses</strong> {{properties.senses}}
</div>
{{/if}}
{{#if properties.languages}}
<div class="meta-row">
<strong>Languages</strong> {{join properties.languages}}
</div>
{{/if}}
{{#if properties.challenge_rating}}
<div class="meta-row">
<strong>Challenge</strong> {{properties.challenge_rating}}
</div>
{{else if properties.cr}}
<div class="meta-row">
<strong>Challenge</strong> {{properties.cr}}{{#if properties.xp}} ({{properties.xp}} XP){{/if}}
</div>
{{/if}}
{{#if properties.proficiency_bonus}}
<div class="meta-row">
<strong>Proficiency Bonus</strong> +{{properties.proficiency_bonus}}
</div>
{{else if (calcProf properties.level properties.cr)}}
<div class="meta-row">
<strong>Proficiency Bonus</strong> +{{calcProf properties.level properties.cr}}
</div>
{{/if}}
</div>
<!-- Special Traits / Features -->
{{#if properties.traits}}
<div class="sb-traits">
{{#each properties.traits}}
<div class="trait-block">
<em><strong>{{this.name}}:</strong></em> {{formatText this.desc}}
</div>
{{/each}}
</div>
{{/if}}
<!-- Actions Section -->
{{#if properties.actions}}
<section class="sb-section">
<h2 class="section-title">Actions</h2>
<div class="section-divider"></div>
{{#each properties.actions}}
<div class="action-block">
<em><strong>{{this.name}}:</strong></em> {{formatText this.desc}}
</div>
{{/each}}
</section>
{{/if}}
<!-- Bonus Actions Section -->
{{#if properties.bonus_actions}}
<section class="sb-section">
<h2 class="section-title">Bonus Actions</h2>
<div class="section-divider"></div>
{{#each properties.bonus_actions}}
<div class="action-block">
<em><strong>{{this.name}}:</strong></em> {{formatText this.desc}}
</div>
{{/each}}
</section>
{{/if}}
<!-- Reactions Section -->
{{#if properties.reactions}}
<section class="sb-section">
<h2 class="section-title">Reactions</h2>
<div class="section-divider"></div>
{{#each properties.reactions}}
<div class="action-block">
<em><strong>{{this.name}}:</strong></em> {{formatText this.desc}}
</div>
{{/each}}
</section>
{{/if}}
<!-- Legendary Actions Section -->
{{#if properties.legendary_actions}}
<section class="sb-section">
<h2 class="section-title">Legendary Actions</h2>
<div class="section-divider"></div>
{{#if properties.legendary_actions.description}}
<p class="legendary-preamble">{{formatText properties.legendary_actions.description}}</p>
{{else if properties.legendariesDescription}}
<p class="legendary-preamble">{{formatText properties.legendariesDescription}}</p>
{{/if}}
{{#if properties.legendary_actions.actions}}
{{#each properties.legendary_actions.actions}}
<div class="action-block">
<em><strong>{{this.name}}:</strong></em> {{formatText this.desc}}
</div>
{{/each}}
{{else if properties.legendaries}}
{{#each properties.legendaries}}
<div class="action-block">
<em><strong>{{this.name}}:</strong></em> {{formatText this.desc}}
</div>
{{/each}}
{{/if}}
</section>
{{/if}}
</div>
</body>
</html>