Skip to main content
In Soul Link mode, all players share a single life pool. When any player’s death would reduce shared health to zero, the entire run ends for everyone.

Death detection

Death is detected through the shared health system. When health reaches zero, RunManager.triggerGameOver() is called:
SharedStatsHandler.java:173
if (sharedHealth <= 0) {
    SoulLink.LOGGER.info("Shared health depleted - triggering game over");
    runManager.triggerGameOver();
    return;
}
This happens in two scenarios:

Direct damage

When a player takes damage that reduces shared health to zero:
SharedStatsHandler.java:139
public static void onPlayerHealthChanged(ServerPlayerEntity damagedPlayer, float newHealth,
        DamageSource damageSource) {
    // ...
    sharedHealth = MathHelper.clamp(newHealth, 0.0f, getMaxHealth());
    
    if (sharedHealth <= 0) {
        SoulLink.LOGGER.info("Shared health depleted - triggering game over");
        runManager.triggerGameOver();
        return;
    }
}

Periodic damage accumulation

When poison/wither damage accumulator reduces health to zero:
SharedStatsHandler.java:299
if (sharedHealth <= 0) {
    runManager.triggerGameOver();
}

Game over sequence

When triggered, the game over sequence executes:

1. State transition

RunManager.java:445
public synchronized void triggerGameOver() {
    if (gameState != RunState.RUNNING) {
        return;
    }
    
    SoulLink.LOGGER.info("Game Over triggered!");
    
    timerService.stop();
    gameState = RunState.GAMEOVER;
}
The synchronized method prevents race conditions from multiple simultaneous deaths.

2. Cleanup teams

In Manhunt mode, teams are removed:
RunManager.java:455
ManhuntManager.getInstance().cleanupTeams(server);
CompassTrackingHandler.reset();

3. Visual effects

Each player receives visual and audio feedback:
RunManager.java:460
for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) {
    if (isInRun(player)) {
        player.changeGameMode(GameMode.SPECTATOR);
        player.getInventory().clear();
        
        ServerWorld world = getPlayerWorld(player);
        if (world != null) {
            world.playSound(null, player.getX(), player.getY(), player.getZ(),
                    SoundEvents.ENTITY_WITHER_DEATH, SoundCategory.PLAYERS, 0.5f, 0.8f);
        }
        
        player.networkHandler.sendPacket(
            new TitleS2CPacket(
                Text.literal("GAME OVER")
                    .formatted(Formatting.RED, Formatting.BOLD)));
        
        player.networkHandler.sendPacket(
            new SubtitleS2CPacket(
                Text.literal(finalTime).formatted(Formatting.WHITE)));
    }
}
Effects applied:
  • Changed to spectator mode
  • Inventory cleared
  • Wither death sound played
  • “GAME OVER” title displayed in red
  • Final time shown as subtitle

4. Chat notification

A clickable restart message is broadcast:
RunManager.java:479
Text restartMessage = Text.empty()
    .append(getPrefix())
    .append(Text.literal("All players are dead. Click ").formatted(Formatting.GRAY))
    .append(Text.literal("here")
        .setStyle(Style.EMPTY
            .withColor(Formatting.BLUE)
            .withUnderline(true)
            .withClickEvent(new ClickEvent.RunCommand("/start"))
            .withHoverEvent(new HoverEvent.ShowText(
                Text.literal("Start a new attempt").formatted(Formatting.GRAY)))))
    .append(Text.literal(" or use ").formatted(Formatting.GRAY))
    .append(Text.literal("/start").formatted(Formatting.GOLD))
    .append(Text.literal(" to start a new attempt.").formatted(Formatting.GRAY));

server.getPlayerManager().broadcast(restartMessage, false);
[SoulLink] All players are dead. Click here or use /start to start a new attempt.

Manhunt mode differences

In Manhunt mode, the death system works differently:

Runner death

When a Runner dies, only Runners share the death:
SharedStatsHandler.java:60
private static boolean shouldParticipateInSoulLink(ServerPlayerEntity player) {
    RunManager runManager = RunManager.getInstance();
    if (!runManager.isRunActive()) return false;
    
    if (!Settings.getInstance().isManhuntMode()) return true;
    
    return ManhuntManager.getInstance().isSpeedrunner(player);
}
Hunters continue playing with vanilla health mechanics.

Hunter death

Hunters respawn at spawn with full health:
// Hunter respawn logic in ServerPlayerEntityMixin
if (ManhuntManager.getInstance().isHunter(player)) {
    BlockPos spawnPos = RunManager.getInstance().getSpawnPos();
    player.teleport(overworld, spawnPos.getX(), spawnPos.getY(), spawnPos.getZ(),
        Set.of(), 0, 0, true);
}
Hunters have unlimited lives. Only Runner death ends the run.

Victory condition

The run also ends (successfully) when the Ender Dragon is defeated:
RunManager.java:495
public synchronized void triggerVictory() {
    if (gameState != RunState.RUNNING) {
        return;
    }
    
    SoulLink.LOGGER.info("Victory! Dragon defeated!");
    
    timerService.stop();
    gameState = RunState.GAMEOVER;
}
Victory uses similar cleanup but different visual effects:
RunManager.java:510
for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) {
    world.playSound(null, player.getX(), player.getY(), player.getZ(),
            SoundEvents.UI_TOAST_CHALLENGE_COMPLETE, SoundCategory.PLAYERS, 1.0f, 1.0f);
    
    player.networkHandler.sendPacket(
        new TitleS2CPacket(
            Text.literal("VICTORY")
                .formatted(Formatting.GOLD, Formatting.BOLD)));
    
    player.networkHandler.sendPacket(
        new SubtitleS2CPacket(
            Text.literal(finalTime).formatted(Formatting.WHITE)));
}

State diagram

The GAMEOVER state persists until /start is used to begin a new run.

Thread safety

Both triggerGameOver() and triggerVictory() are synchronized:
RunManager.java:445
public synchronized void triggerGameOver() {
    if (gameState != RunState.RUNNING) {
        return;
    }
    // ...
}
This prevents:
  • Multiple simultaneous deaths from triggering game over twice
  • Death and victory happening at the exact same time
  • Race conditions from async damage events

Why one death ends the run?

The core Soul Link mechanic creates high-stakes cooperation:

Team coordination

Players must work together and watch each other’s backs. No lone-wolfing.

Risk management

Dangerous situations affect everyone. The team must decide together when to take risks.

Shared responsibility

Every player’s mistakes impact the entire team. Everyone must play carefully.

Tension

The constant threat of instant game over creates dramatic, memorable moments.
This is what separates Soul Link from traditional co-op: you truly are linked together.

Build docs developers (and LLMs) love