feat(tunnel-vision): add survivor tunnel vision effect - #178
Open
TheMeinerLP wants to merge 16 commits into
Open
feat(tunnel-vision): add survivor tunnel vision effect#178TheMeinerLP wants to merge 16 commits into
TheMeinerLP wants to merge 16 commits into
Conversation
Four different services (tunnel vision, blood splatter, slender gaze, and the stamina bar) each hand-rolled the same start/stop guard around a Minestom Task: a nullable field, a null-check before scheduling, and a null-check before cancelling, repeated with small variations at every call site. Extract that guard once as RepeatingTask so the follow-up feature branches can adopt a single, tested implementation instead of copying the pattern a fifth time.
Five separate hand-rolled per-player maps existed across the tunnel vision, blood splatter, and slender gaze code, each keyed by UUID and each managing its own put-on-join/remove-on-leave lifecycle by hand. The duplication made every one of those call sites a place a leak or a stale-entry bug could hide. PlayerState wraps that lifecycle once behind a small, tested type, so the three follow-up feature branches can store their per-player values without re-deriving the same map bookkeeping.
The tunnel vision and slender gaze code each clamped values into range with their own Math.min(max, Math.max(min, value)) expression, which is easy to get backwards (min/max swapped) and gives no shared place to fix it once. Add int and double overloads of Helper.clamp so the follow-up feature branches can call one well-tested method instead of repeating the expression.
Three texture-key builders existed across the tunnel vision, blood splatter, and slender gaze designs, each assembling equipment-slot texture paths with its own naming convention (stage_<n>, <direction>_<variant>_<frame>, level_<level>_<frame>), so nothing about them could be reused or tested together. Add the shared overlay package: ScreenOverlay and OverlayLayer describe where an overlay sits and how it renders, OverlayProperties and EquipmentScreenOverlay handle applying it to a player's equipment slot, and OverlayTextureKeys unifies the three texture-key conventions behind one type. This is the rendering base the three follow-up feature branches (tunnel vision, blood splatter, slender gaze) build their per-effect logic on top of.
The three preview commands (tunnel vision, blood, glitch) each hand-rolled an identical private static asPlayer(CommandSender) check to narrow a CommandSender down to a Player, differing only in the error string sent back to the console. Extract that check once as CommandSenders.asPlayer, a stateless static method rather than an abstract base command, since narrowing the sender is the only thing the three commands have in common and a shared base class would force them into one constructor shape and inheritance chain for a single one-line check.
TheMeinerLP
force-pushed
the
feat/survivor-tunnel-vision
branch
from
August 23, 2026 09:11
3f28aaf to
99b8dd4
Compare
PlayerState and RepeatingTask sat in common/util, but common is what game and setup share, and setup uses neither. They move to game/utils, next to the other game-side helpers (Items, ScoreboardDisplay, StaminaHelper, ViewRuleUpdater), which also removes the second util package common/util vs game/utils opened up. Helper.clamp is dropped again: its body was a straight delegation to Math.clamp, and the codebase already calls Math.clamp directly in LobbyWaitingTask, CygnusPlayer and ColorUtil. HelperTest covered nothing else and goes with it. AmbientProvider now uses RepeatingTask instead of its own nullable Task field with the guard-and-return pair. It was one of the four copies the type was extracted from, so the extraction pays for itself here rather than only in the effects that land on top of this branch. RecordingScreenOverlay joins the test sources: every effect test needs the same recording ScreenOverlay, and writing it per test class is the same duplication this branch removes from the production code.
…lding it asPlayer assembled "<red>Only players " + reason from a sentence fragment each caller passed in. That put message assembly in a command helper while every other player-facing text in the project is a Component in Messages, and it left each caller holding half a sentence that only made sense once concatenated here. It now takes the finished Component and only sends it. The callers land in the three follow-up PRs and each bring their own Messages entry.
The three effect branches each added `this.screenOverlay = new EquipmentScreenOverlay()` to the same constructor, and two of them added a byte-identical `currentSurvivors()` to the same class. Merging them meant resolving the same lines three times, where taking both sides gives either a duplicate method - loud, a compile error - or three separate overlay instances, which is silent: a player wears one head slot, so three overlays would each write it without knowing about the others and the last writer would win. Cygnus creates the one overlay here instead, and the roster lookups move to TeamHelper as survivorsOf/slenderOf, next to the other lookups that already take a TeamService. Each effect branch then adds only its own service. The overlay field has no reader on this branch yet, the same way CommandSenders has no caller: this is the base of a stack, and the alternative is letting the three branches conflict over the line that decides whether the effect works.
Each effect branch wrapped its own registration in its own `if (OverlayProperties.enabled())`. Those blocks land on the same lines, and the closing brace sits behind the conflict marker, so resolving one by keeping both sides yields two opened ifs and one brace - it does not compile, which is at least loud, but it is a conflict nobody should have to think about three times. registerOverlayListeners holds the gate once. Each effect adds its own line to it and nothing else, and the property named cygnus.overlays now actually governs all of them rather than however many blocks happened to be written.
PlayerState and RepeatingTask moved to game/utils in the foundation, and Helper.clamp is gone in favour of Math.clamp; both follow here. clearAll() becomes cleanUp(), which is what the round teardown is called on main (PageProvider, StaminaService, JumpScareManager). The two player-facing texts move into Messages: the usage line as a builder, next to the other builders that interpolate a value, and the players-only message as a constant now that CommandSenders takes a finished Component. show(Player, int) and hide(Player) collapse into preview(Player, int), where SlenderGaze.NONE clears. That is not an invented sentinel - tick() already branches on NONE coming out of SlenderGaze.levelOf, so hide() was a second spelling of a level the domain type already had. The recording ScreenOverlay both tests stood up is the foundation's shared one. The comment gating the effect in Cygnus described a vignette font that no longer exists, and OverlayProperties only named two of its three layers.
Minecraft 26.2 has no way to switch a post-processing shader on for a single player, so the narrowing view is delivered as a camera_overlay rendered onto a head-slot item instead. The renderer drives 32 stages with hysteresis so the overlay does not flicker between adjacent stages, and layers a heartbeat pulse on top once stamina runs low. FoodBar gains a remainingShare() getter so the tunnel vision service can read a survivor's stamina without coupling to the experience-bar display detail. /tunnelvision lets a designer freeze a stage or run the heartbeat from the lobby to judge the resource pack's glyph sizes without starting a round. The vignette textures themselves ship separately in cygnus-pack; the overlay stays off wherever OverlayProperties reports the pack is not delivered, since without it survivors would stare at an empty box.
…already uses PlayerState and RepeatingTask moved to game/utils in the foundation, and Helper.clamp is gone in favour of Math.clamp; both follow here. clearAll() becomes cleanUp(), which is what the round teardown is called on main (PageProvider, StaminaService, JumpScareManager). The stage count said three different things: MAX_STAGE is 32, the pack ships stage_1 through stage_32, but the command advertised <0-16> and the design spec described sixteen stages at 1024x576. The command now derives its usage line from MAX_STAGE so the two cannot drift apart again, and the spec is corrected to 32 stages at 768x432 - which is what the textures on cygnus-pack's master actually are. The usage line itself moves into Messages as a builder, next to the other builders that interpolate a value, and the players-only message becomes a constant now that CommandSenders takes a finished Component. The recording ScreenOverlay the tests stood up is the foundation's shared one. The comment gating the effect in Cygnus described a vignette font that no longer exists and tied the gate to the resource pack, which OverlayProperties deliberately is not tied to.
The adapter sat as a private method in Cygnus, right after initCommands, which put that method's closing brace inside the region the sibling effect branches also touch: resolving the merge by keeping both sides left remainingStamina unclosed and the registrations after it stranded inside it. It belongs with the other stamina helpers anyway, next to initStaminaObjects and alongside TeamHelper.survivorsOf, both of which answer the same kind of question for a service. Cygnus keeps none of it.
The spec still described the effect as it was first drafted, before the slender half of it became its own feature: - "Intensity" documented a proximity term, a view factor and a combination formula. TunnelVisionIntensity has only fromStamina; what closed the view as the Slender approached is now the gaze glitch, on its own overlay layer. The section says why they are apart rather than pretending they are together. - "Wiring" claimed the service exists only when a resource pack is configured. It is gated by OverlayProperties, which is deliberately not the same question - a player can arrive with the pack already installed, and can decline one a server hands out. - "Failure modes" listed a missing Slender and a Slender in another instance, neither of which this effect reads any more, and had a missing FoodBar reading as an empty bar when it reads as a full one. - "Tests" named assertions about the Slender that no test makes. TunnelVisionStage.update took a parameter called `combined` for a value that is no longer a combination of anything; it is `intensity` now, which is what every constant around it already called it.
Contributor
Test results 264 files 264 suites 1m 47s ⏱️ Results for commit 99b8dd4. |
TheMeinerLP
force-pushed
the
refactor/overlay-foundation
branch
from
August 23, 2026 09:17
af3dead to
e45d219
Compare
TheMeinerLP
force-pushed
the
feat/survivor-tunnel-vision
branch
from
August 23, 2026 09:17
99b8dd4 to
a001eaa
Compare
theEvilReaper
requested changes
Aug 23, 2026
Contributor
There was a problem hiding this comment.
Commands should be removed before merging
Contributor
There was a problem hiding this comment.
Test should be removed before merging
theEvilReaper
force-pushed
the
refactor/overlay-foundation
branch
from
August 23, 2026 19:57
0be1b7e to
f2f377c
Compare
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
Adds the survivor tunnel vision effect: as a survivor's stamina drains, their field of
view narrows, driven by 32 discrete stages with hysteresis (so the overlay doesn't
flicker between adjacent stages) plus a heartbeat pulse layered on top once stamina
runs critically low.
Minecraft 26.2 has no way to switch a post-processing shader on for a single player, so
the effect is delivered as a
camera_overlayrendered onto a head-slot item instead of ashader. The vignette textures that make the overlay look correct ship separately in
cygnus-pack; this branch only wires up the rendering logic and stays off whereverOverlayPropertiesreports the pack was not delivered, since without it survivors wouldotherwise stare at an empty box.
/tunnelvisionlets a designer freeze a single stage or run the heartbeat preview fromthe lobby, so glyph sizes and pulse timing in the resource pack can be judged without
starting a round.
Stack
This PR is based on #177 (
refactor/overlay-foundation) and targets that branch, notmain. It carries only the tunnel vision slice of the original mixedfeat/tunnel-visionbranch — blood splatter and the slender gaze effect are split into sibling PRs.
Important:
Cygnus.javawires all three effects (tunnel vision, blood, gaze) into thesame constructor and
initListener()guard, so this PR conflicts with the sibling bloodand gaze PRs at that file. Whichever of the three merges second will need a rebase to
reconcile the wiring.
Test plan
./gradlew testpasses fully (no pre-existing failures carried over from the mixedbranch — the two failing SlenderVisibility tests were never part of this feature)