π« Aether β Body Stats & Life-Sim Expansion
Living design document. The canonical reference for the Aether (formerly Life Sim) feature. Aether v1.0.10 is SHIPPED β skeleton (1.0.0), real-time decay + body panel UI (1.0.1), 5 activities (drink/eat/sleep/work/travel) with AI judge (1.0.2), console-error cleanup (1.0.3), stat β XP multiplier (1.0.4), character mood gate (1.0.5), age-gated intimate stats (1.0.6, since simplified in 1.0.10), Workshop Publish toggle (1.0.7), system prompt injection (1.0.8), registry-aware age gate (1.0.9), and simplified age gate (1.0.10: just `age >= 18`, removed the persona pattern-matcher β too many false positives like "demon girl", German "MΓ€del"/"FrΓ€ulein"). The species registry is still loaded for body function defaults and breed dropdowns, but the gate doesn't consult it. Phase 3 polish (cinema/beach/disco activities) is the next planned step. Pack ships as an opt-in expansion; main app version is 4.0.0 (Aether v1.0.10 has independent semver and uses the 1.0.x patch line).
- Β§1. Vision & Design Principles
- Β§2. Phased Rollout
- Β§3. Data Model
- Β§4. Decay & Time Model
- Β§5. Activities Matrix
- Β§6. Stat β XP/Reputation Multiplier
- Β§7. Persona-Aware Scoring
- Β§8. API Surface
- Β§9. UI/UX Wireframes
- Β§10. Edge Cases, Testing, & Future Hooks
- A. Glossary
- B. Open Questions
- C. References
- π 18+ Community Input
Β§1. Vision & Design Principles
The Life Sim is a maintenance-and-modifier layer that lives underneath the existing chat. The chat stays the primary experience β the Life Sim adds depth, urgency, and a sense of physical presence for the user, and (optionally) a body for the character.
Three design pillars:
- Stat decay. Body needs degrade in real time. A user who logs in once a day plays differently than one who logs in every hour β but neither is punished; the system is forgiving (0.2Γ floor on the multiplier).
- Persona-aware actions. Eating, drinking, traveling, etc. are scored by the AI against the character's persona. Peony (succubus) and a vampire character would react very differently to the same food choice. Score: 1-20.
- Modifier synergy. The body stats, the existing Reputation (0-100), and the existing relationship XP (0-2150) all compose multiplicatively. No stat governs alone; everything stacks.
Compatibility promise: the Life Sim is purely additive. Existing rooms, users, and chats work unchanged. Default values for new rows match today's behavior (stats start neutral, no action happens unless the user opts in).
Β§2. Phased Rollout
| Version | Scope | Status |
|---|---|---|
| 4.0.0 | Player body (3 stats: stamina, food, drink), home-only activities (sleep, eat, drink), persona-aware scoring 1-20, character mood (derived, hidden until CLOSE FRIEND), stat β XP/Reputation multiplier, system prompt injection, server tick decay, lazy reconciliation, 18+ community poll | DOCUMENTED |
| 4.1.0 | Fainting mechanic (stamina + food + drink all 0 β user faints, OC narrates the skip), more stats (hygiene, bladder, fun, social, stress, health), activity_log table for dailies | PLANNED |
| 4.2.0 | Locations (work, restaurant, cinema, disco, beach, park, gym, mall), travel action with time cost, money column, work activity earns income, full activities grid per location | PLANNED |
| 4.3.0 | Full character body (separate hunger, energy, health, mood rows), character activities, character-side prompt injection expands | PLANNED |
| 4.4.0 | Intimate stats (arousal, sensuality, intimacy, desire) β gated behind CLOSE FRIEND/INTIMATE tiers, pending community poll results. Multi-character rooms, NPC schedules, item inventory, season/weather, dynamic macro resolver. See the 18+ community input section below. | FUTURE |
Β§3. Data Model
users βββ¬ββ user_stats (per user, per room) ββ room
βββ messages ββ room
βββ relationships ββ room ββ character_mood
βββ character_food_prefs
user_stats β player body, one row per (user, room)
| Column | Type | Default | Notes |
|---|---|---|---|
| user_id | INTEGER | β | FK β users.id, part of PK |
| room_id | INTEGER | β | FK β rooms.id, part of PK |
| stamina | INTEGER | 90 | 0 = exhausted, 100 = fresh |
| food | INTEGER | 80 | 0 = starving, 100 = full |
| drink | INTEGER | 80 | 0 = dehydrated, 100 = quenched |
| mood | INTEGER | 70 | derived; stored for stable-mood freeze |
| last_action_comment | TEXT | NULL | Comment from the last eat/drink, injected into the next system prompt then cleared |
| last_action_at | TEXT | NULL | ISO timestamp; used to track comment age |
| last_updated | TEXT | β | ISO timestamp; tick & lazy decay use this |
character_mood β derived mood, one row per room
| Column | Type | Default | Notes |
|---|---|---|---|
| room_id | INTEGER | β | PK, FK β rooms.id |
| mood | INTEGER | 70 | derived from persona context; updated lazily |
| last_updated | TEXT | β | ISO timestamp |
character_food_prefs β per-character eating preferences (optional, defaults to persona-only)
| Column | Type | Default | Notes |
|---|---|---|---|
| room_id | INTEGER | β | FK β rooms.id, part of PK |
| kind | TEXT | β | 'food' or 'drink', part of PK |
| item | TEXT | β | e.g. "rare steak", "water", part of PK |
| weight | INTEGER | 1 | Higher = stronger preference |
activity_log β every action the user takes (deferred to 4.1.0)
| Column | Type | Default | Notes |
|---|---|---|---|
| id | INTEGER | autoinc | PK |
| user_id | INTEGER | β | FK β users.id |
| room_id | INTEGER | β | FK β rooms.id |
| activity | TEXT | β | 'sleep' | 'eat' | 'drink' | (later: 'work' | 'cinema' | β¦) |
| location | TEXT | 'home' | For 4.0.0 always 'home' |
| money_delta | INTEGER | 0 | For 4.2.0+ |
| created_at | TEXT | β | ISO timestamp |
Indexes (4.0.0): the two primary keys already cover the hot paths. If read latency becomes a concern, add CREATE INDEX idx_user_stats_updated ON user_stats(last_updated) for the tick sweep.
Β§4. Decay & Time Model
Decay rates (per real hour)
| Stat | Rate | Direction | Notes |
|---|---|---|---|
| stamina | β1 / hour | β drains | Slowest. Decays faster if the user is very active (deferred to 4.1.0). |
| food | β2 / hour | β drains | Average. Eating at a restaurant resets this for a few hours. |
| drink | β3 / hour | β drains | Fastest. The body needs water more often than food. |
Tick architecture
- Server tick every 60s. A single
setIntervaliterates alluser_statsandcharacter_moodrows, applies decay proportional to elapsed time since the last tick, and updateslast_updated. - Lazy reconciliation on read. In case the server restarts mid-tick or is offline for hours, the
GET /api/rooms/:id/statsendpoint applies any missed decay on demand before returning the row. This is the same pattern the Reputation system already uses. - No background job / cron. The tick lives in the same Node process as the chat server. PM2 restarts cleanly. If user count grows 100Γ, swap the in-process tick for a job queue (TODO comment in code).
- Performance. At today's scale (3 users, 7 rooms) the tick touches ~10 rows. Worst case: 1 SQLite UPDATE per row, all in one transaction, well under 1ms.
Stabilization rule (mood freeze)
stamina β₯ 75 AND food β₯ 75 AND drink β₯ 75, the player's mood is frozen at its current value β no further decay or formula re-evaluation. As soon as any one of the three drops below 75, normal mood drift resumes.
Worked example: 4 hours of inactivity
Initial state (t = 0):
stamina = 90, food = 80, drink = 80, mood = 70
After 4 hours of no activity:
stamina: 90 β (1 Γ 4) = 86
food: 80 β (2 Γ 4) = 72
drink: 80 β (3 Γ 4) = 68
Mood check: drink (68) is now below 75 β stability broken.
mood = clamp(50 + (86β50)/5 + (72β50)/5 + (68β50)/5, 0, 100)
= clamp(50 + 7.2 + 4.4 + 3.6, 0, 100)
= clamp(65.2, 0, 100)
= 65
State at t = 4h:
stamina = 86, food = 72, drink = 68, mood = 65
Β§5. Activities Matrix
Phase 1 / 2 / 3 / 4 scope (all at home)
| Activity | Time (real min) | Money | Stat effects |
|---|---|---|---|
| sleep | 480 | 0 | stamina +60, food +20, drink +20 |
| eat | 60 | 0 | food += AI score (1-20, soft cap at 5 if unbelievable) |
| drink | 5 | 0 | drink += AI score (1-20, soft cap at 5 if unbelievable) |
Deferred to 4.2.0 (locations, travel, economy)
| Activity | Location | Time (min) | Money | Stat effects |
|---|---|---|---|---|
| work | work | 240 | +120 | energy β25, stress +15, hunger +15, mood β5 |
| eat (restaurant) | restaurant | 60 | β40 | food += AI score, mood +5 |
| cinema | cinema | 120 | β25 | fun +30, mood +10, energy β5 |
| disco | disco | 180 | β35 | fun +40, social +25, energy β15, stress β20 |
| beach | beach | 120 | 0 | fun +20, mood +15, energy β5, hygiene β10 |
| travel | any β any | 15-20 | 0 | energy β2/3, location changes immediately |
Β§6. Body Stats and Chat XP
Status as of Aether 1.0.11 (2026-06-29): the stat β XP coupling was removed. The v1.0.4 geometric-mean dampener that scaled the chat XP delta by the user's body stats is gone. Body stats are now RP cues only β they feed the OC's prompt-injection (Β§5) and the room's character_mood derivation, but the chat XP math no longer reads them.
Current XP formula (1.0.11+)
sentimentVal = -5..+5 (from the AI judge, unchanged) baseline = 1 (v3.2.0, sacred) repMultiplier = 1..6 (v3.8.0, unchanged) delta = round((baseline + sentimentVal) Γ repMultiplier) newScore = clamp(score + delta, 0, 2150)
The v3.2.0 baseline +1 and the v3.8.0 reputation multiplier are the only inputs. A tired / hungry / thirsty / sad character earns and loses XP exactly as a fully-rested one would. The v4.0.3 safety net (force a non-zero direction when the formula rounds to 0) is preserved.
Why the coupling was removed
User feedback (report id 10, Garlramor, 2026-06-27): the stat dampener made "not much sense to link the amount of lost XP bound to the multiplier of the Rep to the Stats as such. The Reputation and XP should only be used as a measure for the Relationship status dynamics based on the User behaviour, whereas the OC Persona of course determines how the behaviour is perceived and how the reputation and XP points will be influened." The intended use for stats is as RP cues β the OC notices posture (low stamina), hunger (low food), thirst (low drink), tone (low mood) β which is what the Β§5 prompt-injection already does. The XP coupling was a duplicate, punitive path: it was already covered (better) by the OC's awareness of the user's body in the prompt, AND it was punishing the user for the very thing the rest of the system was encouraging them to roleplay around.
The 4.3.2 AETHER_XP_MULTIPLIER_ENABLED env-var flip was the A/B test that confirmed the decoupling felt right. 1.0.11 makes that test the permanent default. The env var is gone; the lib/aether/multiplier.js function is kept dormant on disk for any future re-introduction.
What did NOT change in 1.0.11
- The body panel UI, decay, activities, prompt-injection, character_mood, age gate, and Publish toggle are all unchanged.
- The
user_statstable is unchanged (still owned by Aether; read by Mirror for the profile presence snapshot). - The v3.8.0 reputation β XP multiplier is preserved β that's the user-facing RP signal the user did NOT complain about, and it's independent of the body stats.
- No schema, no API, no UI change visible to the user. The multiplier was never displayed anywhere; the chat sidebar is identical at every stat level.
Β§7. Persona-Aware Scoring
When the user performs an eat or drink action, the server calls the AI to score the action against the character's persona. This is a short, single-turn call β cheap and fast.
Request shape (server β AI)
{
"system": "You are scoring a {kind} action for {character_name}, a {persona_summary}.",
"user": "{user_action}"
}
Response shape (AI β server, required JSON)
{
"score": 1-20,
"believable": true | false,
"comment": "<=120 chars describing character reaction"
}
Scoring rubric
| Score band | Meaning |
|---|---|
| 1 β 5 | Off-putting or breaks character (e.g., a vampire eating garlic bread). believable is likely false. |
| 6 β 10 | Neutral β the action is fine but unremarkable. |
| 11 β 15 | Good match for the character's tastes. |
| 16 β 20 | Deeply in-character, memorable, on-theme. |
Believability gate
If the AI returns believable: false, the server clamps the score to 5 before applying it. The food/drink stat still changes, but only by a small amount. The character's reaction in comment still gets surfaced β so the user sees a clear "Peony is confused" or similar note in their next chat.
Comment feed-forward
The returned comment is:
- Stored in
user_stats.last_action_commentwith alast_action_attimestamp. - Injected into the next chat message's system prompt as a single line:
[Last action: {comment}]. - Cleared from the row after one chat exchange so it doesn't pile up.
Worked examples for Peony (succubus persona)
| User input | Score | Believable | Comment |
|---|---|---|---|
| "I slowly savor a rare steak, the juices running down my chin." | 18 | true | "Peony's eyes follow the sensual motion with quiet interest." |
| "a glass of water" | 7 | true | "Peony watches the glass with polite disinterest." |
| "garlic bread" | 3 | false | "Peony arches an eyebrow at the absurdity of a succubus ordering garlic bread." |
| "a thimble of blood" | 20 | true | "Peony takes the thimble with an approving smile." |
Β§8. API Surface
All endpoints are room-scoped. Authentication is the same as the rest of the chat system (cookie-based session token).
| Method | Path | Purpose |
|---|---|---|
| GET | /api/rooms/:id/stats | Lazy-decayed player stats + character mood (gated by relationship tier). Returns the full user_stats row plus derived character_mood if tier β₯ CLOSE FRIEND. |
| POST | /api/rooms/:id/stats/activity | {activity, item?, narration?} β applies effects, returns the new row + AI comment. |
| POST | /api/rooms/:id/stats/travel | {to} β changes location, deducts travel time. (4.2.0+) |
| SSE | stats-updated event | Broadcast on every activity, travel, or tick. Carries the full new user_stats row + the character mood (if visible). |
Request / response shapes
POST /api/rooms/:id/stats/activity
Request:
{
"activity": "eat",
"item": "rare steak", // required for eat/drink
"narration": "I slowly savor it" // optional, sent to AI
}
Response 200:
{
"ok": true,
"stats": {
"stamina": 90,
"food": 98, // was 80, +18 from AI
"drink": 80,
"mood": 70,
"comment": "Peony's eyes follow the sensual motion with quiet interest."
},
"character_mood": 80 // included only if tier β₯ CLOSE FRIEND
}
Error codes
| Status | Code | When |
|---|---|---|
| 400 | not at location | User tried to work while at home (4.2.0+) |
| 400 | invalid activity | Activity name not in the allowed list |
| 400 | missing item | eat/drink without an item string |
| 403 | locked | User is in the kicked state (XP = 0) and cannot act |
| 422 | AI scoring failed | The persona-scoring call timed out or returned malformed JSON. Server falls back to a neutral score of 10 with a generic comment. |
| 429 | rate limited | One eat/drink per 5 minutes per user. Prevents spam. |
Β§9. UI/UX Wireframes
Body sidebar panel (chat.html, under Reputation)
βββββββββββββββββββββββββββββββββββββββββββ β β‘ 90 π 80 π§ 80 π 70 β β βββββββββββββββββββββββββββββ β β π° $200 π Home β β [ π― Activities ] β βββββββββββββββββββββββββββββββββββββββββββ
Activities modal (at home, 4.0.0)
βββββββββββββββββββββββββββββββββββββββββββ β π― Activities β β βββββββββββββββββββββββββββββ β β π Home π° $200 β β β β [ π Sleep β 8h, free ] β β [ π½ Eat β 1h, free ] β β [ π§ Drink β 5m, free ] β βββββββββββββββββββββββββββββββββββββββββββ
Eat modal (with item + optional narration)
βββββββββββββββββββββββββββββββββββββββββββ β π½ Eat β β βββββββββββββββββββββββββββββ β β What are you eating? β β [ rare steak ] β β β β How are you eating it? (optional) β β [ I slowly savor it, the juices ] β β [ running down my chin. ] β β β β 0 / 2000 [ Eat ] β βββββββββββββββββββββββββββββββββββββββββββ
Character mood gate (locked vs visible)
Tier < CLOSE FRIEND (default): βββββββββββββββββββββββββββββββββββββββββββ β Peony β β ...avatar, bio, XP bar... β β π Mood locked β build trust to see β βββββββββββββββββββββββββββββββββββββββββββ Tier β₯ CLOSE FRIEND: βββββββββββββββββββββββββββββββββββββββββββ β Peony β β ...avatar, bio, XP bar... β β π Mood 80/100 β content β βββββββββββββββββββββββββββββββββββββββββββ
Color tokens
| Range | Color | Meaning |
|---|---|---|
| 0 β 29 | #e94560 red | Critical |
| 30 β 49 | #e8c547 amber | Low |
| 50 | #a0a0b0 grey | Neutral |
| 51 β 80 | #53d769 green | Good |
| 81 β 100 | #bc13fe neon | Excellent |
Β§10. Edge Cases, Testing, & Future Hooks
Edge cases
- No user_stats row yet. The first call to
GET /api/rooms/:id/statslazily creates the row with default values. No migration needed; the row materializes on first access. - AI scoring call fails. Server returns a neutral fallback:
{score: 10, believable: true, comment: "Peony accepts the offering without comment."}. The user always gets some stat gain; the action never errors out. - Server restart during a tick. Lazy reconciliation on the next read catches up the missed decay. The 60s tick is best-effort; correctness is guaranteed by the read path.
- User offline during a tick. Same as above β decay accumulates as time passes, applied lazily on next read.
- Both bodies flagged unbelievable. Soft cap applies independently. If the user's persona AND the character's persona both find the action absurd, both stats still change (small) and both comments are surfaced (one in user, one in character). 4.3.0+.
- Rapid-fire eat/drink spam. Rate limit: 1 eat or drink per 5 minutes per user. Returns 429. Sleep is exempt (it's an 8-hour action).
Testing strategy
- Unit tests for the multiplier. 12 corner-case stat combinations, asserting exact modifier values and the 0.20 floor. Run as part of the existing
test_features.jssuite. - Integration test for activity end-to-end. Register a test user, enter a room, POST
eat, verify the newuser_statsrow, verify the SSEstats-updatedevent, verify the next chat's system prompt contains the comment line. - Manual smoke test for the tick. Set a test user's stamina to 100, wait 2 hours, verify the next read returns stamina=98 (2 Γ 1pt/hr Γ 2h = 2 less).
- Manual smoke test for the mood gate. Confirm character mood is hidden in the API response for a user with XP=350 (FRIEND tier). Bump to 950+ (CLOSE FRIEND), confirm it appears.
Future hooks β how to extend the system
This section is the upgradeable-system contract. Every extension below is a non-breaking additive change.
How to add a new stat (e.g. hygiene)
- Add column to
user_statsvia idempotentALTER TABLE ... ADD COLUMN ... DEFAULT 90in the migration block. - Add a decay rate to
DECAY_PER_HOURat the top ofserver.js. - Add a chip in the Body sidebar panel in
public/chat.html. - Add a modifier row to the multiplier table in Β§6 of this doc and in the
statMultiplier()function inserver.js. - Add a test case in
test_features.js. - Done. No other code paths need to know.
How to add a new activity (e.g. shower)
- Add the activity name to the activities map in
server.jswith its effects:{stat, delta, time_min, location: "home"}. - Add a button in the Activities modal in
public/chat.html. - Add a row to Β§5 of this doc.
- Add an icon and label. Done.
How to add a new location (e.g. park)
- Extend the
LOCATION_LISTconstant inserver.jswith"park". - Add a "Travel to: Park" button in the Activities modal (4.2.0+).
- Add the per-location activity grid to Β§5 of this doc.
- Add a travel cost (time + money) to the
travelaction handler. - Done.
How to add a new character with a different persona
- Create the character via the existing /soulforge.html flow (replaces the legacy /workshop.html editor as of 4.3.0).
- The persona is automatically pulled from the
personafield of thecharacterstable and used aspersona_summaryin the AI scoring prompt. - Optional: seed
character_food_prefsrows if the character has strong known likes. - Done.
How to add a new relationship tier (e.g. SOULMATE)
- Extend the
RELATION_LEVELSarray inserver.jswith a new entry. - Update the character mood gate threshold in
/api/rooms/:id/statsif the new tier changes the visibility rule. - Update the existing
getXPInfo()function inpublic/chat.htmlto render the new label. - Add a row to Β§6 of this doc.
Appendix A β Glossary
| Term | Definition |
|---|---|
| Stat mult floor | The minimum value of the body-stat multiplier, currently 0.20. A fully-depleted player always gets at least 20% of the raw XP delta. |
| Stable mood | The state where stamina, food, and drink are all β₯ 75. Player mood is frozen at its current value and does not decay further until one of the three drops below 75. |
| Believability gate | The soft cap applied when the AI scores an action as believable: false. The score is clamped to 5 before being applied to the stat. The comment is still surfaced. |
| Persona | The character-specific system prompt that defines a character's identity, voice, preferences, and taboos. Lives in the characters.persona DB column. |
| OC | Original Character β the AI-driven character bound to a room. |
| Comment feed-forward | The mechanism that stores the AI's reaction comment from an eat/drink and injects it into the next chat's system prompt, then clears it. |
| Reputation | Existing per-room 0-100 axis that multiplies the XP delta (1Γβ6Γ). Decays 1pt/hr toward 50. See changelog.json v3.8.0. |
| XP | Existing per-room 0-2150 axis that drives the relationship tier (STRANGER β INTIMATE). |
| Tick | The 60-second server-side job that applies real-time stat decay to all rows. |
| Lazy reconciliation | The on-read application of any missed decay, used as a safety net if the server was offline for a while. |
Appendix B β Open Questions
- Tick interval. Currently 60s. Should it be shorter (10s for smoother feel) or longer (5min for less DB churn) as the user base grows?
- Multiplier floor. Is
0.20too generous? Too punishing? A/B candidates:0.10,0.30. - Comment feed-forward length. Currently capped at 120 chars. Should it be 200 for richer flavor? Tradeoff: prompt token cost.
- Character mood visibility. CLOSE FRIEND threshold feels right, but is it too late? FRIEND (350 XP) would reveal the mood earlier in the relationship arc.
- Eating at home vs. restaurant. 4.0.0 has eating at home only. Should home eating give a smaller bonus than restaurant eating (4.2.0), or are they equivalent?
- Decay while sleeping. Should stamina decay pause while the user is sleeping? (Trivially: yes, since the sleep action sets stamina to a high value. But what about a user who logs in, sleeps, logs out, comes back later β does stamina decay happen between the sleep end and the next login?)
- 18+ intimate stats. Pending community poll results. See the 18+ section below.
Appendix C β References
- Reputation Bar design (3.8.0) β
changelog.jsonv3.8.0 entry. The reputation system is the closest existing analog and shares the multiplicative-stacking pattern. - Red Update Banner fix (3.7.1) β
changelog.jsonv3.7.1 entry. The first-tick-on-load pattern is the same one the Life Sim'scheckVersion()uses. - Community poll (18+) β /api/life-sim/poll + /api/life-sim/poll/answer. The data file is
data/life-sim-poll.json. The UI is the "π Life Sim 18+ Community Input" section below. - AGENTS.md β versioning rules, process management (thw), deployment notes.
- π₯ Soulforge Design β the companion design document for the new (beta) Character Creator page, also planned for 4.0.0. The (beta) creator feeds the body baselines and age fields that the Life Sim consumes.
A separate design document covers the planned (beta) Character Creator page that ships alongside the Life Sim in 4.0.0. It includes the 9-section form, the categorized species registry (15 seeded species across canines, felines, equines, avians, reptiles, mythical/fae, undead/demonic, humans/humanoids, mechanical/construct), body function baselines + decay rates, body function profile presets, the live age-gate indicator that reflects this document's Β§7.5 design, the 11-field persona schema, and the full API surface.
π Life Sim 18+ Community Input
An open-ended question for the community about a future consideration in the Life Sim design. Your answers here will help inform whether (and how) intimate stats get added in a later phase. This is a separate system from the bug-report / feedback flow β your input goes into a dedicated poll log, not the support queue.