Skip to content

First Login

Status: 🚧 Partial β€” First-login is the strongest page in the zone: the resumable state machine, ship rarity config, 6 guard personalities with +-0.10 modifier, the exact 0.5/0.3/0.2 aggregate formula … Β· ⚠︎ contains code↔spec divergence (impl audit 2026-06-16)

Purpose

First login is a stateful onboarding flow that doubles as character creation. A new account does not yet have a Player record; the flow runs an AI-driven dialogue against a security guard at the Callisto Colony shipyard β€” the in-lore landmark inside Terran Space's Capital Sector (sector 1 in vanilla Terran Space; see ../FEATURES/definitions.md#sector) β€” evaluates the player's persuasion against rarity-tier thresholds, and on success creates the Player row with a starter ship, credits, and initial state. The flow is resumable, has a guaranteed-safe failure path (escape pod), and works fully offline (manual provider fallback).

Inputs

The state machine reads: - The player's User row (auth identity, locale). - PlayerFirstLoginState row (if any) β€” has the player completed onboarding before? - FirstLoginSession row (if any) β€” in-progress session for resume. - ShipPresentationOptions β€” the 3+ ships offered this attempt. - ShipRarityConfig table β€” admin-tunable spawn probability + persuasion thresholds. - DialogueExchange rows for this session β€” full back-and-forth history. - AI provider chain (same as ARIA dialogue) for response evaluation. - The player's free-form text input.

The system fires on: - GET /api/v1/first-login/status β€” bootstraps or resumes a session. - POST /api/v1/first-login/session β€” start a new session. - POST /api/v1/first-login/dialogue β€” submit the player's next response. - POST /api/v1/first-login/claim β€” finalize the ship claim once persuasion succeeds. - POST /api/v1/first-login/abandon β€” accept the escape-pod fallback.

Process

                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚   App launch (auth)  β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚ should_show_first_login β”‚
                  β”‚ (PlayerFirstLoginState  β”‚
                  β”‚  null or completed=False)β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  no  β†’  go to game (skip flow)
                  yes β†’  enter state machine
                             β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   STATE 1 β†’  β”‚  WELCOME / NARRATIVE OPEN   β”‚
              β”‚  (shipyard backdrop, cat)   β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   STATE 2 β†’  β”‚  SHIP PRESENTATION          β”‚
              β”‚  - generate 3+ ships, rolledβ”‚
              β”‚    against ShipRarityConfig β”‚
              β”‚  - persist ShipPresentation β”‚
              β”‚    Options                  β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   STATE 3 β†’  β”‚  SHIP CHOICE                β”‚
              β”‚  player picks claimed ship  β”‚
              β”‚  β†’ record_player_ship_claim β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   STATE 4 β†’  β”‚  TUTORIAL / DIALOGUE LOOP   β”‚
              β”‚  guard probes 4 topics:     β”‚
              β”‚   β€’ identity_verification   β”‚
              β”‚   β€’ arrival_details         β”‚
              β”‚   β€’ ship_knowledge          β”‚
              β”‚   β€’ situational_awareness   β”‚
              β”‚  per turn:                  β”‚
              β”‚   - generate_guard_question β”‚
              β”‚   - record_player_answer    β”‚
              β”‚   - _analyze_player_responseβ”‚
              β”‚     (persuasiveness, conf., β”‚
              β”‚      consistency, skill,    β”‚
              β”‚      inconsistencies, key   β”‚
              β”‚      info, believability)   β”‚
              β”‚  cat_mention_bonus: +0.15   β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   STATE 5 β†’  β”‚  EVALUATE OUTCOME           β”‚
              β”‚  _evaluate_dialogue_outcome β”‚
              β”‚  compare aggregate          β”‚
              β”‚  persuasion vs rarity       β”‚
              β”‚  threshold (weak/avg/strong)β”‚
              β””β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
                 β”‚ pass              β”‚ fail (after retries)
                 β–Ό                   β–Ό
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚ STATE 6: SPAWN  β”‚   β”‚ STATE 6': ESCAPE POD β”‚
       β”‚ - create Player β”‚   β”‚ auto_approve_escape  β”‚
       β”‚ - create Ship   β”‚   β”‚ _pod (always passes) β”‚
       β”‚ - set credits   β”‚   β”‚ - 500 credits        β”‚
       β”‚   per table     β”‚   β”‚ - persuasion=0.5     β”‚
       β”‚ - sector=1      β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚ - mark first    β”‚              β”‚
       β”‚   _login.compl. β”‚              β”‚
       β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚
                β”‚                       β”‚
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β–Ό
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚  STATE 7: COMPLETE           β”‚
              β”‚  complete_first_login()      β”‚
              β”‚  - PlayerFirstLoginState     β”‚
              β”‚    persisted                 β”‚
              β”‚  - finalize Ship + Player    β”‚
              β”‚  - emit notification         β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Ship-claim difficulty matrix

