InteractionSystem
The rules of every moving and touchable object, behind an event and request boundary that keeps it free of Blupi's controller, the camera and graphics.
Location
| Header | src/GalaxyEggbert/Game/InteractionSystem.hpp |
|---|---|
| Sources | src/GalaxyEggbert/Game/InteractionSystem.cpp (with CratePush.hpp, TankShot.hpp, ObjectVerticalPlacement.hpp) |
| Namespace | GalaxyEggbert::Game |
| Engine dependency | None for its logic. The header includes only ObjectiveHint.hpp, Sound.hpp, WorldRuntime.hpp and Def/BlupiAction.hpp. Through Sound it links against CNA's audio types, but it plays sounds only by channel number and its tests never load an audio device. |
Purpose
Each Play tick, InteractionSystem::Update advances every object of the world and resolves its contact with Blupi,
following the original game's Decor code. It works directly on the MobileObjSpec list in
WorldRuntime and on Blupi's position as plain floats. It covers:
- Patrols: platform lifts and conveyors (and Blupi riding them), patrolling creatures, fish, birds, wasps, the big creature, bulldozers, moving mines, all in the original's 20 Hz dwell/advance/dwell/recede timing.
- Contacts: the shared kill list of hazards, balloon pops, mines, followers that wake and home in, enemy projectiles, near misses (which make Blupi gasp), lightning columns.
- Pickups: treasure, eggs (at most ten), keys, dynamite, decoys, ammunition, secret-power items, the exit (red arrow or golden key), and the treasure doors that open as chests are collected.
- Blupi's own tools: placing and detonating dynamite, the tank's shots and the helicopter's glue, decoys, crates
pushed and pulled (
UpdateCratePush). - Counters: lives (reset to three on game over), treasures collected and total, three key colours, eggs, dynamite, bullets, the game-over count.
- Effects as objects: explosions, splashes, bubbles, sparkle bursts, wrecks, pieces flying away ("bye-bye"), the reward icon flying to the HUD ("voyage").
Which object type does what is not decided here but read from the capability flags of
ObjectDefinitionRegistry (patrolMotion, genericContactHazard,
standardPickupTouch, dynamiteDestructible, ...). Type-specific branches remain only for distinct state
machines.
Collaborators
Uses
- WorldRuntime: the object list (mutable), the World for tile reactions and doors, solid BigDecor boxes, the hazard cycles.
- ObjectDefinitionRegistry and BlockDefinitionRegistry: per-type facts.
- Sound: plays the original's channels for pickups, blasts and deaths.
CratePush.hpp(crate kinematics),TankShot.hpp(shots against the grid),ObjectiveHint.hpp(compass).
Used by
- GalaxyEggbertGame only (one
interaction_member), plusVerifyInteractionSystem. - WorldEditor's lift preview through the static
AdvanceEditorPlatforms, called by the host.
The event and request boundary
InteractionSystem never calls BlupiController, the camera, the HUD or the renderer.
Anything that needs them is reported: Update() fills a list of Events that the host reads with
EventsThisFrame() and applies. Facts about Blupi that it needs (crouching, ballooned, invincible, in which vehicle,
whether he may receive a power, whether the action button was just pressed) come in as parameters computed by the host.
| Event kind | The host then |
|---|---|
BalloonTouched, BalloonPopped | calls TriggerBalloon() / PopBalloon() on Blupi |
SmallShakeTriggered, BigShakeTriggered | starts a camera shake |
TankFired, GlueDropStarted, GlueDropped | plays Blupi's FireTank / HelicoGlu pose, kicks the helicopter up |
EnemyNearMiss | arms Blupi's Ouf4 reaction |
ShieldGranted, PowerGranted, CloudGranted, HideGranted, InvertGranted, DynamiteTaken | grants the power on Blupi (two-stage pickups carry their position for the item's return) |
VoyageRequested | projects the end points to the screen and calls BeginVoyage() (ResolvePendingVoyage) |
DeathLockRequested | locks Blupi in his death (ResolveDeathLock), then loses a life and respawns |
Died | kept for tests; the live game uses DeathLockRequested |
VoyageRequested and DeathLockRequested are requests: at most one of each exists per tick (the last
wins, as in the original). Lift riding is reported through getters instead (IsRidingLift(), RideDeltaX(),
RideStandY(), ...), which the host passes to Blupi before his next step.
Key members
A selection of the public API of InteractionSystem.hpp (verified 2026-11-10; the header is about 1 200 lines,
much of it notes on the original's behaviour):
| Member | Signature | What it does |
|---|---|---|
Update | void Update(float dt, WorldRuntime& worldRuntime, float blupiX, float blupiY, float blupiZ, float blupiMoveDX, Sound& sound, bool blupiCrouching = false, bool blupiBallooned = false, ...) | One tick of every object. About thirty parameters, almost all booleans describing Blupi's state and gates, with defaults so tests can pass only what they need. |
EventsThisFrame | const std::vector<Event>& EventsThisFrame() const noexcept | What happened this tick (see above). |
UpdateCratePush | CratePushOutput UpdateCratePush(float dt, WorldRuntime& worldRuntime, const CratePushInput& input) | Push, hold and pull crates along X or Z at the original's speed; tells the host where Blupi's hands keep him. |
CrateBeingPushedThisFrame, LastCratePush | bool ...() const noexcept | For Blupi's Push animation and the crate sound. |
SeedRandom | void SeedRandom(std::uint32_t seed) | Seeds the std::mt19937 used for the original's random choices; set per mission and recorded for resume and demos. |
Lives, SetLives, LoseLife, GameOverCount | int Lives() const noexcept, void LoseLife() | The lives counter; the caller chooses the death sound. |
TreasuresCollected, TotalTreasures, Key1Count ... Key3Count, LifeEggCount, DynamiteCount, BulletCount | int ...() const noexcept | Counters shown by the HUD. |
ExitReached, ExitWasGoldenKey | bool ...() const noexcept | The mission is won (the host plays the 40-tick win cheer first). |
PlaceDynamite, TryPerso | bool PlaceDynamite(WorldRuntime&, float x, float y, float z, bool grounded, ...) | Blupi's action button with dynamite or a decoy. |
OpenDoor | static void OpenDoor(WorldRuntime& worldRuntime, int gx, int gy, int gz, Sound& sound) | Opens the door block at a raw cell: the block becomes air and a slide carries its icon up, as the original's Decor::OpenDoor(). Used for the hub doors when a hub loads. |
BeginVoyage, VoyageActive, VoyageProgress | void BeginVoyage(WorldRuntime&, VoyageKind kind, int iconId, bool isButtonChannel, ...) | A reward or life icon flying between world and HUD; the reward is applied when it lands. |
IsRidingLift, RideDeltaX/Z, RideStandY, LiftTopsNear | bool / float getters, static int LiftTopsNear(...) | Lift support for Blupi's next Step(). |
NearestObjective, NearestTreasureDistance | static std::optional<ObjectiveHint> NearestObjective(...) | The compass aid: nearest chest, then the exit. |
MockeryDetect, BlupiTouches | static Def::BlupiAction MockeryDetect(const WorldRuntime&, float blupiX, ...) | Pure queries: which taunt an enemy calls for, whether a type's contact box touches Blupi. |
AdvanceEditorPlatforms | static void AdvanceEditorPlatforms(WorldRuntime& worldRuntime, float dt) | The editor's lift preview: the same patrol routine, no contacts, rewards or world writes. |
CheatCleanAll, CheatAllTreasure, CheatFindExit, CheatAllKeys, CheatSetDynamite | bool CheatCleanAll(WorldRuntime&), ... | Entry points for the cheats. |
How it is used
The host creates a fresh instance for every mission, keeping only the lives, and seeds it:
const int preservedLives = interaction_.Lives();
interaction_ = InteractionSystem();
interaction_.SetLives(preservedLives);
...
interaction_.SeedRandom(missionSeed_);
Each tick, after Blupi's step and the crate update, it calls interaction_.Update(simDt, worldRuntime_, ...), patches
the terrain if tile reactions changed blocks, resolves a pending voyage, and then applies the events:
if (FindInteractionEvent(InteractionSystem::EventKind::SmallShakeTriggered))
{
cameraShake_.Trigger(CameraShakeType::Small);
}
...
if (FindInteractionEvent(InteractionSystem::EventKind::EnemyNearMiss) && !blupi_.IsInVehicle() && ...)
{
blupi_.ArmOuf(BlupiController::AnimState::Ouf4);
}
(from GalaxyEggbertGame::UpdateTickEvents, src/GalaxyEggbertCNA/GalaxyEggbertGameUpdate.cpp).
Rules and invariants
- No controller, camera or graphics dependency. This is one of the project's non-negotiable boundaries
(
CURRENT.md): keepInteractionSystemindependent ofBlupiController, the camera and graphics, and use the established event/request boundary for anything that crosses systems. A new interaction adds a parameter or an event, never an include. - Registry first. Per-type behaviour comes from
ObjectDefinitionRegistry; do not add parallel type lists here. - The original's timing. Patrols, dwells, crate speeds and lift moves are in 20 Hz ticks copied from the reference. Worlds and autoplay scripts are tuned to them; a small change can break a playthrough without any compile error.
- Deterministic. All randomness goes through the seeded
rng_.
Tests
VerifyInteractionSystem(ctest, runs from the repository root againstworlds3d/world999.vwr): lift patrols and riding, crate push and pull timing against the original's tables, pickups (treasure, egg, keys, exit), enemy and hazard contact, balloons, followers, dynamite blasts, doors, the Blitz/crusher/Temp cycles.Soundis constructed but never loaded, so no audio device is needed.VerifyDefinitionRegistrieschecks the capability flags it reads.tools/audit/patrol_envelope.pyandtools/audit/lift_envelope.pycheck that shipped worlds use the original's creature and lift timings; the autoplay playthroughs exercise everything end to end.