Eros Flight

Category: Simulation & AI Systems Architecture
Context: Eros Flight was conceived as a highly systemic “Erotes Simulator.” Rather than a traditional guided narrative, it relies on densely populated procedural rulesets to govern NPC attraction, player sanity, and the chaotic matchmaking logic of a literal Cupid.

Though development only reached a vertical slice, the under-the-hood systems demonstrate a robust framework for complex behavioral simulation. The design philosophy is that the player should feel like they are operating a matchmaking system, not playing a dating game — the distinction matters because the feedback loops punish carelessness rather than rewarding patience.

Core Simulation Loop

The session-driven loop governs everything from NPC spawning through match resolution to sanity consequences. SessionManager sets the match quota and duration, NPCManager handles the population, and CupidManager resolves every arrow hit against the compatibility matrix.

Eros Flight Core Loop

The loop is deliberately asymmetric: successful matches heal sanity and award points, but head shots (failed attempts that hit the NPC’s head hitbox) deal severe sanity damage. This creates a risk curve where rushing through matches to meet the quota increases the chance of catastrophic sanity loss.

Systemic NPC Generation

The core population engine is driven by NPCManager.cs and the underlying CupidMetrics.cs assigned to every spawned entity. The game does not rely on hand-authored characters; instead, it dynamically generates an evolving dating pool.

When TriggerNPCSpawning() is called:

  1. Procedural Attributes: Every NPC is instantiated with randomized, tightly-coupled variables: LoveStyle (Heterosexual, Lesbian, Gay, Bisexual, Asexual), GenitalType, and GenitalPreference.
  2. Visual Auras: These attributes are not invisible data points. CupidMetrics.cs ties the assigned LoveStyle directly to the entity’s OutlineColor (Green for Heterosexual, Yellow for Bisexual, etc.), giving the player immediate visual feedback on the systemic matrix they are trying to manipulate.
  3. Matched Set Injection: To ensure the simulation always has solvable states, NPCManager periodically forces SpawnMatchedSet(), which mathematically guarantees two compatible NPCs are introduced into the world simultaneously alongside the completely randomized singles. Without this, the random pool could deadlock into an unsolvable configuration.

Love Style Compatibility

The matchmaking system is not a simple “same type matches” rule. CupidManager.CheckIfMatch() evaluates compatibility through a matrix that accounts for both LoveStyle and GenitalPreference. Bisexual acts as a universal bridge. Two Asexual NPCs are actually the strongest match — they are compatible with each other, but not with anyone else.

Beyond the base LoveStyle check, GenitalPreference allows for mutations: a match that passes the love style gate can still be modified by whether the genital preference aligns with the partner’s GenitalType. This creates edge cases where two NPCs look compatible by aura color but fail at the preference layer, or where an unexpected pairing succeeds because the preference mutation overrides the default matrix.

Love Style Compatibility Matrix

The aura colors are the player’s only real-time information about compatibility. There is no tooltip, no hover state, no explicit UI telling the player which NPCs can match. The player has to internalize the matrix through the color language — Green can match Green or Yellow, Blue can match Blue or Yellow, Yellow matches everything except White, and two Whites are a perfect pair. This is implicit information design: the rules are learnable but never spelled out during gameplay.

The Sanity Mechanic

The player (controlling Eros/Cupid) is not an invincible observer. GameManager.cs tracks an internal ErosState (Sane vs. Insane) governed by currentErosSanity, a float that ranges from 0 to 100.

Sanity State Machine

The state machine is binary but the consequences are tonal. Crossing into Insane does not end the game — it changes the feel of the game. The stylized romantic hearts swap to realistic, fleshy heart renders (insaneHeart prefabs), abruptly shifting the simulation’s aesthetic from whimsical to grotesque. The player can recover by successfully matching NPCs, but the visual damage lingers until sanity climbs back above the threshold.

This is consequence-driven design: the player is never told “you are doing badly.” The world simply becomes uglier in proportion to their failures.

Matchmaking Resolution

When the player fires a love arrow and it impacts an NPC’s body (not head), CupidManager.OnArrowHit() tracks the hit:

  1. First Hit: The NPC is stored as FirstNPC. The player has committed to one half of a potential pair.
  2. Second Hit: CheckIfMatch() evaluates FirstNPC.LoveStyle against the second NPC’s LoveStyle using the compatibility matrix. If compatible, the match succeeds.
  3. Match Success: A heart collectible spawns at the match location, both NPCs enter LiminalTransition (walking together toward the temple exit), the player gains +15 sanity and match points.
  4. Match Failure: The pair is incompatible. No sanity penalty for body-hit mismatches, but the player has wasted two arrows and time against the session clock.

The head shot penalty (-15 sanity) exists specifically to punish spray-and-pray tactics. The player cannot just fire arrows at every NPC and hope for matches — they need to read the aura colors, identify compatible pairs, and aim deliberately.

NPC Registry

CupidManager maintains a live registry of every NPC in the simulation, split into two tracked pools:

The registry is what makes the matched-set injection work — NPCManager checks the registry’s unmatched pool before deciding whether to spawn random singles or force a guaranteed compatible pair via SpawnMatchedSet(). If the unmatched pool has drifted into an unsolvable configuration (e.g., all remaining singles are Asexual with no other Asexual present), the registry signals the spawner to inject a solvable pair.

Session Structure

SessionManager.cs governs the macro loop:

The design intent is that early sessions feel manageable — few NPCs, clear aura distinctions, generous time. Later sessions overwhelm the player with population density, forcing faster reads and riskier shots, which naturally drives sanity lower and triggers the Insane state more frequently.