DEFAULT_SHIP_CONFIGS from the service:

Ship Tier Spawn % Base credits Weak / Avg / Strong threshold
Escape Pod 1 100 1,000 0.30 / 0.30 / 0.30
Light Freighter 2 50 2,500 0.40 / 0.35 / 0.30
Scout Ship 3 25 2,000 0.55 / 0.50 / 0.45
Fast Courier 3 20 3,000 0.55 / 0.50 / 0.45
Cargo Hauler 4 10 5,000 0.65 / 0.55 / 0.50
Defender 5 5 7,000 0.75 / 0.65 / 0.60
Colony Ship 6 3 10,000 0.80 / 0.70 / 0.65
Carrier 7 1 15,000 0.85 / 0.75 / 0.70

Threshold selected based on the AI's negotiation_skill classification (weak / average / strong) of the player's dialogue.

Persistence (resume)

  • FirstLoginSession.id is bound to the User.
  • Each DialogueExchange is persisted as it happens; reload mid-flow returns the full history.
  • Re-opening the app at any state replays from the last persisted state.

Skip / abandon

  • Authenticated player whose PlayerFirstLoginState.completed = True is routed to the main game; the flow does not run.
  • A player who explicitly abandons calls auto_approve_escape_pod β€” the safest outcome (escape pod + 500 credits; final_persuasion_score is set to 0.5, while negotiation_skill retains whatever the dialogue last scored).

AI evaluation

The evaluator (_analyze_player_response) returns:

{
  persuasiveness: float (0..1),
  confidence_level: enum,
  consistency: float (0..1),
  negotiation_skill: weak | average | strong,
  inconsistencies: [],
  key_information: { player_name?, claimed_ship?, ... },
  overall_believability: float (0..1)
}

The provider chain matches the ARIA dialogue chain: primary β†’ secondary β†’ manual rule-based provider. The manual provider replicates the same response shape using pattern-matching, so first login works fully offline.

Aggregate persuasion formula

The session-level persuasion score that gates the threshold check is a weighted aggregate over the dialogue exchanges:

final_persuasion = 0.5 Γ— avg(consistency)
                 + 0.3 Γ— avg(confidence)
                 + 0.2 Γ— avg(persuasiveness)
                 + cat_mention_bonus (0.15 if triggered)
                 + guard_personality_modifier (Β±0.10, see below)

Consistency is the dominant weight because cross-turn coherence is the strongest signal of an honest claim; persuasiveness and confidence are smoothing factors.

Hard-fail triggers

Any one of the following routes the session to escape pod with the notoriety_penalty flag set, regardless of the aggregate score:

  • Average consistency across all exchanges < 0.3.
  • Total inconsistencies (contradictions detected across exchanges) β‰₯ 3.
  • Any single exchange has consistency < 0.2.

Hard-fail awards 300 credits (lower than the standard 500-credit escape-pod path; below) and persists notoriety_penalty = true on the session.

