Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion game/src/main/java/net/onelitefeather/cygnus/Cygnus.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
import net.onelitefeather.cygnus.telemetry.CygnusAttributes;
import net.onelitefeather.cygnus.telemetry.TraceCookie;
import net.onelitefeather.cygnus.telemetry.TracingCreekWitness;
import net.onelitefeather.cygnus.creek.tab.HuntedTabWitness;
import net.onelitefeather.cygnus.common.map.GameMap;
import net.minestom.server.entity.Player;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -231,7 +232,8 @@ public Cygnus() {
// either way, and it needs neither the resource pack nor the overlay gate to be heard.
new DamageSoundService(this.gameConfig.damageSound(), System::currentTimeMillis),
new CreekModule(this.gameConfig.creek(), this.teamService, this.mapProvider,
sanityService, new TracingCreekWitness(sanityService, this.actionTracer, Cygnus::survivorActor), this.jumpscareManager, this.staminaService, tickSections),
sanityService, new HuntedTabWitness(new TracingCreekWitness(sanityService, this.actionTracer, Cygnus::survivorActor),
MinecraftServer.getConnectionManager()::getOnlinePlayerByUuid), this.jumpscareManager, this.staminaService, tickSections),
sanityService,
new AdrenalineService(
this.gameConfig.adrenaline(),
Expand Down
27 changes: 26 additions & 1 deletion game/src/main/java/net/onelitefeather/cygnus/creek/Creek.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@
import net.onelitefeather.cygnus.creek.state.CreekActions;
import net.onelitefeather.cygnus.creek.state.CreekContext;
import net.onelitefeather.cygnus.creek.state.CreekState;
import net.onelitefeather.cygnus.creek.state.HuntState;
import net.onelitefeather.cygnus.creek.state.StalkState;
import net.onelitefeather.cygnus.creek.state.SurvivorView;
import net.onelitefeather.cygnus.creek.state.VanishState;
import net.onelitefeather.cygnus.creek.world.RouteProvider;

import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;

Expand All @@ -26,7 +30,7 @@
* </p>
*
* @author theEvilReaper
* @version 1.0.0
* @version 1.1.0
* @since 2.15.0
*/
final class Creek {
Expand All @@ -44,6 +48,7 @@ final class Creek {
private List<SurvivorView> lastViews = List.of();
private Set<UUID> seenLastStep = Set.of();
private boolean entered;
private @Nullable UUID hunting;

/**
* Sets up a creek.
Expand Down Expand Up @@ -87,9 +92,12 @@ void tick(SurvivorSnapshot survivors, Set<UUID> ignored, long now) {
if (!this.entered) {
this.state.enter(ctx);
this.entered = true;
// A hunt the creek starts in may be over within this very step.
this.syncHunt();
}
CreekState next = this.state.tick(ctx);
if (next != this.state) this.switchTo(next, ctx);
this.syncHunt();
// Reported after the step, so a stalk that just ended or turned into a hunt is not counted.
if (this.state instanceof StalkState stalk) this.round.witness().stalked(stalk.target());
}
Expand All @@ -106,6 +114,20 @@ void vanishForGood(SurvivorSnapshot survivors, long now) {
// The state it had so far is never entered now, and never needs to be.
this.entered = true;
this.switchTo(VanishState.forever(), ctx);
this.syncHunt();
}

/**
* Tells the witness when the survivor this creek hunts changes: a hunt that starts, one that
* ends, or a switch to another target. Called after every step, so each hunt is reported once.
*/
private void syncHunt() {
UUID now = this.state instanceof HuntState hunt ? hunt.target() : null;
UUID before = this.hunting;
if (Objects.equals(now, before)) return;
this.hunting = now;
if (before != null) this.round.witness().huntEnded(before);
if (now != null) this.round.witness().hunted(now);
}

/**
Expand Down Expand Up @@ -195,6 +217,9 @@ RouteProvider route() {
}

void remove() {
UUID before = this.hunting;
this.hunting = null;
if (before != null) this.round.witness().huntEnded(before);
this.body.remove();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* </p>
*
* @author theEvilReaper
* @version 1.0.0
* @version 1.1.0
* @since 2.16.0
*/
public interface CreekWitness {
Expand Down Expand Up @@ -68,4 +68,26 @@ public void selected(UUID survivor) {
* @param survivor the survivor who was picked out
*/
void selected(UUID survivor);

/**
* A creek has started to hunt a survivor, the chase that ends in a catch. Called once, when the
* creek takes the survivor as its target. A stalk is not a hunt: it only shadows the survivor.
*
* @param survivor the survivor being hunted
* @since 2.15.0
*/
default void hunted(UUID survivor) {
// Nobody listens by default.
}

/**
* A creek stopped hunting a survivor, however the hunt ended: a catch, time running out, the
* survivor being gone, the creek being sent away or removed. Called once per {@link #hunted(UUID)}.
*
* @param survivor the survivor who is no longer hunted
* @since 2.15.0
*/
default void huntEnded(UUID survivor) {
// Nobody listens by default.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package net.onelitefeather.cygnus.creek.tab;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.server.entity.Player;
import net.onelitefeather.cygnus.creek.dread.CreekWitness;
import org.jetbrains.annotations.Nullable;

import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

/**
* Marks the survivors a creek is hunting in the tab list, and takes the mark away when the hunt is over.
* <p>
* It decorates the {@link CreekWitness}, so the creek needs no knowledge of the tab list. The mark is
* composed on top of whatever display name the survivor carries (role icon, name color) instead of
* replacing it: the name turns {@link NamedTextColor#RED} and a {@value #MARKER} marker goes behind it, after a space.
* Glyphs from the icon font keep their own color.
* </p>
* <p>
* The display name from before is kept and put back exactly, but only if the survivor still shows the
* marked name. If anything else renamed them in the meantime (a spectator's struck-through name, the
* rank tag of the restart lobby), that newer name wins and nothing is restored over it. Two creeks on
* one survivor are counted, so the mark stays until the last hunt ends.
* </p>
* <p>
* The display name is global, so every player sees the mark in the tab list, not only the hunted one.
* </p>
*
* @author TheMeinerLP
* @version 1.1.0
* @since 2.15.0
*/
public final class HuntedTabWitness implements CreekWitness {

/** The marker behind a hunted survivor's name. */
public static final String MARKER = "◆";

private final CreekWitness delegate;
private final Function<UUID, @Nullable Player> players;
private final Map<UUID, Mark> marks = new ConcurrentHashMap<>();

/**
* Creates the decorator.
*
* @param delegate the witness that still has to hear everything
* @param players finds an online player by id, or {@code null} if they are gone
*/
public HuntedTabWitness(CreekWitness delegate, Function<UUID, @Nullable Player> players) {
this.delegate = delegate;
this.players = players;
}

/**
* Builds the marked version of a display name.
*
* @param base the name as it is shown without the mark
* @return the red name with the marker behind it
*/
public static Component highlight(Component base) {
return Component.text()
.color(NamedTextColor.RED)
.append(recolor(base))
.append(Component.text(" " + MARKER, NamedTextColor.DARK_RED))
.build();
}

/**
* Turns every colored text red, except the icon glyphs, which are full-color bitmaps.
*/
private static Component recolor(Component component) {
Component out = component;
if (component.font() == null && component.color() != null) out = out.color(NamedTextColor.RED);
List<Component> children = component.children().stream().map(HuntedTabWitness::recolor).toList();
return out.children(children);
}

@Override
public void hunted(UUID survivor) {
this.delegate.hunted(survivor);
Player player = this.players.apply(survivor);
if (player == null) return;
this.marks.compute(survivor, (_, mark) -> {
if (mark != null) return mark.another();
Component base = player.getDisplayName();
Component shown = highlight(base != null ? base : Component.text(player.getUsername()));
player.setDisplayName(shown);
return new Mark(base, shown, 1);
});
}

@Override
public void huntEnded(UUID survivor) {
this.delegate.huntEnded(survivor);
this.marks.computeIfPresent(survivor, (_, mark) -> {
if (mark.hunts() > 1) return mark.fewer();
Player player = this.players.apply(survivor);
if (player != null && mark.shown().equals(player.getDisplayName())) {
player.setDisplayName(mark.base());
}
return null;
});
}

@Override
public void sighted(UUID survivor) {
this.delegate.sighted(survivor);
}

@Override
public void caught(UUID survivor) {
this.delegate.caught(survivor);
}

@Override
public void stalked(UUID survivor) {
this.delegate.stalked(survivor);
}

@Override
public void selected(UUID survivor) {
this.delegate.selected(survivor);
}

/**
* A survivor's name before the mark, the marked name, and how many creeks hunt them.
*/
private record Mark(@Nullable Component base, Component shown, int hunts) {
Mark another() {
return new Mark(this.base, this.shown, this.hunts + 1);
}

Mark fewer() {
return new Mark(this.base, this.shown, this.hunts - 1);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* What the creek's hunt looks like in the tab list.
*/
@NotNullByDefault
package net.onelitefeather.cygnus.creek.tab;

import org.jetbrains.annotations.NotNullByDefault;
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
* </p>
*
* @author TheMeinerLP
* @version 1.0.0
* @version 1.1.0
* @since 2.15.0
*/
public final class TracingCreekWitness implements CreekWitness {
Expand Down Expand Up @@ -76,6 +76,16 @@ public void selected(UUID survivor) {
this.delegate.selected(survivor);
}

@Override
public void hunted(UUID survivor) {
this.delegate.hunted(survivor);
}

@Override
public void huntEnded(UUID survivor) {
this.delegate.huntEnded(survivor);
}

/**
* A tracing failure must never reach the creek, which is the part of the game that is being
* observed.
Expand Down
Loading
Loading