Skip to content

fix(server): wire game loop, ring feedback, map transitions and firework boost#107

Open
TheMeinerLP wants to merge 20 commits intomainfrom
fix/sprint1-game-loop-wiring
Open

fix(server): wire game loop, ring feedback, map transitions and firework boost#107
TheMeinerLP wants to merge 20 commits intomainfrom
fix/sprint1-game-loop-wiring

Conversation

@TheMeinerLP
Copy link
Copy Markdown
Contributor

Summary

Fixes all 7 Sprint 1 critical gaps identified in the game design analysis, plus the firework rocket boost.

Changes

Game Loop Wiring

Firework Rocket Boost

  • Root cause: ElytraPhysicsSystem was sending player.setVelocity() every tick with values built from Vec.ZERO, fighting the client's native elytra physics and immediately cancelling any boost applied the previous tick
  • Fix: ElytraPhysicsSystem no longer sends velocity to the client; instead it estimates server-side velocity from the player's position delta each tick (used by collision systems)
  • Boost fix: PlayerEventHandler.onUseItem now uses the position-delta-based velocity estimate as the base for applyFireworkBoost(), with a fallback to look-direction × 0.6 when no estimate is available yet; the resulting impulse is sent once to the client via player.setVelocity()
  • ElytraFlightComponent gains previousPosition field for position-delta tracking

Issues closed

Closes #93, #94, #95, #96, #97, #98, #99

Test plan

  • Join server → lobby spawns, countdown starts
  • After countdown → map loads, elytra equipped, flight activates
  • Fly through a ring → actionbar feedback + sound plays
  • BOOST ring → player accelerates
  • SLOW ring → player decelerates
  • Rings visible as particles in-world
  • Use firework rocket while flying → strong forward acceleration
  • Race timer expires → end phase shows ranked scores
  • Next map loads after first map ends
  • All maps complete → end phase → server stops (CloudNet restarts)

ElytraFlightComponent.setFlying(true) was never called, so the
ElytraPhysicsSystem and RingCollisionSystem skipped all player
entities. Now activateElytraFlight() is called after teleport and
race kit equip to enable flight for each player.
RingCollisionSystem now accepts a GameHudManager and calls
showRingPassed() when a player flies through a ring.
GameEntityFactory.createPlayerEntity() now adds a RingEffectComponent
so ring effects (boost/slow) are processed. RingEffectSystem is
registered in GameOrchestrator after RingCollisionSystem.
MinestomGamePhase now tracks elapsed ticks and finishes when the
configurable race duration expires (default 5 min / 6000 ticks) or
when all players have passed every ring on the active map. Also
accepts an onGamePhaseFinished callback for map transition wiring.
GamePhaseFactory now accepts an onGamePhaseFinished callback that is
passed to MinestomGamePhase. GameOrchestrator wires advanceToNextMap()
as the callback so that when a race ends (duration or all rings), the
next map loads automatically. When the cup is complete, the phase
series naturally advances to MinestomEndPhase.
MinestomEndPhase now reads scores from ScoreComponent via the
EntityManager, applies position bonuses (1st:50, 2nd:30, 3rd:20,
rest:10), and displays a ranked scoreboard as title + chat message.
This makes ScoreComponent the single source of truth for scoring
during gameplay, eliminating the duplication with ScoringServiceImpl.
RingVisualizationSystem spawns a circle of particles at each ring
position once per second so players can see where to fly. Different
ring types use different particles: END_ROD (standard), FLAME (boost),
COMPOSTER (checkpoint), SNOWFLAKE (slow), ENCHANT (bonus). The system
operates on the game entity's ActiveMapComponent and broadcasts packets
to all online players.
Minestom does not fire a native firework boost event. PlayerEventHandler
now listens for PlayerUseItemEvent, checks if the player is holding a
FIREWORK_ROCKET while flying elytra, applies ElytraPhysics.applyFireworkBoost()
to their velocity, and consumes one rocket from the stack. The ECS
EntityManager is wired into the event handler via VoyagerServer.
…ramework

Complete rewrite of the game psychologist agent based on academic research
(SDT, Flow Theory, Hook Model, Operant Conditioning, Kahneman/Tversky, Duolingo
streak data). Adds the VOYAGER Psychology Checklist, 9-step feature review
framework, Bartle player type analysis, concrete numeric thresholds, sound
design specifications, onboarding protocol, and hard ethical boundaries.