Outcome credits

Three escape-pod-class outcome bands:

Outcome Credits Trigger
SUCCESS per-ship base credits (1,000 – 15,000) aggregate β‰₯ rarity threshold for the claimed ship
PARTIAL_SUCCESS 800 claimed ship is escape pod and the player's aggregate is above the floor
FAILURE 500 dialogue ended without meeting any non-pod threshold; player auto-routed or chose to abandon
FAILURE (hard-fail) 300 one of the hard-fail triggers above fired

The 500/800/300-credit splits all honor invariant 2 β€” every flow ends with a viable starter. Hard-fail's lower credit value plus the persistent notoriety flag is the only mechanical penalty for outright deceptive play.

Dialogue length cap

Each session hard-caps at 5 exchanges (completed_exchanges >= 5 or early_termination finalizes evaluation). The cap is per-session, not per-attempt; combined with the resumable session model, this means a single session always resolves within 5 exchanges, but a player can reset_player_session and start a fresh session afterwards. There is no max-attempts gate β€” PlayerFirstLoginState.attempts is monotonic and never resets.

Guard personality

The session is bound to one of six guard archetypes at creation, drawn deterministically from guard_personalities.py:GUARD_PERSONALITIES:

Trait base_suspicion Threshold modifier
Strict Rule-Follower 0.60 +0.10 (harder)
Friendly Veteran 0.30 βˆ’0.10 (easier)
Paranoid Newbie 0.70 +0.10 (harder)
Tired Night-Shifter 0.40 βˆ’0.10 (easier)
Shrewd Investigator 0.50 0.00
Cynical Bureaucrat 0.55 0.00

The personality is persisted as four denormalized columns on FirstLoginSession (guard_name, guard_title, guard_trait, guard_description, plus guard_base_suspicion) so dialogue UIs can render the in-character name and prompt style without re-deriving from a lookup table. The threshold modifier nudges the rarity-tier cutoff for this session β€” a Strict Rule-Follower playing the Defender card asks for more persuasion than a Tired Night-Shifter would.

Cat easter egg

If the player mentions the orange cat from the opening narrative, the manual provider awards a flat +0.15 persuasion bonus. The bonus is applied regardless of the active provider (the rule-based detector runs in parallel). The detection is stored on the session for medal / achievement use later.

Outputs / state changes

On success path: - Player row created (sector_id=1, current_sector_id=1, default turns=1000, max_turns=1000, military_rank="Recruit", personal_reputation=0, aria_consciousness_level=1, aria_bonus_multiplier=1.0). - Player.credits set to claimed-ship's base credits (1,000–15,000). - Ship row created via ship_specifications_seeder and assigned as Player.current_ship. - PlayerFirstLoginState.completed = True, with negotiation outcome / cat-boost flags. - FirstLoginSession.outcome = success.

On escape-pod path: - Player row created with Escape Pod ship and 500 credits. - FirstLoginSession.final_persuasion_score = 0.5; negotiation_skill retains the dialogue's last classification (it is not force-overwritten to weak). - All other defaults identical.

Always: - DialogueExchange rows record the full conversation. - ShipPresentationOptions records which ships were shown. - notification event fires on completion.

Completion side-effects (complete_first_login)

