AETHERMUD GUIDE

AetherMUD Complete Guide

The spiritual successor to RiftsMUD (1998-2005). Built on Evennia 6.x and Python 3.11. Six documents consolidated into one reference.

01Builder Guide

First Steps

Evennia gives you two ways to build: directly in-game with commands (covered here) and in Python code outside the game. In-game commands are the fastest way to prototype rooms and objects.

Legacy @ prefix: If you are used to MU* servers that prefix commands with @ (e.g., @create), Evennia accepts that syntax and ignores the leading @.

Basic Workflow

Almost every build task follows the same four-step pattern: look, create, describe, connect.

in-game commands
look
dig Barracks = south;s, north;n
south
desc here = Rows of iron bunks line the walls. The air smells of oil and sweat.
create/drop a worn rifle
desc worn rifle = A battered rifle with a cracked stock. Still functional.

Rooms and Exits

room and exit commands
dig Ruined Highway = east;e, west;w
dig Guard Tower
tunnel n = Coalition Checkpoint
tunnel sw = Wasteland Cliff
open north;n = Guard Tower
open north;n = #42
desc here = Twisted asphalt and burning wreckage stretch to the horizon.
set here/zone = rifts_wasteland
set here/is_dark = 1
tag here = outdoor
tag here = warzone
tel Guard Tower
tel #42

Objects

object commands
create a vibro-blade
create/drop Coalition heavy armor
create TX-30 Ion Rifle : typeclasses.objects.Weapon
create/drop Vibro-Blade
set vibro-blade/damage_dice = 3d6+2
set vibro-blade/damage_type = md
set vibro-blade/mdc = 20
set vibro-blade/weapon_type = melee
create/drop Coalition Heavy Armor
set coalition heavy armor/mdc = 100
set coalition heavy armor/armor_type = heavy
set coalition heavy armor/weight = 40

NPCs

NPC commands
create/drop Coalition Grunt : typeclasses.npcs.NPC
desc coalition grunt = A helmeted CS soldier in full MDC gear.
set coalition grunt/race = human
set coalition grunt/level = 4
set coalition grunt/allegiance = coalition
set coalition grunt/aggressive = 0
set coalition grunt/hit_dice = 4d8
examine coalition grunt

Locks and Permissions