Adds supporting research document 004-game-psychologist-agent-research.md.
Registers a dev-only command that skips the lobby countdown and
immediately loads the first available map. Only active when the
server is started with -Dvoyager.dev=true (set automatically by
the :server:runServerDev Gradle task).
startGame() creates player entities only for players online at that moment.
When the server auto-starts the game at boot (no players yet), any player
joining later has no ECS entity — activateElytraFlight() found nothing and
the firework boost check returned early (flight == null).

Now creates the entity on-demand when no existing entity is found.
…map config

- Fix unit bug: player.setVelocity() expects blocks/second; previous code
  passed blocks/tick (1/20th of needed magnitude) causing the 'braking' feel
- Change boost direction: yaw-only (pitch ignored) + fixed upward angle (25°)
  so players always gain height regardless of look direction
- Sustain boost for 20 ticks via scheduler instead of single packet so the
  client's elytra physics registers the impulse across multiple frames
- Add BoostConfig record with speedBlocksPerTick, upAngleDeg, durationTicks,
  cooldownMs — defaults to 2.5 b/t, 25°, 20 ticks, 3s cooldown
- Thread BoostConfig through MapDefinition → GameOrchestrator.loadNextMap()
  → PlayerEventHandler.setBoostConfig() for per-map tuning
- Add 3-second cooldown to prevent back-to-back boost spam
…oldown, 30-tick duration

Per game design spec:
- 3 firework rockets per map (refilled at each map start via equipForRace)
- 4-second cooldown between boosts (prevents chain-boost spam)
- 30-tick boost duration (1.5 s — matches vanilla firework feel)
- Rockets consumed from hotbar slot on each use

BoostConfig.DEFAULT updated accordingly; per-map overrides remain possible.
…ed angle

Remove upAngleDeg from BoostConfig — direction is now derived entirely from
the player's current pitch and yaw at the moment of activation.

  lookX = -sin(yaw)  * cos(pitch)
  lookY = -sin(pitch)               // pitch < 0 = looking up → positive Y
  lookZ =  cos(yaw)  * cos(pitch)

Players control the boost trajectory by where they look: up to climb, level
to sprint, or down to dive. No fixed upward component is imposed.
Sending setVelocity() every tick for 30 ticks locked the player into a fixed
trajectory and fought the client's own elytra steering. Replace with a single
one-shot velocity impulse; the vanilla client's elytra physics then handles
drag, gravity, and directional control naturally from that starting speed.

Remove durationTicks from BoostConfig — no longer meaningful for a one-shot boost.
Ring collision now only checks the next expected ring in sequence — players
must fly through portals in order instead of being able to skip ahead.
Replaces the random-order loop with a single passedCount() index check.

Also sets chunk and entity view distance to maximum (32) before server init
so players see the full course at all times during elytra flight.

Updates RingCollisionSystemTest and GameOrchestratorTest to match the new
sequential enforcement and 3-arg GameOrchestrator constructor.
- Replace custom lastBoostTime check with SetCooldownPacket so the firework
  rocket greys out in the hotbar for the cooldown duration (vanilla UX)
- Keep a lightweight server-side guard against race-condition double-fires
- Remove rocket consumption — rockets are infinite (never leave the slot)
- Give 1 rocket on equip instead of 3 (count irrelevant when infinite)
Adds OutOfBoundsSystem that teleports a player back to the current map's
spawn position when they fly below Y=-64 (void), above Y=320 (world ceiling),
or land on the ground after having been airborne for at least 1 second (20 ticks).

A 40-tick cooldown after each reset suppresses the landing check while the
player is standing at spawn before re-activating their elytra. Out-of-bounds
checks are always active regardless of cooldown.

The system is registered in GameOrchestrator between ring collision and ring
effects. Velocity and previous-position tracking are cleared on reset to
prevent physics artifacts on the next tick.
Add BoostConfigDTO to shared/common so per-map boost values can be stored
in the existing map.json file alongside portals:

  {"boostConfig": {"speedBlocksPerTick": 3.0, "cooldownMs": 2000}}

Both fields are optional — Gson leaves absent fields as null, and CupLoader
falls back field-by-field to BoostConfig.DEFAULT so old JSON files continue
to work without any changes.
Previously dev-start called loadNextMap() only — the phase series stayed
in Lobby, so MinestomGamePhase.onUpdate() never ran, entityManager.update()
was never called, and ring collision / scoring never worked.

Now calls skipLobbyToGame(): loads map then calls phaseSeries.advance()
to transition Lobby → Game so the ECS game loop ticks every server tick
and rings, scoring and HUD all function correctly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Elytra flight never activated — all physics and collision systems are dead

1 participant