Final transition out of the flow performs a self-healing cleanup so the new player can never enter the game in a partial state:

  • Existing ship purge β€” any Ship rows owned by the player from prior partial attempts are deleted; the awarded ship is the only one assigned. This makes resume + retry safe even if a prior session crashed mid-spawn.
  • ARIA warm-start β€” Player.aria_relationship_score = 50 (versus the column-default 25 for a never-onboarded player), Player.aria_total_interactions = 1. The first-login dialogue counts as the first ARIA interaction, so the player begins onboarded into the consciousness-level ladder.
  • Nickname capture β€” gated on explicit player confirmation (πŸ“ Design-only). The dialogue's extracted_player_name is presented to the player as "Use <name> as your callsign?" with Yes/No buttons; the Player.nickname write only fires on Yes. The extracted name passes through validation before the confirmation prompt is even shown:
  • Length: 3–20 characters.
  • Charset: alphanumeric + underscore + hyphen + a single internal space; no zero-width or RTL characters; no leading/trailing whitespace.
  • Profanity filter: case-insensitive match against the configurable NICKNAME_BLOCKLIST wordlist (services/gameserver/src/services/nickname_validation_service.py:NICKNAME_BLOCKLIST).
  • Impersonation filter: case-insensitive reject if the candidate matches any existing Player.nickname or User.username.
  • Uniqueness: enforced by a unique index on Player.nickname at the model layer; a race-loser at write time gets a "callsign just taken" error and is asked to pick another. Validation failures surface to the player with the specific reason and offer a free-text retry. Hard-fail sessions (escape-pod fallback, AI-provider exhaustion, dialogue abandoned mid-flow) do not seed an extracted name β€” Player.nickname stays null and the ship name falls back to User.username. The pre-decision behavior of overwriting Player.nickname from extracted_player_name without confirmation, validation, or uniqueness check is retired.
  • First-login marker β€” Player.first_login = {"completed": True, "session_id": <uuid>} (the session_id field is a back-pointer for audit, not a quick-check column).
  • Negotiation bonus β€” if the session completed at negotiation_skill = strong and rarity_tier β‰₯ 3, Player.settings.trade_bonus = 0.1 is set (10% better port prices for the rest of the player's life). Lower combinations leave the bonus unset.

Invariants

  1. A User cannot finish first login twice β€” once PlayerFirstLoginState.completed = True, the flow refuses to start a new session.
  2. The escape-pod outcome always succeeds β€” there is no failure mode that leaves the player without a ship.
  3. Every dialogue evaluation persists before a state advance β€” interruption does not lose history.
  4. Spawn (state 6) is a single transaction: Player, Ship, and FirstLoginState change together.
  5. AI provider failure never blocks the flow β€” manual fallback is always available.
  6. Player.first_login.completed = True means no side-effects are pending.
  7. Initial sector is always Sector 1 of Terran Space (the canonical Earth-area starter region).

Failure modes

Mode Target handling
AI provider down Manual fallback evaluates dialogue; flow proceeds normally.
Player closes tab mid-flow State persisted at every transition; resume picks up where they left off.
Player insists on a ship they can't justify The dialogue loop hard-caps at 5 exchanges per session; once the cap is hit, evaluation finalizes against current scores. There is no separate "N attempts" gate that auto-routes to escape pod β€” the player can always choose to abandon and accept the pod fallback.
Spawn transaction fails Whole transaction rolls back; player can retry; session not marked complete.
Duplicate user (race) Unique constraint on Player.user_id rejects the second insert; second request returns the first Player row.
Provider returns malformed analysis Default to negotiation_skill = average and persuasiveness = 0.5; do not crash.
Ship spec missing in seeder Fail spawn; surface clear admin-facing error; player retries with a different ship.
Locale unsupported Default to English; cat-detection still works (English-only patterns + locale-specific aliases).

Source map

Concern Path (target)
Orchestrator services/gameserver/src/services/first_login_service.py
AI dialogue helpers services/gameserver/src/services/ai_dialogue_service.py
Multi-provider AI services/gameserver/src/services/ai_provider_service.py
Manual fallback services/gameserver/src/services/enhanced_manual_provider.py
Guard personalities services/gameserver/src/utils/guard_personalities.py
Ship specifications seeder services/gameserver/src/core/ship_specifications_seeder.py
First-login models services/gameserver/src/models/first_login.py
API routes services/gameserver/src/api/routes/first_login.py
Admin tools services/gameserver/src/api/routes/admin_first_login.py
Client component services/player-client/src/pages/FirstLogin/*