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.
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.
- Log in as your superuser account (user
#1, created duringevennia migrate). - You begin in Limbo (#2), the default root room.
- Step down with
quellto test without bypassing all locks. Your character retains Developer permissions. Restore withunquell. - Run
helpto list all commands. Usehelp <command>for details.
Basic Workflow
Almost every build task follows the same four-step pattern: look, create, describe, connect.
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
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
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
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 north = traverse:tag(cs_soldier)
lock vibro-blade = get:id(#23)
lock iron chest = get:perm(Builder)
lock relic = view:all();get:perm(Admin)
| Type | Controls | Common function |
|---|---|---|
| traverse | Who can pass through exit | tag(tagname), perm(Level) |
| get | Who can pick up object | id(#dbref), perm(Level) |
| view | Who can see object in room | all(), none() |
| delete | Who can destroy the object | perm(Admin) |
| puppet | Who can possess character | id(#dbref) |
Scripts and Timers
script coalition grunt = scripts.patrol.PatrolScript
script coalition grunt
script/stop coalition grunt = patrol
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
batchcommand world.rifts_hellpit batchcommand/interactive world.rifts_hellpit
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.
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
| Attribute | In-Game Set | Python Access | Notes |
|---|---|---|---|
| race | set npc/race = brodkil | npc.db.race | Species string |
| level | set npc/level = 6 | npc.db.level | Integer 1-20+ |
| hit_dice | set npc/hit_dice = 4d8 | npc.db.hit_dice | Dice string |
| aggressive | set npc/aggressive = 1 | npc.db.aggressive | 1 = attacks on sight |
| allegiance | set npc/allegiance = coalition | npc.db.allegiance | Faction string |
| damage_dice | set obj/damage_dice = 3d6 | obj.db.damage_dice | Dice string |
| damage_type | set obj/damage_type = md | obj.db.damage_type | md, sd, fire, plasma |
| mdc | set obj/mdc = 100 | obj.db.mdc | Mega-Damage Capacity |
| zone | set room/zone = rifts_wasteland | room.db.zone | Area identifier |
| is_dark | set room/is_dark = 1 | room.db.is_dark | Light/dark flag |
set stores strings. Cast in Python as needed: int(npc.db.level). Set complex types directly from Python: obj.db.skills = {}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).
{
"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
Typeclasses
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
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 type | Target has MDC armor | Target has no MDC armor |
|---|---|---|
| MDC weapon | Damages armor MDC pool | Instantly fatal to biological targets |
| SDC weapon | Absorbed 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.
- Race selection via EvMenu
- Stat rolling (3d6 with re-roll on 16+)
- sirname command to set true name
- Atlanteans: clan selection (Aerihman not listed)
- Enter world with racial baseline, no OCC yet
- Later: OCC request to wizard via tell or assistance command
OCC System
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).
Design Philosophy
Three systems define what made RiftsMUD feel unique:
- Anonymous identity. You never see another player's name until they introduce themselves. You see race and OCC.
- The who list as population gauge. You cannot surveil the player base. You see population tier and which wizards are watching.
- LPC command syntax.
look at <obj>notlook <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.
Introduce / Remember / sirname / greet
| Command | Syntax | Effect |
|---|---|---|
| introduce | introduce or introduce to <target> | Writes true_name into each listener's known_names dict |
| remember | remember <target> as <nickname> | Overwrites your stored name for that character |
| forget | forget <target> | Removes entry from known_names |
| sirname | sirname <name> | Sets your own true_name (one-time or wizard-gated) |
| greet | greet <target> | Polite acknowledgment, does not reveal your name |
Who Command
| Player count | Display |
|---|---|
| 0 | The world is empty. You are alone. |
| 1-4 | Population: Low |
| 5-14 | Population: Moderate |
| 15-29 | Population: 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.
Full OCC List
| OCC | Category | Race Restriction |
|---|---|---|
| Body Fixer | Civilian | None |
| Bounty Hunter | Mercenary | None |
| City Rat | Civilian | None |
| CS Grunt | Coalition | Human only |
| CS Ranger | Coalition | Human only |
| CS Military Specialist | Coalition | Human only |
| CS SAMAS RPA Pilot | Coalition | Human only |
| CS Technical Officer | Coalition | Human only |
| Cyber-Doc | Civilian | None |
| Cyber-Knight | Independent | Humanoid races, Principled/Scrupulous |
| Forger | Criminal | None |
| Freelance Spy | Independent | None |
| Glitter Boy Pilot | Independent | None |
| Gifted Gypsy | Independent | None |
| Headhunter | Mercenary | None |
| ISS Peacekeeper | Coalition | Human only |
| ISS Specter | Coalition | Human only |
| Juicer | Independent | Humanoid biology required |
| Knight (Europe) | Independent | Human, Elf, Dwarf. Principled/Scrupulous. |
| Master Assassin | Criminal | None |
| Ninja Juicer | Independent | Humanoid biology required |
| NTSET Protector | Independent | None |
| Pirate (S.A.) | Criminal | None |
| Professional Thief | Criminal | None |
| Rogue Scholar | Civilian | None |
| Royal Knight | Independent | Human, Elf, Dwarf |
| Sailor (S.A.) | Independent | None |
| Smuggler | Criminal | None |
| Special Forces (Merc) | Mercenary | None |
| Vagabond | Civilian | None |
| Wilderness Scout | Independent | None |
| Ley Line Walker | Special / Limited | Any OCC-capable, wizard discretion, IQ 12+ |
| Maxi-Man | Special / Limited | Full wizard discretion, rarely granted |
| Sunaj Assassin | Secret | Atlantean, Aerihman clan only. Auto-assigned. |
Alignment System
| Alignment | Category | PK Eligible | Description |
|---|---|---|---|
| Principled | Good | No | Strict code of honor above all else |
| Scrupulous | Good | No | Good but flexible when needed |
| Unprincipled | Selfish | No | Self-interest first, avoids harming innocents |
| Anarchist | Selfish | Yes | Does whatever is expedient, no consistent code |
| Miscreant | Evil | Yes | Pure self-interest, harms others for gain |
| Diabolic | Evil | Yes | Evil for its own sake, enjoys causing suffering |
| Aberrant | Evil | Yes | Evil 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
| Command | Syntax | Notes |
|---|---|---|
| look / l | look at <obj> | LPC primary. Strips "at " internally. |
| i / items | i | Inventory list |
| eq / worn | eq | Equipped items by body location |
| score | score / score combat / score skills | Character sheet |
| pskills / sskills | pskills / sskills | Primary and secondary skill lists |
| sbar | sbar | Compact HP/SDC/MDC/XP one-liner |
| kill / attack | kill <target> | Initiates combat. LPC primary. |
| dodge / parry | dodge / parry | Manual, costs one attack |
| autododge / autoparry | autododge on | Toggle auto-defense |
| wimpy | wimpy 25 | Auto-flee when HP below percentage |
| say | say <message> | Room speech. Anonymous by default. |
| chat | chat <message> | Global OOC channel |
| ooc | ooc <message> | Out-of-character, current room only |
| emote | emote <action> | Pose. Anonymous name prefixed. |
| think | think <thought> | Visible thought bubble |
| converse | converse | Toggle: every line treated as say |
| radio | radio <channel> <message> | Requires radio item |
| consent | consent <target> | Grant PK permission to target |
| rest / sleep / wake | rest | Body position, affects HP regen |
| brief | brief on | Suppress room descriptions on revisit |
| prompt | prompt %h/%s/%m | Customize command prompt |
| assistance | assistance | Alert all online wizards for help |
| sirname | sirname <name> | Set your display name |
| description | description <text> | Set visible body description |
| occs | occs | List available OCCs for your race |
| saving throws | saving throws | Show current saving throw bonuses |
| levels | levels | XP table for your OCC |
| abilities | abilities | Racial special abilities |
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
| Stat | Hatchling Value | Notes |
|---|---|---|
| MDC | 1d4x100 + 50 | MDC creature. No HP/SDC pools. |
| Attacks per melee | 4 (hatchling) | Grows to 8 physical at adult |
| Fire breath | 2d6 MDC, 60ft range | Separate from physical attacks |
| Natural claws | 2d6 MDC | Plus Supernatural PS bonus |
| Bite | 3d6 MDC | |
| PS type | Supernatural | Augmented damage bonuses |
| Horror Factor | 14 | Enemies may freeze or flee |
| Bio-regen | 1d4x10 MDC per 5 min | Becomes per-melee-round at adult |
| Teleport | Up to 5 miles | 1 attempt per 2 melee rounds |
| PPE | 3d6x10 | Dragon magic (Ley Line Walker list) |
| ISP | 3d6x10 | Major Psionic, 8 powers (not super) |
| Fly speed | 70mph | Ignores room movement costs |
| Night vision | 90ft | Ignores room darkness flag |
| Can equip armor | No | Natural body is the armor |
| Can wield humanoid weapons | No | Natural weapons only |
True Atlantean RCC
| Stat | Value | Notes |
|---|---|---|
| PS type | Augmented (4d6+4) | Above human but not supernatural |
| Base SDC | 50 | Plus OCC, skills, tattoo bonuses |
| Tattoo SDC | +10 per tattoo | Max 40 extra SDC from tattoos |
| Marks of Heritage | 2 tattoos | Every True Atlantean starts with these |
| Sense magic | Passive, 30ft | |
| Sense rifts | Passive, 1 mile | |
| Vampire protection | Heritage tattoo | Immune to vampire bite and mind control |
| PPE | 2d6x10 + OCC bonus | Tattoo 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.
Wizard Tools
Two holdable objects give wizards simplified administration commands. When picked up, a CmdSet activates on the wizard. When dropped, it removes.
| Tool | Commands Unlocked | Room echo |
|---|---|---|
| RP-WIZ Skill Tool | wizgive <skill> to <player>, wizlist <player>, wizrevoke <skill> from <player> | "adjusts something on a device and nods" |
| Tattoo-Gun | wiztat <tattoo_key> to <player>, wiztat list <player> | "traces a glowing pattern onto skin" |
| Wizard Clan Key | wizknow 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.
Corrections from Earlier Documents
RCC vs OCC
| Type | Examples | OCC Available | Ability source |
|---|---|---|---|
| RCC only | Great Horned Dragon, Mind Melter, Dog Boy, Burster | Never | Race grants all abilities, magic, psionics, attack progression |
| OCC capable | Human, Elf, Dwarf, Kankoran, Orc | Via wizard, post-creation | Race provides base stats. OCC provides skills, equipment, attacks. |
| OCC capable + secret | True Atlantean | Standard OCC + Sunaj (secret) | Atlantean RCC abilities always active. OCC layers on top. |
Race OCC Eligibility Matrix
| Race | OCC Access | Restriction |
|---|---|---|
| Human | All OCCs including all Coalition | None. Most versatile. |
| Elf | General + Knight + Ley Line Walker | Magic affinity |
| Dwarf | General + Knight + Techno-Wizard | Craftsmen heritage |
| Kankoran | General (Wilderness Scout preferred) | Wolf-person tracker |
| Ogre / Orc / Troll | General (combat-focused) | Some stat caps limit IQ-based skills |
| True Atlantean | General + Sunaj (secret) | Clan affects options |
| All other * races | General OCCs only | Size or biology may restrict some |
| All non-* races | None | RCC only |
| CS OCCs | Human only | Coalition is human-supremacist |
| Knight / Royal Knight | Human, Elf, Dwarf | Principled or Scrupulous alignment |
| Juicer / Ninja Juicer | Humanoid biology required | No 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
- Player types
occsto see eligible list - Player types
assistanceto alert wizards, ortell <wizard> I would like to request <OCC name> - Wizard reviews with
wizocc list <player> - Wizard confirms eligibility and types
wizocc <occ_key> to <player> - System grants starting skills, equipment, HP die, SDC bonus, attacks per melee at level 1
- Player sees: "You have been assigned the OCC. Type pskills to review."
- Room sees (anonymous): "A wizard consults something briefly and nods."
The Aerihman Secret Path
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.
- Weapon Proficiencies -- strike bonus in combat, visible immediately
- Hand to Hand types -- attacks per melee and combat bonuses, critical for every fight
- Pilot skills -- gate vehicle use, needed for EX-5 quest
- Espionage subset -- Tracking, Wilderness Survival, Disguise, Intelligence
- Medical -- First Aid restores SDC on day one
- Physical -- Athletics, Running, Climbing, Acrobatics modify movement and dodge
- Communications -- Radio: Basic enables the radio command
- Everything else -- data-only first, effects added over time
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 Pool | Verb |
|---|---|
| 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
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:
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 command | Effect |
|---|---|
hawk stay | Sets following = False. Hawk settles in place. |
hawk follow me | Sets following = True. Hawk rises and follows owner on move. |
hawk fly | Sets airborne = True. Hawk circles overhead. |
hawk eat corpse | Hawk 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
| Machine | Role | Software |
|---|---|---|
| MAC MacBook Neo | Terminal and editor only. No code runs here. | Emacs, Godot 4 |
| WIN Windows PC (WSL) | MUD development server | WSL2, Fedora Linux (WSL2), Python 3.11, Evennia, sshd on port 22 |
| VPS AlmaLinux VPS | Public test server | AlmaLinux 9, Python 3.11, Evennia, git |
MacBook Neo Setup
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
wsl --install --no-distribution wsl --install -d AlmaLinux-9
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
sudo ssh-keygen -A sudo /usr/sbin/sshd
ipconfig in Windows CMD. Add to ~/.ssh/config on Mac:
Host aethermud-dev HostName 192.168.1.100 Port 22 User yourusername IdentityFile ~/.ssh/id_ed25519
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
- SSH into zeus-wsl from Mac terminal: ssh zeus-wsl
- In the integrated terminal:
source ~/.venv/aethermud/bin/activate - If Evennia is not running:
evennia start - Edit code in VS Code and save
- In a MUD client or browser, type
reloadin-game - Test the change
- When ready for VPS:
git add . && git commit -m "describe change" && git push vps main - SSH into VPS and run
evennia reload
tail -f server/logs/server.log. Errors from reload show file name and line number within one second.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.
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.
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.
| Component | Source | Notes |
|---|---|---|
| Base roll | 1d20 | Natural 1 = automatic miss. Natural 20 = critical. |
| HTH strike bonus | HTH type + level | See HTH tables below |
| WP strike bonus | Weapon Proficiency + level | Only applies when using that weapon type |
| PP strike bonus | PP stat | +1 per 2 points of PP above 16 |
| Ambush/backstab | Specific 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.
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.
Hit 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
| Roll | Name | Effect |
|---|---|---|
| Natural 20 | Critical hit | Double damage. Cannot be parried or dodged. |
| Natural 20 (HTH Martial Arts/Assassin, high level) | Knockout | Target saves vs PE or is unconscious for 1d4 melee rounds. |
| Natural 1 | Fumble | Automatic 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.
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
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
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
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
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 Skill | Level 1 | Level 4 | Level 8 | Level 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 Type | PS Value | Bonus |
|---|---|---|
| Normal | 16 | +1 damage |
| Normal | 17 | +2 damage |
| Normal | 18 | +3 damage |
| Normal | 19-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 |
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 Rating | Base save target | Example creature |
|---|---|---|
| 6-8 | Roll 1d20, need 6+ | Gargoyle, Gurgoyle |
| 9-11 | Roll 1d20, need 8+ | Brodkil Berserker, most demons |
| 12-14 | Roll 1d20, need 10+ | Great Horned Dragon (hatchling), Hell Demon |
| 15-17 | Roll 1d20, need 12+ | Dragon (adult), Demon Lord |
| 18+ | Roll 1d20, need 14+ | Ancient dragon, greater supernatural |
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.
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 State | Condition | Rule |
|---|---|---|
| HP above 1 | Normal | Full combat capability |
| HP 0 to -9 | Coma | Unconscious. 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 lower | Dead | Character dies. at_death() is called. |
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
| Pool | Natural rate | With rest | With skill/treatment |
|---|---|---|---|
| SDC | 1d4 per hour of activity | 2d6 per hour rest | First Aid: +1d6. Paramedic: +2d6. Body Fixer: full OCC tables. |
| HP | 1 per hour of activity | 2 per hour rest | First Aid: stabilizes coma. Paramedic: +1d4. Body Fixer: +2d6 per treatment. |
| MDC (non-creature, armor) | Does not heal naturally | Does not heal naturally | Mechanics skill or Armorer. |
| MDC (dragon) | 1d4x10 per 5 min | 2d6x10 per hour rest | Bio-regeneration is racial, not supplemented by skills. |
| PPE | 5 per hour | 10 per hour sleep | On ley line: 5/min. At nexus: 10/min. |
| ISP | 2 per hour | 6 per hour sleep | Meditation: 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
systems/saves.py-- saving throw function. No dependencies.systems/hth.py-- HTH tables and level-up function.- Update
systems/combat.py-- replace resolve_attack() with full version above. - Add
resolve_defense()to combat.py. - Add
get_total_strike_bonus()summing HTH + WP + PP bonuses. - Add
horror_factor_check()called from initiate(). - Add
coma_tick()called from MeleeCombatScript. - Add
systems/healing.pyas a script-driven recovery 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
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
| Condition | PPE per hour | Notes |
|---|---|---|
| Normal activity | 5 | Baseline for all practitioners |
| Rest (awake) | 10 | db.resting = True |
| Sleep | 15 | db.sleeping = True |
| On a ley line | 5 per minute | db.on_ley_line = True, set by room |
| At a nexus point | 10 per minute | db.at_nexus = True, set by room |
| Meditation | 10 per minute | Specific 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.
| Effect | On ley line | At nexus |
|---|---|---|
| PPE regeneration | 5 per minute | 10 per minute |
| Spell PPE cost | Half normal | Quarter normal |
| Spell range | Double | Triple |
| Spell duration | Double | Triple |
| PPE sensing | Within 1 mile | Within 10 miles |
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
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.
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.
Spell List -- Levels 4 to 6
Spell List -- Level 7 and Higher
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.
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 ISP | Regeneration | Notes |
|---|---|---|
| Major Psionic: 3d6x10 | 2/hr, 6/hr sleep | Great Horned Dragon, Burster |
| Master Psionic: 4d6x10 | 3/hr, 8/hr sleep | Mind Melter only |
| Minor Psionic: 2d6x10 | 2/hr, 4/hr sleep | Dog 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
| Category | Focus | Who can access |
|---|---|---|
| Sensitive | Perception, detection, mind reading, remote sensing | All psionic characters |
| Physical | Physical enhancement, telekinesis, bio-manipulation | Major and Master |
| Healer | Bio-regeneration, curing disease, healing others | Major and Master |
| Super | Pyrokinesis, mind bleeder, psi-sword, advanced TK | Master only (Mind Melter) |
Core Power List
Psionics Code Implementation
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
use instead of cast.