Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
079555900c |
No files matched your search
@@ -110,6 +110,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
private games.dmg.fantasystick.persistence.HealthStealSettings healthStealSettings;
|
||||
private games.dmg.fantasystick.passive.HealthSteal healthSteal;
|
||||
private games.dmg.fantasystick.passive.SpeedI speedI;
|
||||
private games.dmg.fantasystick.passive.LowLightVision lowLightVision;
|
||||
private games.dmg.fantasystick.passive.PassiveRegistry passiveRegistry;
|
||||
private games.dmg.fantasystick.passive.PassiveEngine passiveEngine;
|
||||
private java.util.concurrent.CompletionStage<Void> configurationDrain = java.util.concurrent.CompletableFuture.completedStage(null);
|
||||
@@ -366,11 +367,13 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
event -> getServer().getPluginManager().callEvent(event), games.dmg.fantasystick.session.NativeHealthLimit::read,
|
||||
problem -> getLogger().log(Level.WARNING, "Health Steal failed for character " + problem.character(), problem.cause()));
|
||||
speedI = new games.dmg.fantasystick.passive.SpeedI(sessions);
|
||||
lowLightVision = new games.dmg.fantasystick.passive.LowLightVision(sessions);
|
||||
passiveRegistry = new games.dmg.fantasystick.passive.PassiveRegistry();
|
||||
passiveRegistry.register(quickerRegeneration.definition());
|
||||
passiveRegistry.register(quickerRegenerationII.definition());
|
||||
passiveRegistry.register(healthSteal.definition());
|
||||
passiveRegistry.register(speedI.definition());
|
||||
passiveRegistry.register(lowLightVision.definition());
|
||||
passiveEngine = new games.dmg.fantasystick.passive.PassiveEngine(passiveRegistry, characters, sessions, nextTick,
|
||||
problem -> getLogger().log(Level.WARNING, "Passive change failed for character " + problem.character()
|
||||
+ "; passive " + problem.passive(), problem.cause()));
|
||||
@@ -482,6 +485,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
getServer().getPluginManager().registerEvents(outOfCombatRecovery, this);
|
||||
outOfCombatRecovery.install(this);
|
||||
speedI.install(this);
|
||||
lowLightVision.install(this);
|
||||
getServer().getPluginManager().registerEvents(books, this);
|
||||
getServer().getPluginManager().registerEvents(gameModes, this);
|
||||
getServer().getPluginManager().registerEvents(sheetResourcePack, this);
|
||||
@@ -587,6 +591,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
var closingHealthStealSettings = healthStealSettings; healthStealSettings = null;
|
||||
var closingHealthSteal = healthSteal; healthSteal = null;
|
||||
var closingSpeedI = speedI; speedI = null;
|
||||
var closingLowLightVision = lowLightVision; lowLightVision = null;
|
||||
var closingPassiveEngine = passiveEngine; passiveEngine = null;
|
||||
quickerRegeneration = null; quickerRegenerationII = null; passiveRegistry = null;
|
||||
// Retire client resources independently of successful ordinary-equipment restoration.
|
||||
@@ -707,6 +712,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
if (closingPassiveEngine != null) cleanup("passive engine", closingPassiveEngine::close);
|
||||
if (closingHealthSteal != null) cleanup("Health Steal", closingHealthSteal::close);
|
||||
if (closingSpeedI != null) cleanup("Speed I", closingSpeedI::close);
|
||||
if (closingLowLightVision != null) cleanup("Low-Light Vision", closingLowLightVision::close);
|
||||
if (closingVisuals != null) cleanup("session particles", closingVisuals::close);
|
||||
if (closingParties != null) cleanup("parties", closingParties::close);
|
||||
if (closingPartyCombat != null) cleanup("party combat", closingPartyCombat::close);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package games.dmg.fantasystick.passive;
|
||||
|
||||
import games.dmg.fantasystick.progression.Stat;
|
||||
import games.dmg.fantasystick.session.EquipmentSessions;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
|
||||
/** Current-session native Night Vision I layer for the learned character passive. */
|
||||
public final class LowLightVision implements AutoCloseable {
|
||||
public static final String ID = "low_light_vision";
|
||||
private final PersistentPotionEffect effect;
|
||||
|
||||
public LowLightVision(EquipmentSessions sessions) {
|
||||
effect = new PersistentPotionEffect(ID, PotionEffectType.NIGHT_VISION, 0, false, sessions);
|
||||
}
|
||||
|
||||
public PassiveDefinition definition() {
|
||||
return new PassiveDefinition(ID, "Low-Light Vision", Map.of(Stat.WIS, BigInteger.valueOf(12)), Set.of(), BigInteger.ONE,
|
||||
List.of("Continuously provides native Night Vision I while this character is active.",
|
||||
"Preserves unrelated Night Vision and does not change light, darkness or mob behavior."));
|
||||
}
|
||||
|
||||
public void install(org.bukkit.plugin.Plugin plugin) { effect.install(plugin); }
|
||||
public void sweep() { effect.sweep(); }
|
||||
@Override public void close() { effect.close(); }
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package games.dmg.fantasystick.passive;
|
||||
|
||||
import games.dmg.fantasystick.session.EquipmentSessions;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
|
||||
/** Bounded current-character ownership for one persistent native potion layer. */
|
||||
final class PersistentPotionEffect implements AutoCloseable {
|
||||
private static final int MAX_ACTIVE_CHECKS = 256;
|
||||
|
||||
private final class State {
|
||||
final EquipmentSessions.Active active;
|
||||
final Player player;
|
||||
PotionEffect owned;
|
||||
PotionEffect displaced;
|
||||
long displacedUntil;
|
||||
long refreshedAt;
|
||||
State(EquipmentSessions.Active active, Player player) { this.active = active; this.player = player; }
|
||||
|
||||
void refresh() {
|
||||
refreshedAt = sweepGeneration;
|
||||
if (displaced != null && !displaced.isInfinite() && sweepGeneration >= displacedUntil) displaced = null;
|
||||
PotionEffect current;
|
||||
try { current = player.getPotionEffect(type); }
|
||||
catch (RuntimeException unavailable) { return; }
|
||||
if (owned != null && !same(current, owned)) { owned = null; displaced = null; }
|
||||
if (!eligible()) { remove(); return; }
|
||||
if (current != null && (!replaceWeaker || current.getAmplifier() >= amplifier)) return;
|
||||
var effect = new PotionEffect(type, PotionEffect.INFINITE_DURATION, amplifier, false, false, true);
|
||||
try {
|
||||
if (player.addPotionEffect(effect, true)) {
|
||||
owned = effect; displaced = current;
|
||||
displacedUntil = current == null ? 0 : current.isInfinite()
|
||||
? Long.MAX_VALUE : sweepGeneration + current.getDuration();
|
||||
}
|
||||
} catch (RuntimeException unavailable) { owned = null; displaced = null; }
|
||||
}
|
||||
|
||||
boolean eligible() {
|
||||
if (closed || !sessions.current(active)) return false;
|
||||
var record = sessions.currentRecord(active).orElse(null);
|
||||
return record != null && !record.dead() && record.passives().contains(passive);
|
||||
}
|
||||
|
||||
void remove() {
|
||||
if (owned != null) {
|
||||
PotionEffect current;
|
||||
try { current = player.getPotionEffect(type); }
|
||||
catch (RuntimeException unavailable) { return; }
|
||||
if (same(current, owned)) {
|
||||
try {
|
||||
player.removePotionEffect(type);
|
||||
if (displaced != null && (displaced.isInfinite() || sweepGeneration < displacedUntil)) {
|
||||
int duration = displaced.isInfinite() ? PotionEffect.INFINITE_DURATION
|
||||
: Math.toIntExact(Math.min(Integer.MAX_VALUE, displacedUntil - sweepGeneration));
|
||||
player.addPotionEffect(new PotionEffect(displaced.getType(), duration, displaced.getAmplifier(),
|
||||
displaced.isAmbient(), displaced.hasParticles(), displaced.hasIcon()), true);
|
||||
}
|
||||
} catch (RuntimeException unavailable) { return; }
|
||||
}
|
||||
owned = null; displaced = null;
|
||||
}
|
||||
states.remove(active.player(), this);
|
||||
}
|
||||
}
|
||||
|
||||
private final String passive;
|
||||
private final PotionEffectType type;
|
||||
private final int amplifier;
|
||||
private final boolean replaceWeaker;
|
||||
private final EquipmentSessions sessions;
|
||||
private final Map<UUID, State> states = new java.util.HashMap<>();
|
||||
private int activeCursor;
|
||||
private int stateCursor;
|
||||
private long sweepGeneration;
|
||||
private org.bukkit.scheduler.BukkitTask sweepTask;
|
||||
private boolean closed;
|
||||
|
||||
PersistentPotionEffect(String passive, PotionEffectType type, int amplifier, boolean replaceWeaker,
|
||||
EquipmentSessions sessions) {
|
||||
this.passive = Objects.requireNonNull(passive); this.type = Objects.requireNonNull(type);
|
||||
if (passive.isBlank() || amplifier < 0) throw new IllegalArgumentException("Invalid persistent potion effect");
|
||||
this.amplifier = amplifier; this.replaceWeaker = replaceWeaker; this.sessions = Objects.requireNonNull(sessions);
|
||||
}
|
||||
|
||||
void install(org.bukkit.plugin.Plugin plugin) {
|
||||
Objects.requireNonNull(plugin);
|
||||
if (closed || sweepTask != null) throw new IllegalStateException("Passive potion sweep already installed or closed");
|
||||
sweepTask = plugin.getServer().getScheduler().runTaskTimer(plugin, this::sweep, 1, 1);
|
||||
}
|
||||
|
||||
void sweep() {
|
||||
if (closed) return;
|
||||
sweepGeneration++;
|
||||
var active = sessions.activeCharacters();
|
||||
int activeChecks = Math.min(MAX_ACTIVE_CHECKS, active.size());
|
||||
for (int index = 0; index < activeChecks; index++) {
|
||||
var snapshot = active.get((activeCursor + index) % active.size());
|
||||
reconcile(snapshot);
|
||||
}
|
||||
activeCursor = active.isEmpty() ? 0 : (activeCursor + activeChecks) % active.size();
|
||||
|
||||
var known = List.copyOf(states.values());
|
||||
int stateChecks = Math.min(MAX_ACTIVE_CHECKS, known.size());
|
||||
for (int index = 0; index < stateChecks; index++) {
|
||||
var state = known.get((stateCursor + index) % known.size());
|
||||
if (state.refreshedAt != sweepGeneration) state.refresh();
|
||||
}
|
||||
stateCursor = known.isEmpty() ? 0 : (stateCursor + stateChecks) % known.size();
|
||||
}
|
||||
|
||||
private void reconcile(EquipmentSessions.ActiveCharacter snapshot) {
|
||||
var active = snapshot.session();
|
||||
var state = states.get(active.player());
|
||||
boolean learned = !snapshot.record().dead() && snapshot.record().passives().contains(passive);
|
||||
if (!learned || !sessions.current(active)) { if (state != null) state.refresh(); return; }
|
||||
if (state != null && !state.active.equals(active)) { state.remove(); state = states.get(active.player()); }
|
||||
if (state == null) {
|
||||
var player = sessions.currentPlayer(active).orElse(null);
|
||||
if (player == null) return;
|
||||
state = new State(active, player); states.put(active.player(), state);
|
||||
}
|
||||
state.refresh();
|
||||
}
|
||||
|
||||
private static boolean same(PotionEffect first, PotionEffect second) {
|
||||
return first != null && second != null && first.getType().equals(second.getType())
|
||||
&& first.getAmplifier() == second.getAmplifier() && first.isInfinite() && second.isInfinite()
|
||||
&& first.isAmbient() == second.isAmbient() && first.hasParticles() == second.hasParticles()
|
||||
&& first.hasIcon() == second.hasIcon();
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
if (sweepTask != null) sweepTask.cancel();
|
||||
for (var state : List.copyOf(states.values())) state.remove();
|
||||
states.clear();
|
||||
}
|
||||
}
|
||||
@@ -5,145 +5,25 @@ import games.dmg.fantasystick.session.EquipmentSessions;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
|
||||
/** Current-session Speed I layer for the learned character passive. */
|
||||
/** Current-session native Speed I layer for the learned character passive. */
|
||||
public final class SpeedI implements AutoCloseable {
|
||||
public static final String ID = "speed_i";
|
||||
private static final int MAX_ACTIVE_CHECKS = 256;
|
||||
private static final int AMPLIFIER = 0;
|
||||
private final PersistentPotionEffect effect;
|
||||
|
||||
private final class State {
|
||||
final EquipmentSessions.Active active;
|
||||
final Player player;
|
||||
PotionEffect owned;
|
||||
PotionEffect displaced;
|
||||
long displacedUntil;
|
||||
long refreshedAt;
|
||||
State(EquipmentSessions.Active active, Player player) { this.active = active; this.player = player; }
|
||||
|
||||
void refresh() {
|
||||
refreshedAt = sweepGeneration;
|
||||
if (displaced != null && !displaced.isInfinite() && sweepGeneration >= displacedUntil) displaced = null;
|
||||
PotionEffect current;
|
||||
try { current = player.getPotionEffect(PotionEffectType.SPEED); }
|
||||
catch (RuntimeException unavailable) { return; }
|
||||
if (owned != null && !same(current, owned)) { owned = null; displaced = null; }
|
||||
if (!eligible()) { remove(); return; }
|
||||
if (current != null && current.getAmplifier() >= AMPLIFIER) return;
|
||||
var effect = new PotionEffect(PotionEffectType.SPEED, PotionEffect.INFINITE_DURATION,
|
||||
AMPLIFIER, false, false, true);
|
||||
try {
|
||||
if (player.addPotionEffect(effect, true)) {
|
||||
owned = effect; displaced = current;
|
||||
displacedUntil = current == null ? 0 : current.isInfinite()
|
||||
? Long.MAX_VALUE : sweepGeneration + current.getDuration();
|
||||
}
|
||||
} catch (RuntimeException unavailable) { owned = null; displaced = null; }
|
||||
}
|
||||
|
||||
boolean eligible() {
|
||||
if (closed || !sessions.current(active)) return false;
|
||||
var record = sessions.currentRecord(active).orElse(null);
|
||||
return record != null && !record.dead() && record.passives().contains(ID);
|
||||
}
|
||||
|
||||
void remove() {
|
||||
if (owned != null) {
|
||||
PotionEffect current;
|
||||
try { current = player.getPotionEffect(PotionEffectType.SPEED); }
|
||||
catch (RuntimeException unavailable) { return; }
|
||||
if (same(current, owned)) {
|
||||
try {
|
||||
player.removePotionEffect(PotionEffectType.SPEED);
|
||||
if (displaced != null && (displaced.isInfinite() || sweepGeneration < displacedUntil)) {
|
||||
int duration = displaced.isInfinite() ? PotionEffect.INFINITE_DURATION
|
||||
: Math.toIntExact(Math.min(Integer.MAX_VALUE, displacedUntil - sweepGeneration));
|
||||
player.addPotionEffect(new PotionEffect(displaced.getType(), duration, displaced.getAmplifier(),
|
||||
displaced.isAmbient(), displaced.hasParticles(), displaced.hasIcon()), true);
|
||||
}
|
||||
} catch (RuntimeException unavailable) { return; }
|
||||
}
|
||||
owned = null; displaced = null;
|
||||
}
|
||||
states.remove(active.player(), this);
|
||||
}
|
||||
public SpeedI(EquipmentSessions sessions) {
|
||||
effect = new PersistentPotionEffect(ID, PotionEffectType.SPEED, 0, true, sessions);
|
||||
}
|
||||
|
||||
private final EquipmentSessions sessions;
|
||||
private final Map<UUID, State> states = new java.util.HashMap<>();
|
||||
private int activeCursor;
|
||||
private int stateCursor;
|
||||
private long sweepGeneration;
|
||||
private org.bukkit.scheduler.BukkitTask sweepTask;
|
||||
private boolean closed;
|
||||
|
||||
public SpeedI(EquipmentSessions sessions) { this.sessions = Objects.requireNonNull(sessions); }
|
||||
|
||||
public PassiveDefinition definition() {
|
||||
return new PassiveDefinition(ID, "Speed I", Map.of(Stat.DEX, BigInteger.valueOf(14)), Set.of(), BigInteger.ONE,
|
||||
List.of("Continuously provides the standard native Speed I effect while this character is active.",
|
||||
"Does not stack with equal Speed I and preserves stronger unrelated Speed effects."));
|
||||
}
|
||||
|
||||
public void install(org.bukkit.plugin.Plugin plugin) {
|
||||
Objects.requireNonNull(plugin);
|
||||
if (closed || sweepTask != null) throw new IllegalStateException("Speed I sweep already installed or closed");
|
||||
sweepTask = plugin.getServer().getScheduler().runTaskTimer(plugin, this::sweep, 1, 1);
|
||||
}
|
||||
|
||||
/** Bounded rotating reconciliation for active sessions and previously owned layers. */
|
||||
public void sweep() {
|
||||
if (closed) return;
|
||||
sweepGeneration++;
|
||||
var active = sessions.activeCharacters();
|
||||
int activeChecks = Math.min(MAX_ACTIVE_CHECKS, active.size());
|
||||
for (int index = 0; index < activeChecks; index++) {
|
||||
var snapshot = active.get((activeCursor + index) % active.size());
|
||||
reconcile(snapshot);
|
||||
}
|
||||
activeCursor = active.isEmpty() ? 0 : (activeCursor + activeChecks) % active.size();
|
||||
|
||||
var known = List.copyOf(states.values());
|
||||
int stateChecks = Math.min(MAX_ACTIVE_CHECKS, known.size());
|
||||
for (int index = 0; index < stateChecks; index++) {
|
||||
var state = known.get((stateCursor + index) % known.size());
|
||||
if (state.refreshedAt != sweepGeneration) state.refresh();
|
||||
}
|
||||
stateCursor = known.isEmpty() ? 0 : (stateCursor + stateChecks) % known.size();
|
||||
}
|
||||
|
||||
private void reconcile(EquipmentSessions.ActiveCharacter snapshot) {
|
||||
var active = snapshot.session();
|
||||
var state = states.get(active.player());
|
||||
boolean learned = !snapshot.record().dead() && snapshot.record().passives().contains(ID);
|
||||
if (!learned || !sessions.current(active)) { if (state != null) state.refresh(); return; }
|
||||
if (state != null && !state.active.equals(active)) { state.remove(); state = states.get(active.player()); }
|
||||
if (state == null) {
|
||||
var player = sessions.currentPlayer(active).orElse(null);
|
||||
if (player == null) return;
|
||||
state = new State(active, player); states.put(active.player(), state);
|
||||
}
|
||||
state.refresh();
|
||||
}
|
||||
|
||||
private static boolean same(PotionEffect first, PotionEffect second) {
|
||||
return first != null && second != null && first.getType().equals(second.getType())
|
||||
&& first.getAmplifier() == second.getAmplifier() && first.isInfinite() && second.isInfinite()
|
||||
&& first.isAmbient() == second.isAmbient() && first.hasParticles() == second.hasParticles()
|
||||
&& first.hasIcon() == second.hasIcon();
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
if (sweepTask != null) sweepTask.cancel();
|
||||
for (var state : List.copyOf(states.values())) state.remove();
|
||||
states.clear();
|
||||
}
|
||||
public void install(org.bukkit.plugin.Plugin plugin) { effect.install(plugin); }
|
||||
public void sweep() { effect.sweep(); }
|
||||
@Override public void close() { effect.close(); }
|
||||
}
|
||||
@@ -60,8 +60,8 @@ class FantasyStickLifecycleNativeTest {
|
||||
assertTrue(fixture.order.indexOf("CharacterChatNames") >= 0);
|
||||
assertTrue(fixture.order.indexOf("CharacterChatNames") < recipe);
|
||||
assertNotNull(fixture.packetListener);
|
||||
assertEquals(10, fixture.timers.size(),
|
||||
"Presentation, ability/bond/rally/banner cleanup, provenance, reward retry, recovery, Speed I and session particles");
|
||||
assertEquals(11, fixture.timers.size(),
|
||||
"Presentation, ability/bond/rally/banner cleanup, provenance, reward retry, recovery, passive effects and session particles");
|
||||
assertTrue(fixture.order.indexOf("PartyCombat") >= 0);
|
||||
assertTrue(fixture.order.indexOf("PartyCombat") < recipe);
|
||||
assertTrue(fixture.order.indexOf("CombatExperience") >= 0);
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package games.dmg.fantasystick;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import games.dmg.fantasystick.passive.LowLightVision;
|
||||
import games.dmg.fantasystick.persistence.CharacterRecord;
|
||||
import games.dmg.fantasystick.persistence.CharacterStorage;
|
||||
import games.dmg.fantasystick.persistence.YamlCharacterRepository;
|
||||
import games.dmg.fantasystick.progression.Stat;
|
||||
import games.dmg.fantasystick.session.EquipmentSessions;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.file.Path;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class LowLightVisionNativeTest {
|
||||
@TempDir Path data;
|
||||
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
|
||||
|
||||
@Test void connectedLearnExitTransferUnlearnFailureAndDefeatOwnOnlyCharacterNightVision() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true); var a = new FantasyStickLifecycleNativeTest.Actor(data); withWis(data, a, List.of("", "", ""));
|
||||
var aEffects = effects(a.player); f.players.put(a.core.id, a.player); f.activate(a);
|
||||
var vision = (LowLightVision) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "lowLightVision");
|
||||
PartyBookNativeTest.open(f, a);
|
||||
assertTrue(PartyBookNativeTest.text(a).replaceAll("\\s+", "").contains("Low-LightVisionWIS12[Learn]"));
|
||||
PartyBookNativeTest.click(f, a, action(a, "[Learn]", "low_light_vision")); settle(f); vision.sweep();
|
||||
var learned = record(f, a); assertEquals(List.of("low_light_vision", "", ""), learned.passives());
|
||||
assertEquals(BigInteger.valueOf(39), learned.unspentPoints());
|
||||
assertEquals(0, aEffects.get(PotionEffectType.NIGHT_VISION).getAmplifier());
|
||||
|
||||
var sheet = a.core.inventory.getItemInOffHand().clone(); a.core.inventory.setItemInOffHand(null);
|
||||
f.changed(a); PartyBookNativeTest.drain(f); f.awaitCharacterAndReceipt(a); vision.sweep();
|
||||
assertFalse(aEffects.containsKey(PotionEffectType.NIGHT_VISION));
|
||||
var b = new FantasyStickLifecycleNativeTest.Actor(data); var bEffects = effects(b.player);
|
||||
b.core.inventory.setItemInOffHand(sheet); f.players.put(b.core.id, b.player); f.activate(b); vision.sweep();
|
||||
assertTrue(bEffects.containsKey(PotionEffectType.NIGHT_VISION));
|
||||
PartyBookNativeTest.open(f, b); PartyBookNativeTest.click(f, b, action(b, "[Unlearn]", "low_light_vision")); settle(f); vision.sweep();
|
||||
var unlearned = storage(f).find(learned.id()).get().orElseThrow(); assertFalse(unlearned.passives().contains("low_light_vision"));
|
||||
assertEquals(learned.unspentPoints().add(BigInteger.ONE), unlearned.unspentPoints());
|
||||
assertFalse(bEffects.containsKey(PotionEffectType.NIGHT_VISION));
|
||||
|
||||
PartyBookNativeTest.open(f, b); Runnable failed;
|
||||
try (var channel = java.nio.channels.FileChannel.open(data.resolve("characters").resolve(".records.lock"),
|
||||
java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.WRITE);
|
||||
var lock = channel.lock()) {
|
||||
assertTrue(lock.isValid()); PartyBookNativeTest.click(f, b, action(b, "[Learn]", "low_light_vision"));
|
||||
failed = f.tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS); assertNotNull(failed);
|
||||
}
|
||||
failed.run(); vision.sweep(); assertFalse(bEffects.containsKey(PotionEffectType.NIGHT_VISION));
|
||||
assertFalse(storage(f).find(learned.id()).get().orElseThrow().passives().contains("low_light_vision"));
|
||||
|
||||
b.core.inventory.setItemInOffHand(null); f.changed(b); PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f);
|
||||
storage(f).find(learned.id()).get(); f.receipts().find(b.core.id).toCompletableFuture().get();
|
||||
withWis(data, a, List.of("low_light_vision", "", ""));
|
||||
var c = new FantasyStickLifecycleNativeTest.Actor(data); var cEffects = effects(c.player);
|
||||
c.core.inventory.setItemInOffHand(new games.dmg.fantasystick.items.CharacterSheets().create(
|
||||
new YamlCharacterRepository(data).load(a.core.record.id()).orElseThrow()));
|
||||
f.players.put(c.core.id, c.player); f.activate(c); vision.sweep(); assertTrue(cEffects.containsKey(PotionEffectType.NIGHT_VISION));
|
||||
c.player.setHealth(1);
|
||||
var death = new org.bukkit.event.entity.PlayerDeathEvent(c.player, mock(org.bukkit.damage.DamageSource.class),
|
||||
new java.util.ArrayList<>(), 0, net.kyori.adventure.text.Component.text("defeat"), true);
|
||||
f.dispatcher.accept(death); settle(f); vision.sweep(); assertFalse(cEffects.containsKey(PotionEffectType.NIGHT_VISION));
|
||||
}
|
||||
}
|
||||
|
||||
@Test void milkAndUnrelatedNightVisionReconcileWithoutTouchingBlindnessOrDarkness() {
|
||||
var sessions = mock(EquipmentSessions.class); var player = mock(Player.class); var effects = effects(player);
|
||||
UUID id = UUID.randomUUID(); var active = new EquipmentSessions.Active(id, UUID.randomUUID(), UUID.randomUUID());
|
||||
var record = mock(CharacterRecord.class); when(record.passives()).thenReturn(List.of("low_light_vision", "", ""));
|
||||
when(player.getUniqueId()).thenReturn(id); when(sessions.activeCharacters()).thenReturn(List.of(new EquipmentSessions.ActiveCharacter(active, record)));
|
||||
when(sessions.current(active)).thenReturn(true); when(sessions.currentRecord(active)).thenReturn(java.util.Optional.of(record));
|
||||
when(sessions.currentPlayer(active)).thenReturn(java.util.Optional.of(player));
|
||||
var blindness = new PotionEffect(PotionEffectType.BLINDNESS, 100, 0);
|
||||
var darkness = new PotionEffect(PotionEffectType.DARKNESS, 100, 0);
|
||||
effects.put(PotionEffectType.BLINDNESS, blindness); effects.put(PotionEffectType.DARKNESS, darkness);
|
||||
var unrelated = new PotionEffect(PotionEffectType.NIGHT_VISION, 40, 0); effects.put(PotionEffectType.NIGHT_VISION, unrelated);
|
||||
var vision = new LowLightVision(sessions); assertEquals(Map.of(Stat.WIS, BigInteger.valueOf(12)), vision.definition().requirements());
|
||||
vision.sweep(); assertSame(unrelated, effects.get(PotionEffectType.NIGHT_VISION));
|
||||
effects.remove(PotionEffectType.NIGHT_VISION); vision.sweep(); assertTrue(effects.get(PotionEffectType.NIGHT_VISION).isInfinite());
|
||||
assertSame(blindness, effects.get(PotionEffectType.BLINDNESS)); assertSame(darkness, effects.get(PotionEffectType.DARKNESS));
|
||||
effects.remove(PotionEffectType.NIGHT_VISION); vision.sweep();
|
||||
verify(player, times(2)).addPotionEffect(argThat(effect -> effect.getType().equals(PotionEffectType.NIGHT_VISION)), eq(true));
|
||||
when(record.passives()).thenReturn(List.of("", "", "")); vision.sweep();
|
||||
assertFalse(effects.containsKey(PotionEffectType.NIGHT_VISION));
|
||||
assertSame(blindness, effects.get(PotionEffectType.BLINDNESS)); assertSame(darkness, effects.get(PotionEffectType.DARKNESS));
|
||||
vision.close();
|
||||
}
|
||||
|
||||
private static Map<PotionEffectType, PotionEffect> effects(Player player) {
|
||||
var result = new HashMap<PotionEffectType, PotionEffect>();
|
||||
doAnswer(call -> result.get(call.getArgument(0))).when(player).getPotionEffect(any());
|
||||
doAnswer(call -> { PotionEffect effect = call.getArgument(0); result.put(effect.getType(), effect); return true; })
|
||||
.when(player).addPotionEffect(any(), eq(true));
|
||||
doAnswer(call -> { result.remove(call.getArgument(0)); return null; }).when(player).removePotionEffect(any()); return result;
|
||||
}
|
||||
private static void withWis(Path data, FantasyStickLifecycleNativeTest.Actor actor, List<String> passives) throws Exception {
|
||||
var repository = new YamlCharacterRepository(data); var old = repository.load(actor.core.record.id()).orElseThrow();
|
||||
var stats = new EnumMap<>(old.stats()); stats.put(Stat.WIS, BigInteger.valueOf(12));
|
||||
repository.replace(new CharacterRecord(old.id(), old.name(), old.revision().add(BigInteger.ONE), stats, old.level(), old.experience(),
|
||||
old.unspentPoints(), old.health(), old.learned(), old.loadout(), passives, old.cooldowns(), old.restrictedKills(), false, null, null), old.revision());
|
||||
}
|
||||
private static CharacterStorage storage(FantasyStickLifecycleNativeTest.Fixture f) {
|
||||
return (CharacterStorage) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "characters");
|
||||
}
|
||||
private static CharacterRecord record(FantasyStickLifecycleNativeTest.Fixture f,
|
||||
FantasyStickLifecycleNativeTest.Actor actor) throws Exception {
|
||||
return storage(f).find(actor.core.record.id()).get().orElseThrow();
|
||||
}
|
||||
private static void settle(FantasyStickLifecycleNativeTest.Fixture f) throws Exception {
|
||||
PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f);
|
||||
}
|
||||
private static net.minecraft.network.chat.ClickEvent.Custom action(
|
||||
FantasyStickLifecycleNativeTest.Actor actor, String label, String passive) {
|
||||
var content = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(actor.books.getLast())
|
||||
.get(net.minecraft.core.component.DataComponents.WRITTEN_BOOK_CONTENT); assertNotNull(content);
|
||||
for (var page : content.pages()) { var found = action(page.raw(), label, passive); if (found != null) return found; }
|
||||
throw new AssertionError("Missing " + label + " for " + passive);
|
||||
}
|
||||
private static net.minecraft.network.chat.ClickEvent.Custom action(
|
||||
net.minecraft.network.chat.Component component, String label, String passive) {
|
||||
if (component.getString().equals(label)
|
||||
&& component.getStyle().getClickEvent() instanceof net.minecraft.network.chat.ClickEvent.Custom custom
|
||||
&& custom.payload().orElse(null) instanceof net.minecraft.nbt.StringTag payload
|
||||
&& payload.value().contains(":" + passive)) return custom;
|
||||
for (var child : component.getSiblings()) { var found = action(child, label, passive); if (found != null) return found; }
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ class QuickerRegenerationNativeTest {
|
||||
assertTrue(detail.replaceAll("\\s+", "").contains("out-of-combatrecoverypulse"));
|
||||
|
||||
var beforeLoadout = storage(f).find(a.core.record.id()).get().orElseThrow().loadout();
|
||||
PartyBookNativeTest.click(f, a, custom(nativePages(a), "fantasystick:passive_unlock"));
|
||||
PartyBookNativeTest.click(f, a, custom(List.of(pages.get(jump.page() - 1)), "fantasystick:passive_unlock"));
|
||||
PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f);
|
||||
var learned = storage(f).find(a.core.record.id()).get().orElseThrow();
|
||||
assertEquals(List.of("quicker_regeneration_i", "", ""), learned.passives());
|
||||
|
||||
Reference in new issue
Block a user