lock syntax
lock north = traverse:tag(cs_soldier)
lock vibro-blade = get:id(#23)
lock iron chest = get:perm(Builder)
lock relic = view:all();get:perm(Admin)
TypeControlsCommon function
traverseWho can pass through exittag(tagname), perm(Level)
getWho can pick up objectid(#dbref), perm(Level)
viewWho can see object in roomall(), none()
deleteWho can destroy the objectperm(Admin)
puppetWho can possess characterid(#dbref)
Note: call is not a built-in Evennia lock type. If your codebase defines it as a custom lock function, document that separately. Otherwise omit it from lock definitions.

Scripts and Timers

script commands
script coalition grunt = scripts.patrol.PatrolScript
script coalition grunt
script/stop coalition grunt = patrol
scripts/patrol.py
from evennia import DefaultScript

class PatrolScript(DefaultScript):
    def at_script_creation(self):
        self.key = "patrol"
        self.interval = 30
        self.persistent = True

    def at_repeat(self):
        npc = self.obj
        exits = npc.location.exits
        if exits:
            npc.move_to(exits[0].destination)

Batch Commands

batch commands
batchcommand world.rifts_hellpit

batchcommand/interactive world.rifts_hellpit
world/example.ev format
dig Demon Nest = south;s, north;n

south

desc here =
Pulsing walls of flesh contract rhythmically. The air reeks of sulfur.

set here/zone = rifts_hellpit
tag here = outdoor

create/drop Demon Lord : typeclasses.npcs.NPC
set demon lord/race = demon
set demon lord/level = 12
set demon lord/aggressive = 1

Web Builder UI

For serious area building, an external web-based builder is more efficient than in-game commands. Builders work without logging into the game, form-based input reduces typos, and valid .ev files generate automatically from structured data.

The architecture is a standalone web app (Python/Flask) running separately from Evennia. Builders fill forms for rooms, exits, objects, and NPCs. The app generates a valid .ev batch file. The builder downloads it or the app writes it into the game's world/ directory. The builder then runs batchcommand world.area_name in-game to apply it.

app.py -- minimal Flask example
from flask import Flask, request
app = Flask(__name__)

@app.route('/build-room', methods=['POST'])
def build_room():
    data = request.json
    content = f"dig {data['name']} =\n\n"
    content += f"desc here = {data['description']}\n\n"
    content += f"set here/zone = {data['zone']}\n"
    with open('world/generated_area.ev', 'w') as f:
        f.write(content)
    return {'status': 'saved'}

Attributes Reference

AttributeIn-Game SetPython AccessNotes
raceset npc/race = brodkilnpc.db.raceSpecies string
levelset npc/level = 6npc.db.levelInteger 1-20+
hit_diceset npc/hit_dice = 4d8npc.db.hit_diceDice string
aggressiveset npc/aggressive = 1npc.db.aggressive1 = attacks on sight
allegianceset npc/allegiance = coalitionnpc.db.allegianceFaction string
damage_diceset obj/damage_dice = 3d6obj.db.damage_diceDice string
damage_typeset obj/damage_type = mdobj.db.damage_typemd, sd, fire, plasma
mdcset obj/mdc = 100obj.db.mdcMega-Damage Capacity
zoneset room/zone = rifts_wastelandroom.db.zoneArea identifier
is_darkset room/is_dark = 1room.db.is_darkLight/dark flag
Type note: Evennia set stores strings. Cast in Python as needed: int(npc.db.level). Set complex types directly from Python: obj.db.skills = {}
02Developer Guide

VS Code Setup

VS Code is the recommended editor. Install these extensions: Python (Microsoft), Pylance (Microsoft), Python Debugger (Microsoft), Ruff (Astral Software), SQLite Viewer (Florian Klampfer).

.vscode/settings.json
{
  "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
  "python.analysis.extraPaths": [
    "${workspaceFolder}",
    "${workspaceFolder}/.venv/lib/python3.11/site-packages"
  ],
  "python.analysis.typeCheckingMode": "basic",
  "editor.formatOnSave": true,
  "editor.rulers": [99]
}

Codebase Structure

aethermud/ +-- server/conf/ | +-- settings.py # server configuration +-- typeclasses/ | +-- characters.py # player character, Rifts stats, XP, OCC | +-- npcs.py # NPC base + enemy AI | +-- objects.py # weapon, armor, container typeclasses | +-- rooms.py # zone, dark, radiation fields +-- commands/ | +-- cmdset_character.py # main cmdset | +-- cmd_combat.py # attack, dodge, parry | +-- cmd_character.py # score, skills, inventory +-- systems/ # create this folder + __init__.py | +-- combat.py | +-- chargen.py | +-- occ.py | +-- skills.py | +-- xp.py | +-- faction.py | +-- races.py | +-- frame.py # Unicode border system +-- world/ # .ev batch files +-- scripts/ # combat tick, patrol, regen

Typeclasses

typeclasses/characters.py
from evennia import DefaultCharacter

class Character(DefaultCharacter):
    def at_object_creation(self):
        super().at_object_creation()
        # Eight Rifts primary attributes
        for stat in ["iq","me","ma","ps","pp","pe","pb","spd"]:
            setattr(self.db, stat, 10)
        self.db.mdc = 0; self.db.mdc_max = 0
        self.db.sdc = 30; self.db.sdc_max = 30
        self.db.hp  = 20; self.db.hp_max  = 20
        self.db.ppe = 0; self.db.ppe_max = 0
        self.db.isp = 0; self.db.isp_max = 0
        self.db.occ = None
        self.db.race = None
        self.db.level = 1
        self.db.xp = 0
        self.db.true_name = None
        self.db.race_desc = None
        self.db.known_names = {}
        self.db.allegiance = "neutral"
        self.db.alignment = "unprincipled"
        self.db.attacks_per_melee = 2
        self.db.primary_skills = {}
        self.db.secondary_skills = {}

    def get_display_name(self, looker, **kwargs):
        if looker == self:
            return self.db.true_name or self.key
        if hasattr(looker,"check_permstring") and looker.check_permstring("Builder"):
            return f"{self.db.true_name} [{self.db.race_desc}]"
        known = getattr(looker.db,"known_names",{}) or {}
        if self.id in known:
            return known[self.id]
        return self.db.race_desc or "someone"

    def get_display_name_cap(self, looker, **kwargs):
        n = self.get_display_name(looker)
        return n[0].upper() + n[1:]

    @property
    def is_dead(self):
        return self.db.hp is not None and self.db.hp <= 0

Command Sets

server/conf/settings.py -- wiring
BASE_CHARACTER_TYPECLASS = "typeclasses.characters.Character"
BASE_OBJECT_TYPECLASS    = "typeclasses.objects.Object"
BASE_ROOM_TYPECLASS      = "typeclasses.rooms.Room"
CMDSET_CHARACTER         = "commands.cmdset_character.CharacterCmdSet"
SERVERNAME               = "AetherMUD"
TELNET_PORTS             = [4000]
WEBSERVER_PORTS          = [(4001, 4002)]

Combat System -- MDC/SDC Model

Attack typeTarget has MDC armorTarget has no MDC armor
MDC weaponDamages armor MDC poolInstantly fatal to biological targets
SDC weaponAbsorbed by MDC armor (no effect)Damages SDC then HP

Character Creation Flow

Chargen covers race selection and stat rolling only. OCC is assigned post-creation by a wizard. The EvMenu ends at node_enter_world.

  1. Race selection via EvMenu
  2. Stat rolling (3d6 with re-roll on 16+)
  3. sirname command to set true name
  4. Atlanteans: clan selection (Aerihman not listed)
  5. Enter world with racial baseline, no OCC yet
  6. Later: OCC request to wizard via tell or assistance command

OCC System

systems/occ.py -- structure
OCC_DATA = {
    "coalition_soldier": {
        "name":        "Coalition Soldier",
        "category":    "Coalition",
        "allowed_races": ["human"],
        "alignment":   ["principled","scrupulous","unprincipled","anarchist"],
        "attacks":     [4,5,5,6,6,7,7,8,8,9,9,10,10,11,11],
        "skills":     ["military_etiquette","radio_basic","wpn_energy_rifle"],
        "hp_die":     "d8",
        "sdc_bonus":  30,
    },
    # ... 32 more OCCs
}

Skills

Rifts skills are percentage-based. Base percentage plus per-level bonus. Check: roll d100, succeed if roll is at or below current percentage. Cap at 98%.

XP and Leveling

XP table from Palladium Rifts: 2100, 4200, 8400, 16800, 24600, 33600, 48000, 64000, 88000, 116000, 148000, 184000, 224000, 268000. On level-up: update attacks per melee from OCC table, run skill level-up bonuses, notify player.

Faction System

Faction hostility is determined by a frozenset table of hostile allegiance pairs. is_hostile(a, b) returns True if their allegiances appear together in HOSTILE_PAIRS. Add pairs at runtime with add_hostile_pair(faction_a, faction_b).

03Systems I -- Identity, Commands, Races, OCCs

Design Philosophy

Three systems define what made RiftsMUD feel unique:

  1. Anonymous identity. You never see another player's name until they introduce themselves. You see race and OCC.
  2. The who list as population gauge. You cannot surveil the player base. You see population tier and which wizards are watching.
  3. LPC command syntax. look at <obj> not look <obj>. These syntactic choices feel like a dialect and must be preserved as the primary syntax with modern aliases alongside.

Anonymous Identity System

The key override is get_display_name(looker) on the Character typeclass. See Chapter 2. Every message referencing another character must route through msg_room() in systems/messaging.py which builds separate strings per observer using their known_names dict.

Never use location.msg_contents() for character messages. That method sends the same string to everyone. Use msg_room() for any message that references a character by name. Reserve msg_contents() only for environmental events that do not reference characters.

Introduce / Remember / sirname / greet

CommandSyntaxEffect
introduceintroduce or introduce to <target>Writes true_name into each listener's known_names dict
rememberremember <target> as <nickname>Overwrites your stored name for that character
forgetforget <target>Removes entry from known_names
sirnamesirname <name>Sets your own true_name (one-time or wizard-gated)
greetgreet <target>Polite acknowledgment, does not reveal your name

Who Command

Player countDisplay
0The world is empty. You are alone.
1-4Population: Low
5-14Population: Moderate
15-29Population: High
30+Population: Bustling

Wizards are listed above the population line by their db.wizard_title. Regular players are never individually listed. Set a wizard title with set CharName/wizard_title = Archon of the North.

Full Race List

Green names are OCC-capable (*). All others are RCC-only.

Algor Frost Giant *
an algor frost giant
Basilisk
a basilisk
Bearman *
a bearman
Bogie
a bogie
Brownie
a brownie
Burster
a burster
Changeling *
a changeling
Common Faerie
a common faerie
Common Pixie
a common pixie
Conservator
a conservator
Coyle *
a coyle
Dog Boy
a dog boy
Dwarf *
a dwarf
Elf *
an elf
Equinoid
an equinoid
Fire Dragon
a fire dragon
Frost Pixie
a frost pixie
Gargoyle
a gargoyle
Goblin *
a goblin
Great Horned Dragon
a great horned dragon
Green Wood Faerie
a green wood faerie
Gurgoyle
a gurgoyle
Human *
a human
Ice Dragon
an ice dragon
Jotan *
a jotan
Kankoran *
a kankoran
Mind Melter
a mind melter
Night-Elves Faerie
a night-elves faerie
Nimro Fire Giant *
a nimro fire giant
Ogre *
an ogre
Orc *
an orc
Pogtal
a pogtal
CS Psi-stalker
a psi-stalker
Rahu-man *
a rahu-man
Ratling *
a ratling
Secondary Vampire
a secondary vampire
Silver Bells Faerie
a silver bells faerie
Thunder Lizard Dragon
a thunder lizard dragon
Titan *
a titan
Tree Sprite
a tree sprite
Troll *
a troll
Water Sprite
a water sprite
Werebear
a werebear
Weretiger
a weretiger
Werewolf
a werewolf
Wild Vampire
a wild vampire
True Atlantean *
a true atlantean

Full OCC List

OCCCategoryRace Restriction
Body FixerCivilianNone
Bounty HunterMercenaryNone
City RatCivilianNone
CS GruntCoalitionHuman only
CS RangerCoalitionHuman only
CS Military SpecialistCoalitionHuman only
CS SAMAS RPA PilotCoalitionHuman only
CS Technical OfficerCoalitionHuman only
Cyber-DocCivilianNone
Cyber-KnightIndependentHumanoid races, Principled/Scrupulous
ForgerCriminalNone
Freelance SpyIndependentNone
Glitter Boy PilotIndependentNone
Gifted GypsyIndependentNone
HeadhunterMercenaryNone
ISS PeacekeeperCoalitionHuman only
ISS SpecterCoalitionHuman only
JuicerIndependentHumanoid biology required
Knight (Europe)IndependentHuman, Elf, Dwarf. Principled/Scrupulous.
Master AssassinCriminalNone
Ninja JuicerIndependentHumanoid biology required
NTSET ProtectorIndependentNone
Pirate (S.A.)CriminalNone
Professional ThiefCriminalNone
Rogue ScholarCivilianNone
Royal KnightIndependentHuman, Elf, Dwarf
Sailor (S.A.)IndependentNone
SmugglerCriminalNone
Special Forces (Merc)MercenaryNone
VagabondCivilianNone
Wilderness ScoutIndependentNone
Ley Line WalkerSpecial / LimitedAny OCC-capable, wizard discretion, IQ 12+
Maxi-ManSpecial / LimitedFull wizard discretion, rarely granted
Sunaj AssassinSecretAtlantean, Aerihman clan only. Auto-assigned.

Alignment System

AlignmentCategoryPK EligibleDescription
PrincipledGoodNoStrict code of honor above all else
ScrupulousGoodNoGood but flexible when needed
UnprincipledSelfishNoSelf-interest first, avoids harming innocents
AnarchistSelfishYesDoes whatever is expedient, no consistent code
MiscreantEvilYesPure self-interest, harms others for gain
DiabolicEvilYesEvil for its own sake, enjoys causing suffering
AberrantEvilYesEvil but with a code. Keeps promises.

LPC Syntax Rules

RiftsMUD ran on LPC which used prepositional syntax. These are the primary command forms in AetherMUD. Evennia default forms are added as aliases.

LPC form (primary)Evennia alias
look at <obj>look <obj>
exa <obj>examine <obj>
take <obj> from <container>get <obj>
put <obj> in <container>put <obj> = <container>
give <obj> to <target>give <obj> = <target>
kill <target>attack <target>
tell <target> <message>tell <target> = <message>
whisper <target> <message>whisper <target> = <message>

Both take and get are accepted. take matches the original RiftsMUD syntax.

Command Reference

CommandSyntaxNotes
look / llook at <obj>LPC primary. Strips "at " internally.
i / itemsiInventory list
eq / worneqEquipped items by body location
scorescore / score combat / score skillsCharacter sheet
pskills / sskillspskills / sskillsPrimary and secondary skill lists
sbarsbarCompact HP/SDC/MDC/XP one-liner
kill / attackkill <target>Initiates combat. LPC primary.
dodge / parrydodge / parryManual, costs one attack
autododge / autoparryautododge onToggle auto-defense
wimpywimpy 25Auto-flee when HP below percentage
saysay <message>Room speech. Anonymous by default.
chatchat <message>Global OOC channel
oocooc <message>Out-of-character, current room only
emoteemote <action>Pose. Anonymous name prefixed.
thinkthink <thought>Visible thought bubble
converseconverseToggle: every line treated as say
radioradio <channel> <message>Requires radio item
consentconsent <target>Grant PK permission to target
rest / sleep / wakerestBody position, affects HP regen
briefbrief onSuppress room descriptions on revisit
promptprompt %h/%s/%mCustomize command prompt
assistanceassistanceAlert all online wizards for help
sirnamesirname <name>Set your display name
descriptiondescription <text>Set visible body description
occsoccsList available OCCs for your race
saving throwssaving throwsShow current saving throw bonuses
levelslevelsXP table for your OCC
abilitiesabilitiesRacial special abilities
04Systems II -- Score Sheet, RCCs, Tattoos, Vehicles

Frame System

Score sheets use Unicode box-drawing characters with race-based color themes. Dragon characters get red borders, Atlanteans get blue, Coalition characters get grey-green.

Score Sheet Layout

AETHERMUD CHARACTER SHEET - 109 P.A.
Name: Thurtea          Race: Great Horned Dragon
Align: Scrupulous      OCC:  None (Dragon RCC)

ATTRIBUTES     DAMAGE POOLS           COMBAT
IQ:  14        MDC:  340/340          Atk/Melee: 4
ME:  11        PPE:  95/95            Strike Bon: +2
PS:  18(SN)    ISP:  60/60            HF:  14
PP:  16        Regen: 1d4x10/5min     SPD(fly): 70mph

pskills  sskills  saving throws  abilities

Great Horned Dragon RCC

StatHatchling ValueNotes
MDC1d4x100 + 50MDC creature. No HP/SDC pools.
Attacks per melee4 (hatchling)Grows to 8 physical at adult
Fire breath2d6 MDC, 60ft rangeSeparate from physical attacks
Natural claws2d6 MDCPlus Supernatural PS bonus
Bite3d6 MDC
PS typeSupernaturalAugmented damage bonuses
Horror Factor14Enemies may freeze or flee
Bio-regen1d4x10 MDC per 5 minBecomes per-melee-round at adult
TeleportUp to 5 miles1 attempt per 2 melee rounds
PPE3d6x10Dragon magic (Ley Line Walker list)
ISP3d6x10Major Psionic, 8 powers (not super)
Fly speed70mphIgnores room movement costs
Night vision90ftIgnores room darkness flag
Can equip armorNoNatural body is the armor
Can wield humanoid weaponsNoNatural weapons only

True Atlantean RCC

StatValueNotes
PS typeAugmented (4d6+4)Above human but not supernatural
Base SDC50Plus OCC, skills, tattoo bonuses
Tattoo SDC+10 per tattooMax 40 extra SDC from tattoos
Marks of Heritage2 tattoosEvery True Atlantean starts with these
Sense magicPassive, 30ft
Sense riftsPassive, 1 mile
Vampire protectionHeritage tattooImmune to vampire bite and mind control
PPE2d6x10 + OCC bonusTattoo magic fuel

Clan System

Atlanteans select a clan during chargen. Known public clans: Archerean, Undead Slayer, Stone Master, Nomad, Algor. The Aerihman clan is not listed. It is discovered through RP or wizard narrative.

Sunaj Assassin Path

Aerihman Atlanteans who discover the clan and confirm membership are auto-assigned the Sunaj Assassin OCC. They become MDC creatures, receive the Sunaj Shadow Armor (200 MDC), and their alignment shifts to Anarchist minimum. The clan aerihman command returns "You have not heard of any clan by that name" unless db.knows_aerihman = True.

Tattoo System

Tattoos are permanent magical enhancements granted by wizards using the Tattoo-Gun. Each grants +10 SDC on application. Active tattoos cost PPE and have a duration. Passive tattoos are always on.

Mark of Heritage: Vampire Stake
Protection vs vampire bite and mind control. Passive.
+10 SDC. Right wrist slot.
Mark of Heritage: Flaming Sword
Protection vs vampires. Passive. Aerihman variant has black flames.
+10 SDC. Left wrist slot.
Weapon: Flaming Sword
Summon fire sword. 2d6 MDC. 10 PPE, 5 min per level.
+10 SDC on grant.
Weapon: Lightning Bolt
Ranged attack. 3d6 MDC, 100ft range. 15 PPE per use.
+10 SDC on grant.
Power: Superhuman Strength
PS becomes Supernatural for 2 min per level. 20 PPE.
+10 SDC on grant.
Protection: Globe of Daylight
30ft radius magical daylight. Devastating to vampires. 5 PPE.
+10 SDC on grant.
Power: Invulnerability
+30 MDC for 1 min per level. 30 PPE.
+10 SDC on grant.
Weapon: Bone Sword (Sunaj)
Sunaj-specific. Bone blade. 3d6 MDC. 10 PPE. Requires Sunaj path.
+10 SDC on grant.

Wizard Tools

Two holdable objects give wizards simplified administration commands. When picked up, a CmdSet activates on the wizard. When dropped, it removes.

ToolCommands UnlockedRoom echo
RP-WIZ Skill Toolwizgive <skill> to <player>, wizlist <player>, wizrevoke <skill> from <player>"adjusts something on a device and nods"
Tattoo-Gunwiztat <tattoo_key> to <player>, wiztat list <player>"traces a glowing pattern onto skin"
Wizard Clan Keywizknow aerihman to <player>Silent. No room echo. No confirmation message.
OCC Tool (on skill tool)wizocc <occ_key> to <player>, wizocc list <player>"consults something briefly and nods"

Vehicle System

Vehicles are object hierarchies. The vehicle object sits in a world room. Inside it are internal rooms (hull, cockpit, quarters, cargo bay). When a player types enter <vehicle> they move to the hull room. From the cockpit, pilot <vehicle> adds the VehiclePilotCmdSet which intercepts movement commands and moves the external vehicle object instead of the pilot. All passengers feel the movement but stay in internal rooms.

EX-5 Behemoth Explorer

Northern Gun mobile fortress. 60ft tall, 35 tons, 400 MDC, 90mph. Buried beneath Splynn market. Requires pilot_giant_robot skill. To find it: locate the shovel hidden in Splynn, carry it to the dig site room (db.can_dig = True), type dig with shovel. You drop into the underground chamber. Enter the Behemoth, go to the cockpit, type pilot behemoth. The HUD shows the external room you are navigating.

Rift Travel and Moxim

Moxim is an NPC in Splynn who opens dimensional rifts for universal credits. rift lazlo (500 credits), rift chi_town (750 credits), rift new_lazlo (600 credits). A rift object appears in the room and persists for 60 seconds. Type enter rift to travel. Only one player can feed per rift opening.

05Systems III -- Race/OCC Matrix and Wizard Assignment

Corrections from Earlier Documents

The EvMenu OCC selection node must be removed from chargen. Systems I and Systems II both showed OCC selection during character creation. This is wrong. OCC is assigned post-creation by a wizard. Chargen ends at sirname. Delete node_occ_select and node_confirm_occ from systems/chargen.py.

RCC vs OCC

TypeExamplesOCC AvailableAbility source
RCC onlyGreat Horned Dragon, Mind Melter, Dog Boy, BursterNeverRace grants all abilities, magic, psionics, attack progression
OCC capableHuman, Elf, Dwarf, Kankoran, OrcVia wizard, post-creationRace provides base stats. OCC provides skills, equipment, attacks.
OCC capable + secretTrue AtlanteanStandard OCC + Sunaj (secret)Atlantean RCC abilities always active. OCC layers on top.

Race OCC Eligibility Matrix

RaceOCC AccessRestriction
HumanAll OCCs including all CoalitionNone. Most versatile.
ElfGeneral + Knight + Ley Line WalkerMagic affinity
DwarfGeneral + Knight + Techno-WizardCraftsmen heritage
KankoranGeneral (Wilderness Scout preferred)Wolf-person tracker
Ogre / Orc / TrollGeneral (combat-focused)Some stat caps limit IQ-based skills
True AtlanteanGeneral + Sunaj (secret)Clan affects options
All other * racesGeneral OCCs onlySize or biology may restrict some
All non-* racesNoneRCC only
CS OCCsHuman onlyCoalition is human-supremacist
Knight / Royal KnightHuman, Elf, DwarfPrincipled or Scrupulous alignment
Juicer / Ninja JuicerHumanoid biology requiredNo dragons, vampires, lycanthropes

OCC Eligibility Checks in Code

The is_eligible(character, occ_key) function in systems/occ.py validates: RCC-only races block all OCCs, allowed_races list, requires_clan (blocks Sunaj from normal wizocc), alignment list, min_stats dict. Returns (True, None) or (False, reason_string).

OCC Request Flow

  1. Player types occs to see eligible list
  2. Player types assistance to alert wizards, or tell <wizard> I would like to request <OCC name>
  3. Wizard reviews with wizocc list <player>
  4. Wizard confirms eligibility and types wizocc <occ_key> to <player>
  5. System grants starting skills, equipment, HP die, SDC bonus, attacks per melee at level 1
  6. Player sees: "You have been assigned the OCC. Type pskills to review."
  7. Room sees (anonymous): "A wizard consults something briefly and nods."

The Aerihman Secret Path

Internal mechanic -- must not appear in any help file or public documentation. Aerihman was not listed in RiftsMUD helpfiles. Players discovered it through word of mouth or RP. That secrecy is part of the feature.

The Aerihman clan does not appear in the public clan selection list. The clan aerihman command returns "You have not heard of any clan by that name" unless db.knows_aerihman = True is set on the character. The message is identical to any random unknown clan name. There is no difference in the response.

Wizards grant knowledge via wizknow aerihman to <player>. There is no room echo. No confirmation shown. The player discovers it works only by trying clan aerihman themselves.

Discovery methods: another Sunaj reveals it through RP, a wizard runs a plot involving the Sunaj, or a wizard grants knowledge in a private RP scene with a shadowy encounter in Splynn.

150 Skills Reality Check

Build skill mechanical effects in this priority order. Data-only skills (display in pskills with percentage, no active effect yet) are valid and sustainable at launch.

  1. Weapon Proficiencies -- strike bonus in combat, visible immediately
  2. Hand to Hand types -- attacks per melee and combat bonuses, critical for every fight
  3. Pilot skills -- gate vehicle use, needed for EX-5 quest
  4. Espionage subset -- Tracking, Wilderness Survival, Disguise, Intelligence
  5. Medical -- First Aid restores SDC on day one
  6. Physical -- Athletics, Running, Climbing, Acrobatics modify movement and dodge
  7. Communications -- Radio: Basic enables the radio command
  8. Everything else -- data-only first, effects added over time
06Systems IV -- Combat, Pets, Dev Environment

Verb System (No Damage Numbers)

AetherMUD shows no damage numbers. A verb describes the severity based on what percentage of the target's current damage pool the hit represents.

% of PoolVerb
1 to 5%barely, grazes, glances off
6 to 15%lightly, nicks, clips
16 to 30%solidly, squarely, firmly
31 to 50%heavily, hard, brutally
51 to 70%viciously, savagely, ferociously
71%+devastatingly, critically, with crushing force

Pool selection: MDC armor takes priority. If MDC is 0, use SDC pool. If SDC is 0, use HP. Players track how a fight is going by watching verbs shift from "lightly" toward "viciously."

HTH Archetypes

Humanoid
Human, Elf, Dwarf, Ogre, Orc, Kankoran, True Atlantean, most OCC-capable races
drives a fist into, slams an elbow into, drives a knee into, delivers a kick to, throws a right hook at, smashes a headbutt into
Dragon
Great Horned Dragon, Fire Dragon, Ice Dragon, Thunder Lizard Dragon
slashes at, rakes claws across, lunges and bites at, whips a tail at, drives a horn into, buffets with a massive wing at
Feral / Beast
Werebear, Weretiger, Werewolf, Gargoyle, Gurgoyle, Basilisk
slashes at, snaps at, lunges and bites, tackles, claws at, mauls, rakes across
Undead
Secondary Vampire, Wild Vampire, Bogie, faerie creature types
claws at, reaches for, digs fingers into, lunges and bites, grasps at, tears at, drains at

Add a plant archetype for magic bushes: jabs thorns at, whips a branch at, reaches with thorny vines toward, entangles roots around.

Automatic Combat

Combat is fully automatic once initiated with kill <target>. A MeleeCombatScript attached to the room fires every 3 seconds. Each tick it sorts all combatants by initiative (1d20 + PP bonus), then fires each combatant's full attack count against their target in order. Players can spend their turn on dodge, parry, or special commands but the base attack cycle runs without input. The script stops when fewer than two combatants remain in the room.

Magic Bush Starter Mob

Located in the Camelot garden maze. 20 SDC, 10 HP, 1 attack per melee, 1d4 SD damage, plant type (no blood pool on death), edible corpse, 50 XP, 120 second respawn. The bush is aggressive and attacks any player that enters the maze. It teaches verb-based combat, the corpse system, and the hawk eat command in a consequence-free environment. Combat preview:

-- Camelot Garden Maze --
You drive a fist into a magic bush lightly.
You drive a knee into a magic bush solidly.
A magic bush jabs thorns at you but misses.
You slam an elbow into a magic bush viciously.
The magic bush shudders and collapses into a lifeless heap.
A withered magic bush lies here.

Corpse System

Every mob that dies creates a corpse object in the room. Plant mobs create only a corpse. Non-plant mobs create a corpse and a fresh blood pool. Both decay on timers using Evennia's delay() function. Corpses decay after 5 minutes. Blood pools pass through three stages: fresh (120 sec, drinkable), congealing (180 sec, not drinkable), dark stain (300 sec), then delete.

Blood Pool and Vampire Feeding

Secondary Vampire and Wild Vampire characters have the feed command. It finds a drinkable blood pool in the current room, restores 20 SDC, marks the pool as drained (one feed per pool), and sends an anonymous room message. Blood can also be purchased in Splynn but that route is not obvious by design.

Pet System

Pets are NPCs bound to a player. The hawk is the first implemented pet, purchased in Splynn by trading a falconer's glove. A HawkListenerScript on the hawk intercepts every say event in the room. If the speaker is the owner and the speech starts with "hawk ", the script parses the command and dispatches it. Without the falconry skill, commands fail 30% of the time.

Hawk Verbal Commands

Spoken commandEffect
hawk staySets following = False. Hawk settles in place.
hawk follow meSets following = True. Hawk rises and follows owner on move.
hawk flySets airborne = True. Hawk circles overhead.
hawk eat corpseHawk eats the first edible corpse in room. Corpse is deleted.
hawk attack <target>Hawk initiates combat on target. Uses search_by_display_name() for anonymous system.

Development Environment Overview

MachineRoleSoftware
MAC MacBook NeoTerminal and editor only. No code runs here.Emacs, Godot 4
WIN Windows PC (WSL)MUD development serverWSL2, Fedora Linux (WSL2), Python 3.11, Evennia, sshd on port 22
VPS AlmaLinux VPSPublic test serverAlmaLinux 9, Python 3.11, Evennia, git

MacBook Neo Setup

MAC
Step 1 -- VS Code
Install VS Code and Remote-SSH
Download from code.visualstudio.com. Install the Remote - SSH extension (Microsoft) from the Extensions panel. That is the only extension needed on the Mac side. All Python extensions install on the remote machine over SSH.
MAC
Step 2 -- Godot
Install Godot 4
Download from godotengine.org. The .app bundle needs no installer. Drag to Applications. Entirely separate from the MUD work.
MAC
Step 3 -- emacs
Install emacs
Install via Homebrew: brew install emacs. Or download GUI version from emacsformacosx.com. Use for local quick edits. Everything real happens in VS Code over SSH.

WSL AlmaLinux Setup

WIN
Step 4 -- WSL
Install WSL2 and AlmaLinux
PowerShell (as Administrator)
wsl --install --no-distribution
wsl --install -d AlmaLinux-9
If AlmaLinux-9 is not available, install it from the Microsoft Store directly.
WIN
Step 5 -- Evennia
Install Python 3.11 and Evennia inside AlmaLinux
AlmaLinux (WSL terminal)
sudo dnf update -y
sudo dnf install -y python3.11 python3.11-pip git gcc openssh-server
python3.11 -m venv ~/.venv/aethermud
source ~/.venv/aethermud/bin/activate
pip install evennia
evennia --init aethermud
cd aethermud
evennia migrate
evennia start
Game runs at http://localhost:4001 in your Windows browser. Telnet at localhost:4000.
WIN
Step 6 -- sshd
Enable SSH inside AlmaLinux for Mac access
AlmaLinux (WSL terminal)
sudo ssh-keygen -A
sudo /usr/sbin/sshd
MAC
Step 7 -- SSH config
Configure SSH on Mac and connect VS Code
Find your Windows PC LAN IP via ipconfig in Windows CMD. Add to ~/.ssh/config on Mac:
~/.ssh/config
Host aethermud-dev
  HostName 192.168.1.100
  Port 22
  User yourusername
  IdentityFile ~/.ssh/id_ed25519
Generate key: ssh-keygen -t ed25519. Copy to WSL: ssh-copy-id -p 22 user@192.168.1.100. In VS Code press Cmd+Shift+P, type Remote-SSH: Connect to Host, select aethermud-dev. Install Python and Pylance extensions in the remote window. Open the aethermud folder. Full IntelliSense and integrated terminal now run inside AlmaLinux.

Daily Development Workflow

  1. SSH into zeus-wsl from Mac terminal: ssh zeus-wsl
  2. In the integrated terminal: source ~/.venv/aethermud/bin/activate
  3. If Evennia is not running: evennia start
  4. Edit code in VS Code and save
  5. In a MUD client or browser, type reload in-game
  6. Test the change
  7. When ready for VPS: git add . && git commit -m "describe change" && git push vps main
  8. SSH into VPS and run evennia reload
Keep two terminal tabs open: one for evennia commands, one running tail -f server/logs/server.log. Errors from reload show file name and line number within one second.
07Combat Resolution, Spell System & Psionics

Why the Current System is Incomplete

The combat system in Systems IV rolls 1d20 + PP bonus vs a static AR and calls it a hit or miss. That is not Palladium combat. In Palladium, the defender actively responds to each incoming strike by choosing to parry or dodge. Both rolls happen in real time and each choice has a cost. This three-way interaction (attacker strikes, defender parries or dodges) is the mechanical heart of every fight and determines whether combat feels like Rifts or like a generic MUD.

The Melee Round

A melee round is 15 seconds of in-game time. Each character gets a number of attacks per melee round based on their race and OCC. The MeleeCombatScript fires every 3 seconds. A character with 4 attacks per melee fires one attack on each tick (4 attacks over 12 seconds with a 3 second buffer). Characters with 6 attacks fire attacks more frequently.

Attacks per melee in the tick script: Do not fire all attacks at once on each 3-second tick. Spread them across the round. A character with 4 attacks fires on ticks 1, 4, 7, and 10. A character with 8 attacks fires on ticks 1, 2, 4, 5, 7, 8, 10, 11. This creates the fast-combat feel where a Juicer's attacks land noticeably faster than a starting character's.

Initiative

Initiative is rolled once at the start of combat and determines who fires their first attack in the round. After the first attack is resolved, attacks alternate in initiative order until all attacks for that round are spent. High initiative means you land your attacks before your opponent, which can end a fight early.

systems/combat.py -- initiative
import random

def roll_initiative(character) -> int:
    """1d20 + initiative bonus from HTH type and level."""
    base  = random.randint(1, 20)
    bonus = int(getattr(character.db, "initiative_bonus", 0) or 0)
    return base + bonus

The Strike Roll

When a character attacks, they roll 1d20 and add their total strike bonus. A natural 1 is always a miss. A natural 20 is a critical hit.

ComponentSourceNotes
Base roll1d20Natural 1 = automatic miss. Natural 20 = critical.
HTH strike bonusHTH type + levelSee HTH tables below
WP strike bonusWeapon Proficiency + levelOnly applies when using that weapon type
PP strike bonusPP stat+1 per 2 points of PP above 16
Ambush/backstabSpecific skills+4 to strike on first attack from hiding

Defense: Parry vs Dodge

This is the most important mechanical decision in Palladium combat. The defender chooses parry or dodge after the strike roll is declared.

Attacker Strikes
Rolls 1d20 + strike bonus. Result announced to room with verb but no number.
Defender Chooses
Parry (free, uses a weapon or HTH) or Dodge (costs 1 attack). If no attacks remain, they take the hit.
Defense Roll
Rolls 1d20 + parry or dodge bonus. Must beat the attacker's strike number.
Defense Fails
Strike lands. Damage rolled and applied through MDC/SDC/HP routing. Verb selected from damage tiers.
Defense Succeeds
Strike deflected. Room sees anonymous miss message.

Parry

Parry is a free defense -- it does not cost an attack. The defender rolls 1d20 + parry bonus and must exceed the attacker's strike number. Unarmed parry against an armed opponent takes an additional -2 penalty unless the defender has a HTH type that ignores this.

Dodge

Dodge costs one of the defender's remaining attacks for that round. More reliable against ranged attacks and area effects where parry is not possible. Auto-dodge allows dodging without spending an attack but uses half the normal dodge bonus.

When dodge is the only option: Against ranged weapon fire, explosions, and area effect spells, parry is not possible. If no attacks remain, the character takes the hit automatically. This makes attack economy matter strategically.

Hit Resolution

systems/combat.py -- full attack resolution
def resolve_attack(attacker, defender, weapon=None):
    from systems.combat_archetypes import get_attack_verb
    from systems.messaging import msg_room

    raw_strike   = random.randint(1, 20)
    is_critical  = raw_strike == 20
    is_fumble    = raw_strike == 1
    strike_total = raw_strike + get_total_strike_bonus(attacker, weapon)

    if is_fumble:
        msg_room(attacker.location, attacker,
            "{Actor} swings wildly and completely misses {target}.",
            target=defender)
        return

    defense_result = resolve_defense(defender, strike_total, weapon)

    if defense_result["success"] and not is_critical:
        # Critical hits cannot be parried or dodged in Palladium
        msg_room(attacker.location, attacker,
            f"{{Actor}} {get_attack_verb(attacker,weapon)} {{target}} but {defense_result['verb']}.",
            target=defender)
        return

    if weapon:
        dmg_dice, dmg_type = weapon.db.damage_dice, weapon.db.damage_type
    else:
        nat      = get_natural_attack(attacker)
        dmg_dice, dmg_type = nat["damage"], nat["type"]

    damage = roll_dice(dmg_dice) + get_ps_damage_bonus(attacker)
    if is_critical: damage *= 2
    apply_damage(attacker, defender, damage, dmg_type)
    prefix = "With a critical strike, " if is_critical else ""
    msg_room(attacker.location, attacker,
        f"{prefix}{{Actor}} {get_attack_verb(attacker,weapon)} {{target}} {get_damage_verb(damage,defender)}.",
        target=defender)


def resolve_defense(defender, strike_total: int, incoming_weapon=None) -> dict:
    mode    = getattr(defender.db, "defense_mode", "parry")
    attacks = int(getattr(defender.db, "attacks_remaining", 0) or 0)
    is_ranged = incoming_weapon and incoming_weapon.db.weapon_type in ["ranged","burst","heavy"]
    if is_ranged: mode = "dodge"

    if mode == "parry":
        bonus = int(getattr(defender.db, "parry_bonus", 0) or 0)
        roll  = random.randint(1, 20) + bonus
        return {"success": roll > strike_total, "verb": "parries"}
    if mode == "dodge" and attacks > 0:
        defender.db.attacks_remaining -= 1
        bonus = int(getattr(defender.db, "dodge_bonus", 0) or 0)
        roll  = random.randint(1, 20) + bonus
        return {"success": roll > strike_total, "verb": "dodges"}
    return {"success": False, "verb": "is caught off guard"}

Critical Hits and Fumbles

RollNameEffect
Natural 20Critical hitDouble damage. Cannot be parried or dodged.
Natural 20 (HTH Martial Arts/Assassin, high level)KnockoutTarget saves vs PE or is unconscious for 1d4 melee rounds.
Natural 1FumbleAutomatic miss. 50% chance to drop weapon.

Power Punch

Costs two attacks instead of one but doubles the damage dice before the PS bonus is added. If a power punch rolls a natural 20, treat it as a critical (double final damage, not double-double). Declared before rolling via powerattack command, which sets db.next_attack_powered = True.

Pulling Punches

Any MDC-capable attacker can declare they are pulling their punch. A pulled punch deals SDC damage instead of MDC regardless of weapon type. Toggle with pull command -- sets db.pulling_punches = True. In apply_damage() if attacker.db.pulling_punches and dmg_type == "md", treat as "sd" for that strike.

HTH Types and Level Bonus Tables

HTH type is assigned at OCC selection. Bonuses are stored in db.strike_bonus, db.parry_bonus, db.dodge_bonus, and db.initiative_bonus. Recalculated on level-up via apply_hth_levelup() in systems/hth.py.

HTH: Basic
Vagabond, City Rat, most civilian types
L1: +1 parry, +1 dodge
L2: +1 strike
L3: +1 parry, +1 dodge
L4: +1 attack per melee
L5: +1 strike, +1 parry
L6: +1 initiative
L7: +1 attack per melee
L8: +1 parry, +1 dodge
L9: +1 strike
L10: +1 attack per melee
L11: +1 parry, +1 dodge
L12: +1 attack per melee
L13: +1 strike, +1 parry
L14: +1 initiative
L15: +1 attack per melee
HTH: Expert
Coalition Soldier, Headhunter, most military types
L1: +2 parry, +2 dodge, +2 strike
L2: +1 attack, +1 damage
L3: +1 parry, +1 dodge, Knockout on 18-20
L4: +1 attack, +1 damage
L5: +1 strike, +1 initiative
L6: +1 attack per melee
L7: +1 parry, +1 dodge, +1 damage
L8: +1 attack per melee
L9: +2 strike, +1 initiative
L10: +1 attack, +1 damage
L11: +1 parry, +1 dodge
L12: +1 attack per melee
L13: +2 strike
L14: +1 attack, +1 initiative
L15: +1 parry, +1 dodge, +1 damage
HTH: Martial Arts
Cyber-Knight, some special forces types
L1: +3 parry, +3 dodge, +2 strike, +1 initiative
L2: +1 attack, +1 damage, +1 strike
L3: +1 parry, +1 dodge, Knockout on 18-20
L4: +1 attack, +2 damage
L5: +1 strike, +1 initiative, +1 parry
L6: +1 attack, Knockout on 16-20
L7: +1 parry, +1 dodge, +2 damage
L8: +1 attack, +1 strike
L9: +2 parry, +2 dodge, +1 initiative
L10: +1 attack, +2 damage
L11: +1 parry, +1 dodge, +1 strike
L12: +1 attack per melee
L13: +2 strike, +1 initiative
L14: +1 attack, +2 damage
L15: +1 parry, +1 dodge, Knockout on 14-20
HTH: Assassin
Sunaj Assassin, Master Assassin, Ninja Juicer
L1: +4 strike, +4 parry, +4 dodge, +2 damage
L2: +1 attack, +1 initiative
L3: +1 strike, +2 damage, Backstab on first attack from hiding
L4: +1 attack, +1 parry, +1 dodge
L5: +1 strike, +1 initiative
L6: +1 attack, +3 damage
L7: +1 parry, +1 dodge, Knockout on 18-20
L8: +1 attack, +1 strike
L9: +1 parry, +1 dodge, +2 damage, +1 initiative
L10: +1 attack per melee
L11: +2 strike, +3 damage
L12: +1 attack, Knockout on 16-20
L13: +1 parry, +1 dodge, +1 initiative
L14: +1 attack, +2 damage
L15: +2 strike, +2 parry, Knockout on 14-20
systems/hth.py -- level-up function
HTH_TABLES = {
    "hand_to_hand_basic": [
        {"parry":1,"dodge":1},   # level 1
        {"strike":1},           # level 2
        {"parry":1,"dodge":1},   # level 3
        {"attacks":1},          # level 4 ... 11 more
    ],
}

def apply_hth_levelup(character, new_level: int):
    """Apply HTH bonuses for new_level. Called from xp.py on level-up."""
    hth_key = character.db.hth_type
    if not hth_key or hth_key not in HTH_TABLES: return
    table = HTH_TABLES[hth_key]
    gains = table[min(new_level - 1, len(table) - 1)]
    for stat, amount in gains.items():
        attr = f"{stat}_bonus" if stat != "attacks" else "attacks_per_melee"
        current = int(getattr(character.db, attr, 0) or 0)
        setattr(character.db, attr, current + amount)

Weapon Proficiency Bonuses

WP bonuses only apply when using the specific weapon type the proficiency covers. They stack on top of HTH bonuses. get_total_strike_bonus() must check the equipped weapon's db.weapon_category against the character's WP skills each time it is called.

WP SkillLevel 1Level 4Level 8Level 12
WP Energy Pistol+1 strike+2 strike+3 strike+4 strike
WP Energy Rifle+1 strike+2 strike+4 strike+5 strike
WP Heavy MD+1 strike+2 strike+3 strike+4 strike
WP Knife+1 strike, +1 parry+2 strike, +1 parry+3 strike, +2 parry+4 strike, +3 parry
WP Blunt+1 strike, +1 parry+2 strike, +2 parry+3 strike, +2 parry+4 strike, +3 parry
WP Sword+1 strike, +1 parry+2 strike, +2 parry+3 strike, +3 parry+4 strike, +4 parry

Physical Strength Damage Bonuses

PS TypePS ValueBonus
Normal16+1 damage
Normal17+2 damage
Normal18+3 damage
Normal19-20+4 damage
Augmented (Atlantean)any+6 damage regardless of PS value
Supernatural (Dragon)18+12 damage on melee
Supernatural (Dragon)20+14 damage on melee
Supernatural (Dragon)22++16 damage on melee
Supernatural PS bonus does not apply to ranged weapons or natural fire breath. It only adds to direct physical strikes: claws, bite, tail, and melee weapons. A dragon's fire breath is flat 2d6 MDC regardless of PS.

Horror Factor

When a character encounters a creature with an HF rating, they must make a ME-based saving throw or be affected. This check happens once at the start of combat. Failure means they freeze (attacks_remaining = 0) for one melee round.

HF RatingBase save targetExample creature
6-8Roll 1d20, need 6+Gargoyle, Gurgoyle
9-11Roll 1d20, need 8+Brodkil Berserker, most demons
12-14Roll 1d20, need 10+Great Horned Dragon (hatchling), Hell Demon
15-17Roll 1d20, need 12+Dragon (adult), Demon Lord
18+Roll 1d20, need 14+Ancient dragon, greater supernatural
systems/combat.py -- horror_factor_check()
def horror_factor_check(viewer, creature) -> bool:
    hf    = int(getattr(creature.db, "horror_factor", 0) or 0)
    if hf < 6: return True
    me    = int(getattr(viewer.db, "me", 10) or 10)
    bonus = max(0, (me - 10) // 2)
    target = max(2, (hf - 6) // 3 + 6)
    roll   = random.randint(1, 20) + bonus
    if roll < target:
        viewer.db.attacks_remaining = 0
        viewer.msg(f"|rThe sight of {creature.get_display_name(viewer)} fills you with dread. You freeze!|n")
        return False
    return True

Saving Throws

Five separate categories, each using a different primary stat and a different base target. A successful save negates or reduces the effect.

vs Magic
1d20 + PE bonus, need 12+
PE bonus: +1 per 2 points above 14.
vs Psionics
1d20 + ME bonus, need 15+
ME bonus: +1 per 2 points above 14.
vs Poison (Lethal)
1d20 + PE bonus, need 14+
Failure means full poison damage over time.
vs Poison (Non-Lethal)
1d20 + PE bonus, need 16+
Sedatives, paralytic agents, knockout drugs.
vs Horror Factor
1d20 + ME bonus, scales with HF
See Horror Factor section above.
vs Insanity
1d20 + ME bonus, need 12+
Resist madness from psionic or magic attacks.
systems/saves.py
SAVE_TARGETS = {
    "magic":           {"stat":"pe","target":12,"above":14},
    "psionics":        {"stat":"me","target":15,"above":14},
    "poison_lethal":   {"stat":"pe","target":14,"above":14},
    "poison_nonlethal":{"stat":"pe","target":16,"above":14},
    "insanity":        {"stat":"me","target":12,"above":14},
}

def saving_throw(character, save_type: str, difficulty_mod: int = 0) -> bool:
    cfg      = SAVE_TARGETS.get(save_type)
    if not cfg: return True
    stat_val = int(getattr(character.db, cfg["stat"], 10) or 10)
    bonus    = max(0, (stat_val - cfg["above"]) // 2)
    bonus   += int(getattr(character.db, f"save_bonus_{save_type}", 0) or 0)
    roll     = random.randint(1, 20) + bonus
    return roll >= (cfg["target"] + difficulty_mod)

Coma and Death

HP StateConditionRule
HP above 1NormalFull combat capability
HP 0 to -9ComaUnconscious. Each melee round must save vs coma (PE-based, need 12+) or lose 1 more HP. Body Fixer or Paramedic skill can stabilize.
HP -10 or lowerDeadCharacter dies. at_death() is called.
systems/combat.py -- coma_tick(), called by MeleeCombatScript
def coma_tick(character):
    """Called each melee round for characters in coma state (HP 0 to -9)."""
    hp = int(character.db.hp or 0)
    if hp > 0 or hp <= -10: return
    pe    = int(getattr(character.db, "pe", 10) or 10)
    bonus = max(0, (pe - 14) // 2)
    if random.randint(1,20) + bonus < 12:
        character.db.hp -= 1
        if character.db.hp <= -10:
            character.at_death()
        else:
            character.msg("|rYou slip deeper into unconsciousness.|n")
    else:
        character.msg("|xYou cling to life, barely conscious.|n")

Healing and Recovery

PoolNatural rateWith restWith skill/treatment
SDC1d4 per hour of activity2d6 per hour restFirst Aid: +1d6. Paramedic: +2d6. Body Fixer: full OCC tables.
HP1 per hour of activity2 per hour restFirst Aid: stabilizes coma. Paramedic: +1d4. Body Fixer: +2d6 per treatment.
MDC (non-creature, armor)Does not heal naturallyDoes not heal naturallyMechanics skill or Armorer.
MDC (dragon)1d4x10 per 5 min2d6x10 per hour restBio-regeneration is racial, not supplemented by skills.
PPE5 per hour10 per hour sleepOn ley line: 5/min. At nexus: 10/min.
ISP2 per hour6 per hour sleepMeditation: 6/hour (replaces sleep rate).

Implement recovery as a repeating script attached to the character. It fires every in-game minute (60 real seconds) and adds the appropriate amount based on db.resting and db.on_ley_line flags.

Combat Code -- Build Order

  1. systems/saves.py -- saving throw function. No dependencies.
  2. systems/hth.py -- HTH tables and level-up function.
  3. Update systems/combat.py -- replace resolve_attack() with full version above.
  4. Add resolve_defense() to combat.py.
  5. Add get_total_strike_bonus() summing HTH + WP + PP bonuses.
  6. Add horror_factor_check() called from initiate().
  7. Add coma_tick() called from MeleeCombatScript.
  8. Add systems/healing.py as a script-driven recovery system.
07bSpell System

Overview

Palladium magic uses PPE as fuel. Every spell has a PPE cost, a casting time, a duration, a range, and a saving throw target. Spells are learned by level -- a Ley Line Walker does not have access to level 5 spells until they reach character level 5. The Great Horned Dragon learns from the same LLW list.

The in-game command is cast <spell name> or cast <spell name> at <target>. The system checks PPE, deducts the cost, applies the effect, and starts a duration timer.

PPE System

characters.py -- PPE fields in at_object_creation
self.db.ppe            = 0
self.db.ppe_max        = 0
self.db.is_practitioner = False   # True for LLW, dragons, gifted gypsy
self.db.spell_list     = []        # list of learned spell keys
self.db.active_spells  = []        # list of {key, expires_at, effect_data}
self.db.on_ley_line    = False     # set by room flag
self.db.at_nexus       = False     # set by room flag

PPE Regeneration

ConditionPPE per hourNotes
Normal activity5Baseline for all practitioners
Rest (awake)10db.resting = True
Sleep15db.sleeping = True
On a ley line5 per minutedb.on_ley_line = True, set by room
At a nexus point10 per minutedb.at_nexus = True, set by room
Meditation10 per minuteSpecific skill, locks from combat

Ley Lines and Nexus Points

Rooms on a ley line have db.ley_line = True. Where two ley lines cross, db.nexus = True. These flags are set by builders in batch files.

EffectOn ley lineAt nexus
PPE regeneration5 per minute10 per minute
Spell PPE costHalf normalQuarter normal
Spell rangeDoubleTriple
Spell durationDoubleTriple
PPE sensingWithin 1 mileWithin 10 miles
systems/spells.py -- ley line cost modifier
def get_ppe_cost(caster, spell_key: str) -> int:
    base_cost = SPELL_DATA[spell_key]["ppe"]
    if getattr(caster.db, "at_nexus", False):
        return max(1, base_cost // 4)
    if getattr(caster.db, "on_ley_line", False):
        return max(1, base_cost // 2)
    return base_cost

Casting a Spell

commands/cmd_magic.py -- CmdCast
class CmdCast(Command):
    """Cast a magic spell. Usage: cast <spell> [at <target>]"""
    key   = "cast"
    locks = "cmd:attr(is_practitioner)"

    def func(self):
        from systems.spells import SPELL_DATA, get_ppe_cost, apply_spell
        from systems.messaging import msg_room
        ch   = self.caller
        args = self.args.strip().lower()
        target, spell_str = None, args
        if " at " in args:
            spell_str, tstr = args.split(" at ",1)
            target = ch.search(tstr.strip(), location=ch.location)
        spell_key = next((k for k,d in SPELL_DATA.items()
                          if d["name"].lower().startswith(spell_str)), None)
        if not spell_key or spell_key not in (ch.db.spell_list or []):
            ch.msg("You do not know that spell."); return
        if SPELL_DATA[spell_key]["level"] > int(ch.db.level):
            ch.msg("You are not yet skilled enough to cast that spell."); return
        cost = get_ppe_cost(ch, spell_key)
        if int(ch.db.ppe) < cost:
            ch.msg(f"You need {cost} PPE. You have {ch.db.ppe}."); return
        if int(getattr(ch.db,"attacks_remaining",0) or 0) < 1:
            ch.msg("You do not have an action available to cast."); return
        ch.db.ppe -= cost
        ch.db.attacks_remaining -= 1
        msg_room(ch.location, ch, "{Actor} gestures and speaks a word of power.")
        apply_spell(ch, spell_key, target)

Duration Tracking

Active spells are stored in db.active_spells as a list of dicts. Durations are calculated in real seconds at cast time. Evennia's delay() fires the expiry function automatically -- no polling script needed.

systems/spells.py -- apply_spell and expire_spell
from evennia.utils import delay
import time

def apply_spell(caster, spell_key: str, target=None):
    data     = SPELL_DATA[spell_key]
    target   = target or caster
    dur_mult = 3 if getattr(caster.db,"at_nexus",False) else \
               2 if getattr(caster.db,"on_ley_line",False) else 1
    level    = int(caster.db.level)
    dur_s    = data.get("duration_fn", lambda l: 60)(level) * dur_mult
    data["effect_fn"](caster, target, level)
    if dur_s > 0:
        active = target.db.active_spells or []
        active.append({"key":spell_key, "expires_at":time.time()+dur_s, "caster_id":caster.id})
        target.db.active_spells = active
        delay(dur_s, expire_spell, target, spell_key)

def expire_spell(character, spell_key: str):
    """Called by delay() when a spell's duration ends."""
    character.db.active_spells = [s for s in
        (character.db.active_spells or []) if s["key"] != spell_key]
    expire_fn = SPELL_DATA.get(spell_key,{}).get("expire_fn")
    if expire_fn: expire_fn(character)
    character.msg(f"|xThe effect of {SPELL_DATA[spell_key]['name']} fades.|n")

Saving Throw vs Magic

Spells targeting an unwilling creature must be resisted. After casting, the target rolls a save vs magic. Success negates or reduces the effect. Uses saving_throw(target, "magic") from saves.py.

Spell List -- Levels 1 to 3

Ley Line Walker spells, also available to Great Horned Dragons. Spell level equals minimum character level required to cast.

Globe of Daylight
Level 1 -- 2 PPE -- Duration: 3 min/level -- Range: 30ft radius
Sphere of magical daylight. Vampires take 2d6 SDC per melee round inside it. Illuminates dark rooms.
See the Invisible
Level 1 -- 4 PPE -- Duration: 2 min/level -- Range: self
See invisible and astral entities within 120ft. Sets db.see_invisible = True for duration.
Sense Magic
Level 1 -- 2 PPE -- Duration: 2 min/level -- Range: 120ft
Detects magic items, enchantments, and spell effects in the area. Names type but not exact nature.
Sense Evil
Level 1 -- 2 PPE -- Duration: 2 min/level -- Range: 60ft
Detects evil alignments and supernatural evil entities. Knows evil is present and its direction.
Cloud of Slumber
Level 1 -- 4 PPE -- Duration: 6 min -- Range: 60ft -- Save: vs magic
30ft radius sleep cloud. Targets who fail save fall asleep. Woken by damage or loud noise.
Befuddle
Level 2 -- 6 PPE -- Duration: 4 min/level -- Range: 60ft -- Save: vs magic
Target becomes confused. -3 to all rolls. Cannot use skills above 40% effectively.
Levitation
Level 2 -- 5 PPE -- Duration: 3 min/level -- Range: self or 60ft
Target rises at 6ft per melee. Cannot move horizontally without wind. Useful for elevated exits.
Turn Dead
Level 2 -- 6 PPE -- Duration: instant -- Range: 60ft
Affects up to 1d4 undead per caster level. Failure: flee in terror for 1d6 melee rounds.
Armor of Ithan
Level 3 -- 10 PPE -- Duration: 1 min/level -- Range: self or touch
Magical force field of 10 MDC per caster level. Absorbs MDC damage. Does not stack with physical armor.
Energy Bolt
Level 3 -- 5 PPE -- Duration: instant -- Range: 150ft
2d6 SDC or 1d6 MDC depending on target. Requires a strike roll to hit.
Fly
Level 3 -- 15 PPE -- Duration: 10 min/level -- Range: self
Flight at 50mph. Sets db.can_fly = True and db.fly_speed = 50. Allows aerial exits tagged "flying".
Invisibility: Simple
Level 3 -- 6 PPE -- Duration: 3 min/level -- Range: self
Caster invisible. Broken by any attack. Sets db.invisible = True.
Magic Net
Level 3 -- 7 PPE -- Duration: 1 min/level -- Range: 60ft -- Save: vs magic
Entangles target. Failure: immobilized, loses all attacks. MDC creatures break free on PS roll 20+.

Spell List -- Levels 4 to 6

Blind
Level 4 -- 6 PPE -- Duration: 4 min/level -- Range: 60ft -- Save: vs magic
-10 to strike, parry, and dodge. Cannot read or see exits. -6 to all combat rolls.
Fireball
Level 4 -- 10 PPE -- Duration: instant -- Range: 150ft -- Save: vs magic (half dmg)
1d6 MDC per caster level in 20ft radius. Save = half damage.
Impervious to Fire
Level 4 -- 5 PPE -- Duration: 5 min/level -- Range: self or touch
Immune to normal fire. Half damage from magical and MDC fire. Sets db.impervious_fire = True.
Telekinesis
Level 4 -- 8 PPE -- Duration: 2 min/level -- Range: 60ft
Move objects up to 40lbs. Attack deals SDC equal to 1d4 per 10lbs.
Electro-Bolt
Level 5 -- 8 PPE -- Duration: instant -- Range: 200ft
2d6 MDC direct hit. Arcs to adjacent targets within 5ft for 1d6 MDC. Extra 1d6 in water/metal armor.
Horrific Illusion
Level 5 -- 10 PPE -- Duration: 5 min -- Range: 60ft -- Save: vs magic
Targets who fail treat it as Horror Factor 14 encounter.
Sanctuary
Level 5 -- 15 PPE -- Duration: 5 min/level -- Range: 20ft radius
Evil supernatural entities (demons, vampires) cannot enter without a save vs magic.
Call Lightning
Level 6 -- 15 PPE -- Duration: instant -- Range: 300ft
2d6 MDC per 3 caster levels. Outdoors or large indoor spaces only.
Frequency Jamming
Level 6 -- 15 PPE -- Duration: 5 min/level -- Range: 300ft radius
Jams all radio communications. Coalition commands, robot sensors, electronic systems disrupted.
Circle of Rain
Level 6 -- 20 PPE -- Duration: 10 min/level -- Range: 100ft radius
Torrential downpour. Vampires take 2d6 SDC per melee (running water). +1d6 to electrical attacks.

Spell List -- Level 7 and Higher

Negate Magic
Level 7 -- 20 PPE -- Duration: instant -- Range: 60ft
Cancels one active spell, or creates anti-magic field in 30ft radius preventing new casting for 1d4 melee rounds. Does not affect psionics.
Teleport: Lesser
Level 7 -- 30 PPE -- Duration: instant -- Range: self
Teleport to any visited location within 100 miles. 4% misfire chance per level below 12. Free at nexus.
Wind Rush
Level 7 -- 20 PPE -- Duration: instant -- Range: 60ft
Knocks targets off feet (lose 2 attacks, -2 rolls next round) unless save. Disperses Cloud of Slumber.
Rift: Temporary
Level 10 -- 200 PPE -- Duration: 2 min -- Range: caster location
Opens a dimensional rift. Requires ley line or nexus. Up to 6 people can pass through.

Dragon Magic

Great Horned Dragons do not need to select spells at chargen. They automatically know all LLW spells up to their current character level -- magic is intuitive to their nature. Teleportation (up to 5 miles, once per 2 melee rounds) is racial and costs zero PPE.

Dragon PPE vs human PPE: A GHD with PPE 95 and Fireball at 10 PPE can cast it 9 times before resting. Their massive PPE pool means they cast far more freely than a starting Ley Line Walker with 30 PPE. Balance by making high-level spells cost proportionally more.
07cPsionics

Overview

Psionics use ISP instead of PPE. The two pools are completely separate and never interchangeable. The four categories are Sensitive, Physical, Healer, and Super. Minor psionics access Sensitive only. Major psionics access Sensitive, Physical, and Healer. Master psionics (Mind Melter) access all four including Super.

The Great Horned Dragon is a Major Psionic selecting 8 powers at chargen. The Dog Boy is a Minor Psionic with automatic Sensitive powers. The Mind Melter is the only playable RCC with Super powers.

ISP System

Starting ISPRegenerationNotes
Major Psionic: 3d6x102/hr, 6/hr sleepGreat Horned Dragon, Burster
Master Psionic: 4d6x103/hr, 8/hr sleepMind Melter only
Minor Psionic: 2d6x102/hr, 4/hr sleepDog Boy, some human OCCs

Meditation doubles ISP regen while active. The meditate command sets db.meditating = True and locks combat actions. Any attack on the meditating character breaks meditation.

The Four Psionic Categories

CategoryFocusWho can access
SensitivePerception, detection, mind reading, remote sensingAll psionic characters
PhysicalPhysical enhancement, telekinesis, bio-manipulationMajor and Master
HealerBio-regeneration, curing disease, healing othersMajor and Master
SuperPyrokinesis, mind bleeder, psi-sword, advanced TKMaster only (Mind Melter)

Core Power List

Sense Evil [S]
Sensitive -- 2 ISP -- Duration: 2 min/level
Detect evil presence within 140ft. Direction and intensity known.
Empathy [S]
Sensitive -- 4 ISP -- Duration: 2 min/level -- Range: 100ft -- Save: vs psionics
Sense emotions of others. Can project emotions onto a target. Save negates projection.
Telepathy [S]
Sensitive -- 4 ISP -- Duration: 2 min -- Range: 60ft -- Save: vs psionics
Surface thought reading. Save negates. Target unaware unless roll was a natural 1.
See Aura [S]
Sensitive -- 6 ISP -- Duration: instant
Reveals: general alignment, psionic/magic ability, approximate level range, whether currently lying.
Mind Block [S]
Sensitive -- 4 ISP -- Duration: 10 min/level -- Range: self
+4 to all saves vs psionics. Detects telepathic intrusion attempts.
Object Read [S]
Sensitive -- 6 ISP -- Duration: 2 min -- Range: touch
Impressions of object history: strong emotional events, last holder, approximate age, recent violence.
Telekinesis [P]
Physical -- 8 ISP/min -- Duration: concentration -- Range: 60ft
Move objects up to 4lbs per ISP. Attack: 1d6 SDC per 10lbs. Cannot affect MDC objects or Supernatural PS.
Bio-Manipulation [P]
Physical -- 10 ISP -- Duration: varies -- Range: 60ft -- Save: vs psionics
Inflict 4d6 SDC as pain, temporary blindness, nausea (-5 all rolls), or 50% slow. One effect per use.
Psychic Surgery [H]
Healer -- 14 ISP -- Duration: permanent -- Range: touch
Heal 4d6 SDC or 2d6 HP. Cannot heal MDC. Requires willing, still target. Cannot be used on self.
Resist Fatigue [P]
Physical -- 4 ISP -- Duration: 2 hours
No fatigue for duration. +4 to PE-based saves. Full capacity without rest.
Psi-Sword [Super]
Super -- 30 ISP -- Duration: 2 min/level -- Range: self
Mind Melter only. 4d6 MDC blade from forearm. Vanishes if knocked unconscious.
Mind Bleeder [Super]
Super -- 25 ISP -- Duration: special -- Range: 60ft -- Save: vs psionics
Mind Melter only. Drain 2d6 ISP from another psionic. Stolen ISP dissipates in 1 hour. Save negates.

Psionics Code Implementation

systems/psionics.py -- mirrors spells.py structure
POWER_DATA = {
    "telepathy": {
        "name":        "Telepathy",
        "category":    "sensitive",
        "isp":         4,
        "save_type":   "psionics",
        "duration_fn": lambda level: 120,
        "effect_fn":   _telepathy_effect,
    },
    "psi_sword": {
        "name":        "Psi-Sword",
        "category":    "super",
        "isp":         30,
        "requires_psi_level": "master",
        "duration_fn": lambda level: level * 120,
        "effect_fn":   _psi_sword_effect,
        "expire_fn":   _psi_sword_expire,
    },
    # ... all powers follow the same pattern
}

# Command: use <power name> [at <target>]
# Mirrors CmdCast: checks ISP instead of PPE
# Checks db.psi_level against power["requires_psi_level"] for Super powers
# Uses saving_throw(target, "psionics") for resisted powers
Build spell and psionic systems in parallel. They share the same pattern: resource pool, cost check, effect function, duration tracking via delay(), expiry function. Write the spell system first. Then copy the pattern for psionics with ISP instead of PPE and use instead of cast.