fix(server): wire game loop, ring feedback, map transitions and firework boost#107
Open
TheMeinerLP wants to merge 20 commits intomainfrom
Open
fix(server): wire game loop, ring feedback, map transitions and firework boost#107TheMeinerLP wants to merge 20 commits intomainfrom
TheMeinerLP wants to merge 20 commits intomainfrom
Conversation
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.
…lta and applying look-direction impulse
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes all 7 Sprint 1 critical gaps identified in the game design analysis, plus the firework rocket boost.
Changes
Game Loop Wiring
ElytraFlightComponent.flying = truefor every online playerMinestomLobbyPhase.onFinish()now triggersGameOrchestrator.loadNextMap()via callback inGamePhaseFactoryMinestomGamePhaseends after 5 min (6000 ticks at 20 TPS) or when all players pass all ringsRingCollisionSystemcallsGameHudManager.showRingPassed()on collision;RingEffectComponentadded to player entities so BOOST/SLOW rings apply velocity multipliersRingVisualizationSystemspawns 16 particles per ring in a circle pattern once per second; particle type varies by ring typeGameOrchestrator.advanceToNextMap()called whenMinestomGamePhasefinishes; cup-complete triggersMinestomEndPhaseScoringServiceImplremoved from the active code path;ScoreComponent(ECS) is the single source of truth;MinestomEndPhasedisplays final ranked resultsFirework Rocket Boost
ElytraPhysicsSystemwas sendingplayer.setVelocity()every tick with values built fromVec.ZERO, fighting the client's native elytra physics and immediately cancelling any boost applied the previous tickElytraPhysicsSystemno longer sends velocity to the client; instead it estimates server-side velocity from the player's position delta each tick (used by collision systems)PlayerEventHandler.onUseItemnow uses the position-delta-based velocity estimate as the base forapplyFireworkBoost(), with a fallback to look-direction × 0.6 when no estimate is available yet; the resulting impulse is sent once to the client viaplayer.setVelocity()ElytraFlightComponentgainspreviousPositionfield for position-delta trackingIssues closed
Closes #93, #94, #95, #96, #97, #98, #99
Test plan