fix: coop session creation, schematic cleanup, wire dead generator config

- coop: create() builds a real CoopGenerator session (host + lobby), join() tracks contributions (п.23)
- schematics: removeSchematicBlocks() airs and prunes schematic blocks instead of removeTrailBlocks(0) (п.34)
- config: wire jump-validation into JumpValidator, useSpecialBlocks gates special chances,
  blockLead drives generation lead; remove dead jump-types keys and EffectManager (п.38)
This commit is contained in:
loki5512344 2026-08-06 10:59:30 +02:00
parent fea15e61c8
commit 4c255111f0
Signed by: boba
GPG key ID: 253067914055423B
12 changed files with 151 additions and 301 deletions

View file

@ -95,9 +95,6 @@ public class Option {
public static double MAX_HORIZONTAL_DISTANCE; public static double MAX_HORIZONTAL_DISTANCE;
public static double MAX_VERTICAL_UP; public static double MAX_VERTICAL_UP;
public static double MAX_VERTICAL_DOWN; public static double MAX_VERTICAL_DOWN;
public static boolean JUMP_TYPES_ENABLED;
public static Map<String, Boolean> JUMP_TYPE_ENABLED;
public static Map<String, Double> JUMP_TYPE_CHANCE;
// General // General
public static double BORDER_SIZE; public static double BORDER_SIZE;
@ -154,9 +151,6 @@ public class Option {
MAX_HORIZONTAL_DISTANCE = OptionGeneration.MAX_HORIZONTAL_DISTANCE; MAX_HORIZONTAL_DISTANCE = OptionGeneration.MAX_HORIZONTAL_DISTANCE;
MAX_VERTICAL_UP = OptionGeneration.MAX_VERTICAL_UP; MAX_VERTICAL_UP = OptionGeneration.MAX_VERTICAL_UP;
MAX_VERTICAL_DOWN = OptionGeneration.MAX_VERTICAL_DOWN; MAX_VERTICAL_DOWN = OptionGeneration.MAX_VERTICAL_DOWN;
JUMP_TYPES_ENABLED = OptionGeneration.JUMP_TYPES_ENABLED;
JUMP_TYPE_ENABLED = OptionGeneration.JUMP_TYPE_ENABLED;
JUMP_TYPE_CHANCE = OptionGeneration.JUMP_TYPE_CHANCE;
// General // General
BORDER_SIZE = OptionGeneral.BORDER_SIZE; BORDER_SIZE = OptionGeneral.BORDER_SIZE;

View file

@ -3,11 +3,8 @@ package dev.loki.loparkour.config.options.section;
import dev.loki.loparkour.LoParkour; import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.config.core.Config; import dev.loki.loparkour.config.core.Config;
import java.util.HashMap;
import java.util.Map;
/** /**
* Generation-related configuration options (jump types, validation, memory, ghost mode). * Generation-related configuration options (validation, memory, ghost mode).
*/ */
public class OptionGeneration { public class OptionGeneration {
@ -62,14 +59,8 @@ public class OptionGeneration {
public static double MAX_VERTICAL_UP; public static double MAX_VERTICAL_UP;
public static double MAX_VERTICAL_DOWN; public static double MAX_VERTICAL_DOWN;
// ── Jump types ────────────────────────────────────────────────────────────
public static boolean JUMP_TYPES_ENABLED;
public static Map<String, Boolean> JUMP_TYPE_ENABLED;
public static Map<String, Double> JUMP_TYPE_CHANCE;
public static void init() { public static void init() {
initJumpValidation(); initJumpValidation();
initJumpTypes();
initMemory(); initMemory();
initGhostMode(); initGhostMode();
initGenerationProbabilities(); initGenerationProbabilities();
@ -88,23 +79,6 @@ public class OptionGeneration {
? Config.CONFIG.getDouble("jump-validation.max-vertical-down") : DEFAULT_MAX_VERTICAL_DOWN; ? Config.CONFIG.getDouble("jump-validation.max-vertical-down") : DEFAULT_MAX_VERTICAL_DOWN;
} }
private static void initJumpTypes() {
JUMP_TYPES_ENABLED = Config.CONFIG.isPath("jump-types.enabled")
&& Config.CONFIG.getBoolean("jump-types.enabled");
JUMP_TYPE_ENABLED = new HashMap<>();
JUMP_TYPE_CHANCE = new HashMap<>();
if (Config.CONFIG.isPath("jump-types.types")) {
for (final String type : Config.CONFIG.getChildren("jump-types.types")) {
final String p = "jump-types.types." + type;
JUMP_TYPE_ENABLED.put(type, Config.CONFIG.isPath(p + ".enabled")
&& Config.CONFIG.getBoolean(p + ".enabled"));
JUMP_TYPE_CHANCE.put(type, Config.CONFIG.isPath(p + ".chance")
? Config.CONFIG.getDouble(p + ".chance") : 0.0);
}
}
}
private static void initMemory() { private static void initMemory() {
BLOCK_CLEANUP_DISTANCE = Config.CONFIG.isPath("memory.block-cleanup-distance") BLOCK_CLEANUP_DISTANCE = Config.CONFIG.isPath("memory.block-cleanup-distance")
? Config.CONFIG.getInt("memory.block-cleanup-distance") : DEFAULT_BLOCK_CLEANUP_DISTANCE; ? Config.CONFIG.getInt("memory.block-cleanup-distance") : DEFAULT_BLOCK_CLEANUP_DISTANCE;

View file

@ -127,6 +127,12 @@ public class GeneratorProfileManager {
generator.state.defaultChances.put(ParkourGenerator.BlockGenerationType.SPECIAL, (double) specialChance); generator.state.defaultChances.put(ParkourGenerator.BlockGenerationType.SPECIAL, (double) specialChance);
generator.state.defaultChances.put(ParkourGenerator.BlockGenerationType.SCHEMATIC, (double) schematicChance); generator.state.defaultChances.put(ParkourGenerator.BlockGenerationType.SCHEMATIC, (double) schematicChance);
final boolean specialsEnabled = !generator.profile.settings.containsKey("useSpecialBlocks")
|| generator.profile.get("useSpecialBlocks").asBoolean();
if (!specialsEnabled) {
generator.state.defaultChances.put(ParkourGenerator.BlockGenerationType.SPECIAL, 0.0);
}
normalizeMap(generator.state.defaultChances); normalizeMap(generator.state.defaultChances);
} }

View file

@ -1,95 +0,0 @@
package dev.loki.loparkour.generator.effect;
import dev.loki.loparkour.config.options.Option;
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
import dev.loki.loparkour.player.core.ParkourPlayer;
import dev.loki.loparkour.util.particle.ParticleUtil;
import org.bukkit.Location;
import org.bukkit.block.Block;
import java.util.List;
/**
* Handles particle and sound effects for the parkour generator.
* Extracted from ParkourGenerator to keep it focused on generation logic.
*/
public class EffectManager {
private static final int DOT_PARTICLE_COUNT = 5;
private static final double DOT_PARTICLE_OFFSET = 0.1;
private static final double CIRCLE_CENTER_OFFSET = 0.5;
private static final int CIRCLE_RADIUS = 8;
private static final double BOX_PARTICLE_SPACING = 0.3;
private final ParkourGenerator generator;
public EffectManager(ParkourGenerator generator) {
this.generator = generator;
}
/**
* Spawns particles around the given blocks based on the configured shape.
*/
public void particles(List<Block> blocks) {
if (!generator.profile.get("particles").asBoolean()) {
return;
}
final List<Location> locations = blocks.stream().map(Block::getLocation).toList();
final Location max = locations.stream().reduce((a, b) -> {
final double x = Math.max(a.getX(), b.getX());
final double y = Math.max(a.getY(), b.getY());
final double z = Math.max(a.getZ(), b.getZ());
return new Location(a.getWorld(), x, y, z);
}).orElse(null);
final Location min = locations.stream().reduce((a, b) -> {
final double x = Math.min(a.getX(), b.getX());
final double y = Math.min(a.getY(), b.getY());
final double z = Math.min(a.getZ(), b.getZ());
return new Location(a.getWorld(), x, y, z);
}).orElse(null);
if (max == null || min == null || max.getWorld() == null) {
return;
}
final List<ParkourPlayer> viewers = generator.getPlayers();
switch (Option.PARTICLE_SHAPE) {
case DOT -> {
final Location center = min.clone().add(
(max.getX() - min.getX()) / 2,
(max.getY() - min.getY()) / 2,
(max.getZ() - min.getZ()) / 2
);
viewers.forEach(p -> p.player.spawnParticle(
Option.PARTICLE_TYPE, center, DOT_PARTICLE_COUNT, DOT_PARTICLE_OFFSET, DOT_PARTICLE_OFFSET, DOT_PARTICLE_OFFSET, 0,
Option.PARTICLE_DATA
));
}
case CIRCLE -> ParticleUtil.circle(
min.clone().add(CIRCLE_CENTER_OFFSET, CIRCLE_CENTER_OFFSET, CIRCLE_CENTER_OFFSET), Option.PARTICLE_TYPE, CIRCLE_RADIUS, 1
);
case BOX -> ParticleUtil.box(
org.bukkit.util.BoundingBox.of(min, max), max.getWorld(), Option.PARTICLE_TYPE,
viewers.isEmpty() ? null : viewers.get(0).player, BOX_PARTICLE_SPACING
);
default -> {}
}
}
/**
* Plays the configured sound at the first block's location.
*/
public void sound(List<Block> blocks) {
if (!generator.profile.get("sound").asBoolean() || blocks.isEmpty()) {
return;
}
final Location loc = blocks.get(0).getLocation();
generator.getPlayers().forEach(p ->
p.player.playSound(loc, Option.SOUND_TYPE, Option.SOUND_VOLUME, Option.SOUND_PITCH));
generator.getSpectators().forEach(s ->
s.player.playSound(loc, Option.SOUND_TYPE, Option.SOUND_VOLUME, Option.SOUND_PITCH));
}
}

View file

@ -1,5 +1,6 @@
package dev.loki.loparkour.generator.jump.calculation; package dev.loki.loparkour.generator.jump.calculation;
import dev.loki.loparkour.config.options.Option;
import dev.loki.loparkour.util.misc.MaterialUtil; import dev.loki.loparkour.util.misc.MaterialUtil;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
@ -22,7 +23,12 @@ public class JumpValidator {
private final double maxVerticalDown; private final double maxVerticalDown;
public JumpValidator() { public JumpValidator() {
this(MAX_JUMP_DISTANCE, MAX_HORIZONTAL_DISTANCE, MAX_VERTICAL_UP, MAX_VERTICAL_DOWN); this(
Option.JUMP_VALIDATION_ENABLED ? Option.MAX_JUMP_DISTANCE : MAX_JUMP_DISTANCE,
Option.JUMP_VALIDATION_ENABLED ? Option.MAX_HORIZONTAL_DISTANCE : MAX_HORIZONTAL_DISTANCE,
Option.JUMP_VALIDATION_ENABLED ? Option.MAX_VERTICAL_UP : MAX_VERTICAL_UP,
Option.JUMP_VALIDATION_ENABLED ? Option.MAX_VERTICAL_DOWN : MAX_VERTICAL_DOWN
);
} }
public JumpValidator(double maxDistance, double maxHorizontal, double maxVerticalUp, double maxVerticalDown) { public JumpValidator(double maxDistance, double maxHorizontal, double maxVerticalUp, double maxVerticalDown) {

View file

@ -93,12 +93,17 @@ public class LifecycleTickManager {
private void maintainBlockLead() { private void maintainBlockLead() {
final int currentBlocks = generator.state.generatedBlocks; final int currentBlocks = generator.state.generatedBlocks;
final int targetBlocks = generator.state.score + GENERATION_LEAD; final int targetBlocks = generator.state.score + generationLead();
if (currentBlocks < targetBlocks) { if (currentBlocks < targetBlocks) {
final int needed = targetBlocks - currentBlocks; final int needed = targetBlocks - currentBlocks;
// Cap generation per tick to prevent runaway generation in multiplayer // Cap generation per tick to prevent runaway generation in multiplayer
generator.generate(Math.min(needed, MAX_GENERATE_PER_TICK)); generator.generate(Math.min(needed, MAX_GENERATE_PER_TICK));
} }
} }
private int generationLead() {
final int lead = generator.profile.get("blockLead").asInt();
return lead > 0 ? lead : GENERATION_LEAD;
}
} }

View file

@ -9,7 +9,9 @@ import org.bukkit.block.Block;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
/** /**
* Handles cleanup operations for parkour generator. * Handles cleanup operations for parkour generator.
@ -91,26 +93,28 @@ public class GeneratorCleanup {
} }
/** /**
* Remove trail blocks behind player at specified index. * Remove schematic blocks once the player completes them.
* The end block is kept in history so the landing player can score it.
*/ */
public void removeTrailBlocks(int startIndex) { public void removeSchematicBlocks() {
if (startIndex < 0 || startIndex >= generator.state.history.size()) { final List<Block> schematicBlocks = generator.state.schematicBlocks;
if (schematicBlocks == null || schematicBlocks.isEmpty()) {
return; return;
} }
final int endIndex = Math.min(startIndex + Option.BLOCK_CLEANUP_DISTANCE, generator.state.history.size()); final Set<Block> remove = new HashSet<>();
for (int i = 0; i < schematicBlocks.size() - 1; i++) {
for (int i = startIndex; i < endIndex; i++) { final Block block = schematicBlocks.get(i);
final Block block = generator.state.history.get(i); if (block != null) {
if (block != null && block.getType() != Material.AIR) { if (block.getType() != Material.AIR) {
block.setType(Material.AIR); block.setType(Material.AIR);
}
remove.add(block);
} }
} }
// Remove from history (synchronized to prevent concurrent index shift) generator.state.history.removeIf(remove::contains);
synchronized (generator.state.history) { generator.state.schematicBlocks = null;
generator.state.history.subList(startIndex, endIndex).clear();
}
} }
/** /**

View file

@ -112,7 +112,7 @@ public class PlayerInteractionHandler {
private void handleBlockInteraction(@NotNull ParkourPlayer player, @NotNull Block block) { private void handleBlockInteraction(@NotNull ParkourPlayer player, @NotNull Block block) {
// Check if it's a schematic end block // Check if it's a schematic end block
if (isSchematicEndBlock(block)) { if (isSchematicEndBlock(block)) {
handleSchematicEndBlock(block); handleSchematicEndBlock();
} else if (isOnSchematicBlock(block)) { } else if (isOnSchematicBlock(block)) {
// Schematic block that is NOT the last — skip scoring // Schematic block that is NOT the last — skip scoring
return; return;
@ -131,13 +131,9 @@ public class PlayerInteractionHandler {
&& historyContains(generator.state.schematicBlocks, block); && historyContains(generator.state.schematicBlocks, block);
} }
private void handleSchematicEndBlock(@NotNull Block block) { private void handleSchematicEndBlock() {
// Remove schematic blocks and continue with normal generation cleanup.removeSchematicBlocks();
if (generator.state.schematicBlocks != null) {
cleanup.removeTrailBlocks(0); // Remove schematic trail
generator.state.schematicBlocks = null;
}
// Continue normal generation // Continue normal generation
generator.generate(GENERATION_LEAD); generator.generate(GENERATION_LEAD);
} }

View file

@ -0,0 +1,89 @@
package dev.loki.loparkour.mode.impl.multi;
import dev.loki.loparkour.config.locale.Locales;
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
import dev.loki.loparkour.mode.base.Mode;
import dev.loki.loparkour.mode.base.Modes;
import dev.loki.loparkour.player.core.ParkourPlayer;
import dev.loki.loparkour.session.core.Session;
import dev.loki.loparkour.util.text.ColorUtil;
import org.bukkit.Sound;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Co-op generator — all players share one score; per-UUID block
* contributions are tracked for the fall() stats message.
*/
class CoopGenerator extends ParkourGenerator {
private static final int MILESTONE_INTERVAL = 50;
private static final int TITLE_FADE_IN = 5;
private static final int TITLE_STAY = 40;
private static final int TITLE_FADE_OUT = 10;
private static final float MILESTONE_VOLUME = 0.6f;
private static final float MILESTONE_PITCH = 1.5f;
private static final int PERCENT_MULTIPLIER = 100;
private final Map<UUID, Integer> contributions = new HashMap<>();
CoopGenerator(@NotNull Session session) {
super(session);
session.getPlayers().forEach(pp -> contributions.put(pp.getUUID(), 0));
}
@Override
protected void score() {
super.score();
for (final ParkourPlayer pp : getPlayers()) {
contributions.compute(pp.getUUID(), (uuid, count) -> (count == null ? 0 : count) + 1);
}
if (state.score % MILESTONE_INTERVAL == 0) {
getPlayers().forEach(pp -> {
pp.player.sendTitle(
ColorUtil.color(Locales.getString(pp.locale, "modes.coop.milestone_title").formatted(state.score)),
ColorUtil.color(Locales.getString(pp.locale, "modes.coop.milestone_subtitle")),
TITLE_FADE_IN, TITLE_STAY, TITLE_FADE_OUT);
pp.player.playSound(pp.getLocation(),
Sound.ENTITY_PLAYER_LEVELUP, MILESTONE_VOLUME, MILESTONE_PITCH);
});
}
}
/**
* Registers a player joining mid-game so their blocks count in fall() stats.
*/
void onPlayerJoin(ParkourPlayer pp) {
contributions.putIfAbsent(pp.getUUID(), 0);
}
@Override
public void fall() {
getPlayers().forEach(pp -> {
final int contrib = contributions.getOrDefault(pp.getUUID(), 0);
final int pct = state.score > 0 ? (contrib * PERCENT_MULTIPLIER / state.score) : 0;
pp.sendTranslated("modes.coop.stats",
Integer.toString(state.score),
Integer.toString(contrib),
Integer.toString(pct));
});
super.fall();
}
@Override
public void reset(boolean regenerate) {
super.reset(regenerate);
contributions.clear();
getPlayers().forEach(pp -> contributions.put(pp.getUUID(), 0));
}
@Override
public Mode getMode() {
return Modes.COOP;
}
}

View file

@ -1,44 +1,23 @@
package dev.loki.loparkour.mode.impl.multi; package dev.loki.loparkour.mode.impl.multi;
import dev.loki.loparkour.mode.base.MultiMode;
import dev.loki.loparkour.mode.base.ModeMessages;
import dev.loki.loparkour.mode.base.Mode;
import dev.loki.loparkour.mode.base.Modes;
import dev.loki.loparkour.config.core.Config; import dev.loki.loparkour.config.core.Config;
import dev.loki.loparkour.config.locale.Locales;
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
import dev.loki.loparkour.util.text.ColorUtil;
import dev.loki.loparkour.leaderboard.core.Leaderboard; import dev.loki.loparkour.leaderboard.core.Leaderboard;
import dev.loki.loparkour.menu.core.Menus; import dev.loki.loparkour.menu.core.Menus;
import dev.loki.loparkour.mode.base.ModeMessages;
import dev.loki.loparkour.mode.base.MultiMode;
import dev.loki.loparkour.player.core.ParkourPlayer; import dev.loki.loparkour.player.core.ParkourPlayer;
import dev.loki.loparkour.player.core.ParkourUser; import dev.loki.loparkour.player.core.ParkourUser;
import dev.loki.loparkour.session.core.Session; import dev.loki.loparkour.session.core.Session;
import org.bukkit.Sound;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/** /**
* Co-op Mode — multiple players share one session and one score. * Co-op Mode — multiple players share one session and one score.
* *
* <p>Implements {@link MultiMode} so it is excluded from the single-player * <p>Implements {@link MultiMode} so it is excluded from the single-player
* mode selection menu in {@link dev.loki.loparkour.menu.play.SingleMenu}. * mode selection menu in {@link dev.loki.loparkour.menu.play.SingleMenu}.
* Players join via /parkour coop or through a lobby invite flow.</p> * Players join via /parkour coop or through a lobby invite flow.</p>
*
* <p>Problems fixed vs previous version:
* <ul>
* <li>Did NOT implement MultiMode → showed up in single-player menu</li>
* <li>CoopGenerator was a private inner class — inaccessible from join/leave</li>
* <li>sharedScore was a separate field that duplicated state.score</li>
* <li>fall() called super.fall() THEN tried to clear state that was already reset</li>
* <li>playerContributions never cleared between falls correctly</li>
* <li>create(Player) silently created a solo session instead of rejecting</li>
* </ul>
*/ */
public class CoopMode implements MultiMode { public class CoopMode implements MultiMode {
@ -50,10 +29,6 @@ public class CoopMode implements MultiMode {
return "coop"; return "coop";
} }
/**
* Returns null so this mode never appears in any menu item list.
* Players join via lobby invite or command only.
*/
@Override @Override
@Nullable @Nullable
public dev.loki.loparkour.util.item.Item getItem(String locale) { public dev.loki.loparkour.util.item.Item getItem(String locale) {
@ -67,23 +42,28 @@ public class CoopMode implements MultiMode {
} }
/** /**
* Called when a player tries to start coop solo from a command/menu. * Creates a coop session with the caller as host and opens the lobby so
* Opens the lobby menu so they can invite others — does NOT create a session here. * the host can invite others. Later joiners are added via {@link #join}.
* A session is created only once a second player joins via {@link #join}.
*/ */
@Override @Override
public void create(Player player) { public void create(Player player) {
if (!ModeMessages.checkJoiningEnabled(player)) { if (!ModeMessages.checkJoiningEnabled(player)) {
return; return;
} }
// Opening lobby menu lets player invite others — actual session created in join()
final ParkourPlayer pp = ParkourPlayer.getPlayer(player);
if (pp != null && pp.session.generator instanceof CoopGenerator) {
return;
}
player.closeInventory();
Session.create(CoopGenerator::new,
session -> session.getPlayers().size() < getMaxPlayers(),
null,
player);
Menus.LOBBY.open(player); Menus.LOBBY.open(player);
} }
/**
* Adds a player to an existing coop session, or creates a new session if
* the target player is not yet in one.
*/
@Override @Override
public void join(Player joiningPlayer, Session session) { public void join(Player joiningPlayer, Session session) {
if (!ModeMessages.checkJoiningEnabled(joiningPlayer)) { if (!ModeMessages.checkJoiningEnabled(joiningPlayer)) {
@ -94,20 +74,17 @@ public class CoopMode implements MultiMode {
return; return;
} }
// Register the joining player into the existing session
final ParkourPlayer pp = ParkourUser.register(joiningPlayer, session); final ParkourPlayer pp = ParkourUser.register(joiningPlayer, session);
session.addPlayers(pp); session.addPlayers(pp);
pp.updateGeneratorSettings(session.generator); pp.updateGeneratorSettings(session.generator);
if (session.generator instanceof CoopGenerator coopGenerator) {
coopGenerator.onPlayerJoin(pp);
}
// Inform everyone
session.getPlayers().forEach(p -> session.getPlayers().forEach(p ->
p.sendTranslated("modes.coop.player_joined", joiningPlayer.getName())); p.sendTranslated("modes.coop.player_joined", joiningPlayer.getName()));
} }
/**
* Removes a player from a coop session.
* If the last player leaves, the session ends normally.
*/
@Override @Override
public void leave(Player player, Session session) { public void leave(Player player, Session session) {
final ParkourPlayer pp = ParkourPlayer.getPlayer(player); final ParkourPlayer pp = ParkourPlayer.getPlayer(player);
@ -121,79 +98,4 @@ public class CoopMode implements MultiMode {
public int getMaxPlayers() { public int getMaxPlayers() {
return Config.CONFIG.getInt("modes.coop.max-players"); return Config.CONFIG.getInt("modes.coop.max-players");
} }
/** Package-accessible so Session.create() lambda and join() can use it. */
static class CoopGenerator extends ParkourGenerator {
private static final int MILESTONE_INTERVAL = 50;
private static final int TITLE_FADE_IN = 5;
private static final int TITLE_STAY = 40;
private static final int TITLE_FADE_OUT = 10;
private static final float MILESTONE_VOLUME = 0.6f;
private static final float MILESTONE_PITCH = 1.5f;
private static final int PERCENT_MULTIPLIER = 100;
/** Individual block contributions per player UUID. */
private final Map<UUID, Integer> contributions = new HashMap<>();
CoopGenerator(@NotNull Session session) {
super(session);
session.getPlayers().forEach(pp -> contributions.put(pp.getUUID(), 0));
}
@Override
protected void score() {
super.score(); // increments state.score and totalScore
// Track contributions for all players in session
for (final ParkourPlayer pp : getPlayers()) {
contributions.compute(pp.getUUID(), (uuid, count) -> (count == null ? 0 : count) + 1);
}
// Milestone every 50 points
if (state.score % MILESTONE_INTERVAL == 0) {
getPlayers().forEach(pp -> {
pp.player.sendTitle(
ColorUtil.color(Locales.getString(pp.locale, "modes.coop.milestone_title").formatted(state.score)),
ColorUtil.color(Locales.getString(pp.locale, "modes.coop.milestone_subtitle")),
TITLE_FADE_IN, TITLE_STAY, TITLE_FADE_OUT);
pp.player.playSound(pp.getLocation(),
Sound.ENTITY_PLAYER_LEVELUP, MILESTONE_VOLUME, MILESTONE_PITCH);
});
}
}
/**
* When a new player joins mid-game, add them to contributions map.
*/
public void onPlayerJoin(ParkourPlayer pp) {
contributions.putIfAbsent(pp.getUUID(), 0);
}
@Override
public void fall() {
// Show each player their individual contribution before reset
getPlayers().forEach(pp -> {
final int contrib = contributions.getOrDefault(pp.getUUID(), 0);
final int pct = state.score > 0 ? (contrib * PERCENT_MULTIPLIER / state.score) : 0;
pp.sendTranslated("modes.coop.stats",
Integer.toString(state.score),
Integer.toString(contrib),
Integer.toString(pct));
});
super.fall(); // handles reset, leaderboard, etc.
}
@Override
public void reset(boolean regenerate) {
super.reset(regenerate);
contributions.clear();
getPlayers().forEach(pp -> contributions.put(pp.getUUID(), 0));
}
@Override
public Mode getMode() {
return Modes.COOP;
}
}
} }

View file

@ -150,26 +150,6 @@ jump-validation:
max-vertical-up: 1.25 max-vertical-up: 1.25
max-vertical-down: 3.0 max-vertical-down: 3.0
# -= Jump types options =-
jump-types:
enabled: false
types:
neo-jump:
enabled: true
chance: 0.05
head-hitter:
enabled: true
chance: 0.08
fence-jump:
enabled: true
chance: 0.10
trapdoor-jump:
enabled: true
chance: 0.12
ladder-jump:
enabled: true
chance: 0.07
# -= Memory optimization =- # -= Memory optimization =-
memory: memory:
block-cleanup-distance: 20 block-cleanup-distance: 20

View file

@ -34,17 +34,6 @@ generation:
pane: 10 pane: 10
fence: 10 fence: 10
# -= Jump type variations =-
# Chances for different jump types within normal blocks
# These add variety to the parkour generation
jump-types:
normal: 70
neo: 5
head-hitter: 5
fence: 10
trapdoor: 5
ladder: 5
# -= Jump settings =- # -= Jump settings =-
settings: settings: