diff --git a/world/MANIFEST.md b/world/MANIFEST.md new file mode 100644 index 0000000..3c25560 --- /dev/null +++ b/world/MANIFEST.md @@ -0,0 +1,34 @@ +# World Manifest Reference + +The file `world/manifest.toml` defines the world identity and spawn location. There is exactly one manifest per world. + +## Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | Yes | World name (e.g. shown at login). | +| `spawn_room` | string | Yes | Full room ID where new characters and respawned dead characters appear (e.g. `"town:town_square"`). Must reference an existing room. | + +## Example + +```toml +name = "The Shattered Realm" +spawn_room = "town:town_square" +``` + +## Directory layout + +The loader expects (under `world/`): + +- `manifest.toml` — this file +- `races/*.toml` — race definitions (see `races/RACES.md`) +- `classes/*.toml` — class definitions (see `classes/CLASSES.md`) +- `guilds/*.toml` — guild definitions (see `guilds/GUILDS.md`) +- `spells/*.toml` — spell definitions (see `spells/SPELLS.md`) +- `/` — one folder per region (e.g. `town/`), each containing: + - `region.toml` — region metadata (see `town/REGION.md`) + - `rooms/*.toml` — rooms (see `town/rooms/ROOMS.md`) + - `npcs/*.toml` — NPCs (see `town/npcs/NPCS.md`) + - `objects/*.toml` — objects (see `town/objects/OBJECTS.md`) + +Folder names `races`, `classes`, `guilds`, and `spells` are reserved; other top-level directories are treated as regions. diff --git a/world/classes/CLASSES.md b/world/classes/CLASSES.md new file mode 100644 index 0000000..85dd4ee --- /dev/null +++ b/world/classes/CLASSES.md @@ -0,0 +1,64 @@ +# Class TOML Reference + +Each file in `world/classes/` defines one class. The filename (without `.toml`) becomes the class ID with prefix `class:` (e.g. `warrior.toml` → `class:warrior`). + +## Top-level fields + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `name` | string | Yes | — | Display name of the class. | +| `description` | string | Yes | — | Short description shown in chargen. | +| `hidden` | boolean | No | `false` | If `true`, the class does not appear in character creation. Use for NPC-only classes (e.g. Peasant, Creature). | +| `guild` | string | No | — | Guild ID (e.g. `"guild:warriors_guild"`). If set, new characters who choose this class automatically join this guild at level 1 and receive that guild’s base mana/endurance. | + +## `[base_stats]` — Starting stats at level 1 + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `max_hp` | integer | No | `0` | Base maximum HP before race/stat modifiers. | +| `attack` | integer | No | `0` | Base attack before modifiers. | +| `defense` | integer | No | `0` | Base defense before modifiers. | + +## `[growth]` — Per-level gains + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `hp_per_level` | integer | No | `0` | HP added each level. | +| `attack_per_level` | integer | No | `0` | Attack added each level. | +| `defense_per_level` | integer | No | `0` | Defense added each level. | + +## Minimal example (hidden NPC class) + +```toml +name = "Peasant" +description = "A common folk with no particular training." +hidden = true + +[base_stats] +max_hp = 50 +attack = 4 +defense = 4 + +[growth] +hp_per_level = 5 +attack_per_level = 1 +defense_per_level = 1 +``` + +## Example with guild (playable class) + +```toml +name = "Warrior" +description = "Masters of arms and armor." +guild = "guild:warriors_guild" + +[base_stats] +max_hp = 120 +attack = 14 +defense = 12 + +[growth] +hp_per_level = 15 +attack_per_level = 3 +defense_per_level = 2 +``` diff --git a/world/guilds/GUILDS.md b/world/guilds/GUILDS.md new file mode 100644 index 0000000..f064d44 --- /dev/null +++ b/world/guilds/GUILDS.md @@ -0,0 +1,58 @@ +# Guild TOML Reference + +Each file in `world/guilds/` defines one guild. The filename (without `.toml`) becomes the guild ID with prefix `guild:` (e.g. `warriors_guild.toml` → `guild:warriors_guild`). + +## Top-level fields + +Put these **before** any `[section]` headers so they are not parsed as part of a table. + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `name` | string | Yes | — | Display name of the guild. | +| `description` | string | Yes | — | Short description. | +| `max_level` | integer | No | `50` | Maximum guild level. | +| `resource` | string | No | `"mana"` | Primary resource: `"mana"` or `"endurance"`. | +| `base_mana` | integer | No | `0` | Mana granted when joining (and per new member level if used). | +| `base_endurance` | integer | No | `0` | Endurance granted when joining. | +| `spells` | array of strings | No | `[]` | Spell IDs (e.g. `"spell:power_strike"`) this guild grants. Order can matter for display. | +| `min_player_level` | integer | No | `0` | Minimum character level required to join. | +| `race_restricted` | array of strings | No | `[]` | Race IDs (e.g. `"race:dragon"`) that cannot join this guild. | + +## `[growth]` — Per guild-level gains + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `hp_per_level` | integer | No | `0` | HP per guild level. | +| `mana_per_level` | integer | No | `0` | Mana per guild level. | +| `endurance_per_level` | integer | No | `0` | Endurance per guild level. | +| `attack_per_level` | integer | No | `0` | Attack per guild level. | +| `defense_per_level` | integer | No | `0` | Defense per guild level. | + +## Important: TOML layout + +Top-level keys such as `spells`, `min_player_level`, and `race_restricted` must appear **before** the `[growth]` section. Otherwise they are parsed as part of `[growth]` and ignored. + +## Example + +```toml +name = "Warriors Guild" +description = "Masters of martial combat." +max_level = 50 +resource = "endurance" +base_mana = 0 +base_endurance = 50 +spells = ["spell:power_strike", "spell:battle_cry", "spell:shield_wall", "spell:whirlwind"] +min_player_level = 0 +race_restricted = [] + +[growth] +hp_per_level = 8 +mana_per_level = 0 +endurance_per_level = 5 +attack_per_level = 2 +defense_per_level = 1 +``` + +## Spell IDs + +Spell IDs are the spell’s file stem with prefix `spell:` (e.g. `world/spells/power_strike.toml` → `spell:power_strike`). Spells are defined in `world/spells/*.toml`; see `world/spells/SPELLS.md`. diff --git a/world/races/RACES.md b/world/races/RACES.md new file mode 100644 index 0000000..e0b46b8 --- /dev/null +++ b/world/races/RACES.md @@ -0,0 +1,117 @@ +# Race TOML Reference + +Each file in `world/races/` defines one race. The filename (without `.toml`) becomes the race ID with prefix `race:` (e.g. `human.toml` → `race:human`). + +## Top-level fields + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `name` | string | Yes | — | Display name of the race. | +| `description` | string | Yes | — | Short description shown in chargen and elsewhere. | +| `metarace` | string | No | — | Category (e.g. `"animal"`, `"draconic"`). Used for flavour and filtering; NPCs without a fixed race are chosen from races that are not hidden (metarace does not exclude them from random NPC pool). | +| `hidden` | boolean | No | `false` | If `true`, the race does not appear in character creation. Use for NPC-only races (e.g. Beast). | +| `default_class` | string | No | — | Class ID (e.g. `"class:peasant"`) used as the default class for NPCs of this race when the NPC has no fixed class. Omit for “random compatible class” (e.g. Dragon). | + +## `[stats]` — Stat modifiers + +All values are integers applied as modifiers (e.g. +1, -2). Defaults are `0` if omitted. + +| Field | Description | +|-------|-------------| +| `strength` | STR modifier. | +| `dexterity` | DEX modifier. | +| `constitution` | CON modifier. | +| `intelligence` | INT modifier. | +| `wisdom` | WIS modifier. | +| `perception` | PER modifier. | +| `charisma` | CHA modifier. | + +## `[body]` + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `size` | string | No | `"medium"` | One of: `"tiny"`, `"small"`, `"medium"`, `"large"`, `"huge"`. Flavour and potential future rules. | +| `weight` | integer | No | `0` | Weight in arbitrary units. | +| `slots` | array of strings | No | humanoid default | Equipment slot names this race can use. If empty, default humanoid slots are used: `head`, `neck`, `torso`, `legs`, `feet`, `main_hand`, `off_hand`, `finger`, `finger`. | + +## `[natural]` — Natural armor and attacks + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `armor` | integer | No | `0` | Natural armor bonus added to defense. | + +### `[natural.attacks.]` + +Each key under `natural.attacks` defines one natural attack (e.g. `bite`, `claw`, `fire_breath`). + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `damage` | integer | No | `0` | Base damage. | +| `type` | string | No | `"physical"` | Damage type (e.g. `"physical"`, `"fire"`, `"magical"`). | +| `cooldown_ticks` | integer | No | — | If set, minimum ticks between uses of this attack. | + +## `[resistances]` + +Map of damage type → multiplier (float). + +- `0.0` = immune +- `1.0` = normal +- `1.5` = vulnerable (e.g. 50% more damage) +- `< 1.0` = resistant (e.g. `0.5` = half damage) + +Example: `fire = 0.0`, `cold = 1.5`, `physical = 0.7` + +## `traits` and `disadvantages` + +Top-level arrays of strings (free-form). Shown in chargen and used for flavour. + +- `traits` — e.g. `["darkvision", "lucky"]` +- `disadvantages` — e.g. `["light_sensitivity"]` + +## `[regen]` — Regeneration multipliers + +Multipliers applied to passive HP/mana/endurance regen. Float; default `1.0`. + +| Field | Description | +|-------|-------------| +| `hp` | HP regen multiplier. | +| `mana` | Mana regen multiplier. | +| `endurance` | Endurance regen multiplier. | + +## `[guild_compatibility]` + +Used when picking a random class for NPCs (and potentially future guild rules). Guild IDs in each list are string identifiers (e.g. `"guild:warriors_guild"`). + +| Field | Description | +|-------|-------------| +| `good` | Guilds this race is well suited to. | +| `average` | Guilds with no special modifier. | +| `poor` | Guilds this race is poorly suited to. | +| `restricted` | Guilds this race cannot join. | + +All default to empty arrays. + +## `[misc]` + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `lifespan` | integer | No | — | Flavour lifespan. | +| `diet` | string | No | — | Flavour diet (e.g. `"omnivore"`, `"carnivore"`). | +| `xp_rate` | float | No | `1.0` | Multiplier for XP gain (e.g. `0.7` = slower leveling). | +| `natural_terrain` | array of strings | No | `[]` | Flavour terrain list. | +| `vision` | array of strings | No | `[]` | Vision types (e.g. `["normal", "darkvision", "infravision"]`). | + +## Minimal example + +```toml +name = "Human" +description = "Versatile and adaptable." +default_class = "class:peasant" + +[stats] +charisma = 1 +``` + +## Full example (excerpt) + +See `dragon.toml` for a race using body slots, natural attacks, resistances, regen, and guild compatibility. diff --git a/world/spells/SPELLS.md b/world/spells/SPELLS.md new file mode 100644 index 0000000..df54c49 --- /dev/null +++ b/world/spells/SPELLS.md @@ -0,0 +1,85 @@ +# Spell TOML Reference + +Each file in `world/spells/` defines one spell or skill. The filename (without `.toml`) becomes the spell ID with prefix `spell:` (e.g. `magic_missile.toml` → `spell:magic_missile`). Spells are referenced from guilds in `world/guilds/*.toml`. + +## Top-level fields + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `name` | string | Yes | — | Display name. | +| `description` | string | Yes | — | Short description shown in `spells` and `guild info`. | +| `spell_type` | string | No | `"offensive"` | One of: `"offensive"`, `"heal"`, `"utility"`. Affects when and how the spell can be used (e.g. heal/utility out of combat; offensive usually in combat). | +| `damage` | integer | No | `0` | Base damage for offensive spells. | +| `heal` | integer | No | `0` | HP restored for heal spells. | +| `damage_type` | string | No | `"magical"` | Damage type (e.g. `"physical"`, `"fire"`, `"magical"`, `"holy"`, `"poison"`). | +| `cost_mana` | integer | No | `0` | Mana cost to cast. | +| `cost_endurance` | integer | No | `0` | Endurance cost to cast. | +| `cooldown_ticks` | integer | No | `0` | Ticks before the spell can be used again. | +| `casting_ticks` | integer | No | `0` | Ticks before the spell resolves (future use; currently casting is one tick). | +| `min_guild_level` | integer | No | `0` | Minimum guild level required to know/use this spell. | +| `effect` | string | No | — | Status effect kind applied (e.g. `"poison"`, `"regen"`, `"defense_up"`). | +| `effect_duration` | integer | No | `0` | Duration of the effect in ticks. | +| `effect_magnitude` | integer | No | `0` | Magnitude (e.g. damage per tick for poison, heal per tick for regen). | + +## Spell types + +- **offensive** — Deals damage to current combat target. Requires combat; blocked out of combat. +- **heal** — Restores HP. Can be used in or out of combat; out of combat resolves immediately. +- **utility** — Buffs, cleanses, etc. Can apply status effects; out of combat resolves immediately. + +## Examples + +**Offensive (mana):** + +```toml +name = "Magic Missile" +description = "Hurl bolts of pure arcane energy." +spell_type = "offensive" +damage = 15 +damage_type = "magical" +cost_mana = 10 +cooldown_ticks = 1 +min_guild_level = 1 +``` + +**Heal:** + +```toml +name = "Heal" +description = "Channel divine energy to mend wounds." +spell_type = "heal" +heal = 25 +cost_mana = 15 +cooldown_ticks = 2 +min_guild_level = 1 +``` + +**Utility with effect:** + +```toml +name = "Battle Cry" +description = "A thunderous war shout that steels your resolve." +spell_type = "utility" +cost_endurance = 10 +cooldown_ticks = 10 +min_guild_level = 3 +effect = "regen" +effect_duration = 5 +effect_magnitude = 4 +``` + +**Offensive with DoT:** + +```toml +name = "Poison Blade" +description = "Coat your weapon with virulent toxin." +spell_type = "offensive" +damage = 8 +damage_type = "poison" +cost_endurance = 10 +cooldown_ticks = 6 +min_guild_level = 3 +effect = "poison" +effect_duration = 4 +effect_magnitude = 3 +``` diff --git a/world/town/REGION.md b/world/town/REGION.md new file mode 100644 index 0000000..f40b43e --- /dev/null +++ b/world/town/REGION.md @@ -0,0 +1,28 @@ +# Region TOML Reference + +Each region is a directory under `world/` (e.g. `world/town/`) containing a `region.toml` file. The directory name is the region ID (e.g. `town`). Room, NPC, and object IDs in this region are prefixed with `":"` (e.g. `town:tavern`). + +## Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | Yes | Display name of the region (e.g. for logs or future use). | + +Additional keys (e.g. `description`) may be present in the file; only `name` is required by the loader. + +## Example + +```toml +name = "Thornwall" +description = "A fortified trading town at the crossroads of the known world." +``` + +## Region contents + +Place TOML files in these subdirectories of the region folder: + +- `rooms/*.toml` — room definitions (see `rooms/ROOMS.md`) +- `npcs/*.toml` — NPC definitions (see `npcs/NPCS.md`) +- `objects/*.toml` — object definitions (see `objects/OBJECTS.md`) + +The server loads all regions whose folder contains a `region.toml` file. diff --git a/world/town/npcs/NPCS.md b/world/town/npcs/NPCS.md new file mode 100644 index 0000000..96dc406 --- /dev/null +++ b/world/town/npcs/NPCS.md @@ -0,0 +1,89 @@ +# NPC TOML Reference + +Each file in a region’s `npcs/` folder (e.g. `world/town/npcs/`) defines one NPC template. The NPC ID is `":"` (e.g. `barkeep.toml` in region `town` → `town:barkeep`). Race and class are resolved at **spawn time** (and again on respawn if not fixed); omit them for random selection. + +## Top-level fields + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `name` | string | Yes | — | Display name. | +| `description` | string | Yes | — | Shown when the player looks at or examines the NPC. | +| `room` | string | Yes | — | Full room ID where the NPC appears (e.g. `"town:tavern"`). | +| `base_attitude` | string | No | `"neutral"` | One of: `friendly`, `neutral`, `wary`, `aggressive`, `hostile`. Hostile NPCs auto-engage players in the room. | +| `faction` | string | No | — | Optional faction ID for attitude grouping. | +| `race` | string | No | — | Race ID (e.g. `"race:beast"`, `"race:human"`). If omitted, a random non-hidden race is chosen at each spawn/respawn. | +| `class` | string | No | — | Class ID (e.g. `"class:peasant"`, `"class:rogue"`). If omitted, the race’s `default_class` is used, or a random compatible non-hidden class. | +| `respawn_secs` | integer | No | — | If set, the NPC respawns this many seconds after death. Race/class are re-rolled on respawn unless fixed above. | +| `dialogue` | table | No | — | See below. | +| `combat` | table | No | — | If set, the NPC can be attacked and has combat stats. If omitted, the NPC has default combat stats when attacked. | + +## `[dialogue]` + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `greeting` | string | No | — | Line shown when a player uses `talk` on this NPC (only if attitude allows talking). | + +## `[combat]` + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `max_hp` | integer | Yes | — | Maximum HP. | +| `attack` | integer | Yes | — | Attack value. | +| `defense` | integer | Yes | — | Defense value. | +| `xp_reward` | integer | No | `0` | XP awarded when the NPC is killed. | + +## Examples + +**Fixed race/class (e.g. animal):** + +```toml +name = "Giant Rat" +description = "A mangy rat the size of a small dog." +room = "town:cellar" +base_attitude = "hostile" +race = "race:beast" +class = "class:creature" +respawn_secs = 60 + +[combat] +max_hp = 25 +attack = 6 +defense = 2 +xp_reward = 15 +``` + +**Random race, fixed class (e.g. barkeep):** + +```toml +name = "Grizzled Barkeep" +description = "A weathered man with thick forearms." +room = "town:tavern" +base_attitude = "friendly" + +[dialogue] +greeting = "Welcome to The Rusty Tankard." +``` + +(No `race` or `class` → random race each spawn, default class from race, e.g. Peasant for humanoids.) + +**Fixed class, random race:** + +```toml +name = "Shadowy Thief" +description = "A cloaked figure lurking in the darkness." +room = "town:dark_alley" +base_attitude = "aggressive" +faction = "underworld" +class = "class:rogue" +respawn_secs = 90 + +[combat] +max_hp = 45 +attack = 12 +defense = 6 +xp_reward = 35 +``` + +## Display + +`look ` and `examine ` show the NPC’s **resolved** race and class (from the current spawn instance). diff --git a/world/town/objects/OBJECTS.md b/world/town/objects/OBJECTS.md new file mode 100644 index 0000000..5cce1ca --- /dev/null +++ b/world/town/objects/OBJECTS.md @@ -0,0 +1,80 @@ +# Object TOML Reference + +Each file in a region’s `objects/` folder (e.g. `world/town/objects/`) defines one object type. The object ID is `":"` (e.g. `rusty_sword.toml` in region `town` → `town:rusty_sword`). + +## Top-level fields + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `name` | string | Yes | — | Display name. | +| `description` | string | Yes | — | Shown when the player looks at or examines the object. | +| `room` | string | No | — | Full room ID where the object is placed (e.g. `"town:forge"`). If omitted, the object does not appear in the world until given by script or placed elsewhere. | +| `kind` | string | No | — | Flavour/legacy: e.g. `"weapon"`, `"armor"`, `"consumable"`. If `slot` is not set, `weapon` → `main_hand`, `armor` → `torso` for equip. | +| `slot` | string | No | — | Equipment slot name (e.g. `"main_hand"`, `"off_hand"`, `"torso"`, `"head"`). Must be a slot the equipper’s race has. If set, overrides `kind` for slot choice. | +| `takeable` | boolean | No | `false` | If `true`, players can take the object and put it in inventory. | +| `stats` | table | No | — | See `[stats]` below. | + +## `[stats]` + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `damage` | integer | No | — | Weapon damage bonus when equipped in a weapon slot. | +| `armor` | integer | No | — | Armor bonus when equipped in an armor slot. | +| `heal_amount` | integer | No | — | HP restored when the object is **used** (consumable). The object is consumed on use. | + +## Examples + +**Weapon (explicit slot):** + +```toml +name = "Rusty Sword" +description = "A battered iron blade with a cracked leather grip." +room = "town:cellar" +kind = "weapon" +slot = "main_hand" +takeable = true + +[stats] +damage = 6 +``` + +**Shield:** + +```toml +name = "Iron Shield" +description = "A dented but serviceable round shield." +room = "town:forge" +slot = "off_hand" +takeable = true + +[stats] +armor = 4 +``` + +**Consumable:** + +```toml +name = "Healing Potion" +description = "A small glass vial filled with a shimmering red liquid." +room = "town:temple" +kind = "consumable" +takeable = true + +[stats] +heal_amount = 30 +``` + +**Non-takeable (e.g. scenery):** + +```toml +name = "Stone Fountain" +description = "A worn fountain, water trickling quietly." +room = "town:town_square" +takeable = false +``` + +## Equipment and races + +- A race defines which slots it has (see `world/races/RACES.md`). Equipping fails if the race does not have the object’s `slot`. +- If `slot` is omitted, `kind = "weapon"` defaults to `main_hand`, `kind = "armor"` to `torso`; other kinds have no default slot. +- Items are treated as fitting any race that has the slot (no size restriction). diff --git a/world/town/rooms/ROOMS.md b/world/town/rooms/ROOMS.md new file mode 100644 index 0000000..0c2f39a --- /dev/null +++ b/world/town/rooms/ROOMS.md @@ -0,0 +1,33 @@ +# Room TOML Reference + +Each file in a region’s `rooms/` folder (e.g. `world/town/rooms/`) defines one room. The room ID is `":"` (e.g. `town_square.toml` in region `town` → `town:town_square`). Exits reference these full IDs. + +## Top-level fields + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `name` | string | Yes | — | Room title shown to players. | +| `description` | string | Yes | — | Room description. Can be a multi-line string (`"""..."""`). | +| `exits` | table | No | `{}` | Map of direction → room ID. Direction keys are lowercase (e.g. `north`, `south`, `east`, `west`, `up`, `down`). Values are full room IDs (e.g. `"town:tavern"`). | + +## Example + +```toml +name = "Town Square" +description = """\ +You stand in the heart of Thornwall. A worn stone fountain sits at the \ +center, water trickling quietly. Cobblestone paths branch in every \ +direction.""" + +[exits] +north = "town:tavern" +east = "town:market" +west = "town:temple" +south = "town:dark_alley" +``` + +## Notes + +- NPCs and objects are placed in rooms via their own TOML: NPCs have a `room` field, objects have a `room` field. The server builds room contents from those references. +- Exits can point to rooms in other regions; use the full ID (e.g. `"dungeon:entrance"`). +- The world `manifest.toml` must list a valid `spawn_room` ID that exists.