This commit is contained in:
1 parent
08d13d2192
commit
3163a491a0
8 files changed
+236
-23
No files matched your search
@@ -713,6 +713,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
message -> getLogger().warning(message));
|
||||
eventCapture = new games.dmg.fantasystick.events.EventCaptureListener(eventLog, sessions, Clock.systemUTC());
|
||||
eventCapture.excludeOwnedDamage(fireball::ownsAcceptedDamage);
|
||||
eventCapture.excludeOwnedDamage(guardianBond::ownsAcceptedRedirect);
|
||||
downedEvents = new games.dmg.fantasystick.events.DownedEventBridge(eventLog::emit, Clock.systemUTC());
|
||||
rareEvents = new games.dmg.fantasystick.events.RareEventBridge(eventLog::emit, Clock.systemUTC());rareShop.attachEvents(rareEvents);
|
||||
bookEvents = new games.dmg.fantasystick.events.BookEventBridge(eventLog::emit, Clock.systemUTC());books.attachEvents(bookEvents);
|
||||
@@ -722,6 +723,7 @@ public final class FantasyStickPlugin extends JavaPlugin {
|
||||
abilityEngine.enableCastEventsFor("fireball");
|
||||
abilityEngine.enableCastEventsFor("mending_light");
|
||||
abilityEngine.enableCastEventsFor("purify");
|
||||
abilityEngine.enableCastEventsFor("guardian_bond");
|
||||
admin.attachAbilityConfigEvents(new games.dmg.fantasystick.events.AbilityConfigEventBridge(eventLog::emit, Clock.systemUTC()));
|
||||
passiveEvents = new games.dmg.fantasystick.events.PassiveEventBridge(eventLog::emit, Clock.systemUTC());books.attachPassiveEvents(passiveEvents);admin.attachPassiveEvents(passiveEvents);
|
||||
recoveryEvents = new games.dmg.fantasystick.events.RecoveryEventBridge(eventLog::emit, Clock.systemUTC());outOfCombatRecovery.attachEvents(recoveryEvents,record->record.passives().contains(games.dmg.fantasystick.passive.QuickerRegenerationI.ID)?quickerRegeneration.bonus():java.math.BigInteger.ZERO);
|
||||
|
||||
@@ -432,6 +432,13 @@ public final class AbilityEngine implements Consumer<ControlInput>, AutoCloseabl
|
||||
if (castEvents != null && audit != null)
|
||||
castEvents.cleansed(audit, target, aided.character(), aided.session(), removed);
|
||||
}
|
||||
@Override public void acceptedPrevention(EquipmentSessions.Active aided, Player ally, Entity attacker,
|
||||
double incoming, double actual, double remaining,
|
||||
UUID bondApplication) {
|
||||
if (castEvents != null && audit != null)
|
||||
castEvents.prevented(audit, ally, aided.character(), aided.session(), attacker,
|
||||
incoming, actual, remaining, bondApplication);
|
||||
}
|
||||
@Override public void effectEnded(UUID application, String reason) {
|
||||
Objects.requireNonNull(application); Objects.requireNonNull(reason);
|
||||
var effectId = observedEffects.remove(application);
|
||||
|
||||
@@ -36,6 +36,10 @@ public interface AbilityExecution extends AutoCloseable {
|
||||
/** Record only harmful native effects actually removed from this target. */
|
||||
void acceptedCleanse(games.dmg.fantasystick.session.EquipmentSessions.Active aided,
|
||||
org.bukkit.entity.Player target, List<String> removedEffects);
|
||||
/** Report only the final uncancelled native ally reduction, not a proposed redirect. */
|
||||
void acceptedPrevention(games.dmg.fantasystick.session.EquipmentSessions.Active aided,
|
||||
org.bukkit.entity.Player ally, Entity attacker, double incoming,
|
||||
double actual, double remaining, UUID bondApplication);
|
||||
/** End an owned observed effect once; ordinary close ends any remaining observed effects. */
|
||||
void effectEnded(UUID applicationId, String reason);
|
||||
/** An owner may specify a truthful terminal outcome before finishing. */
|
||||
|
||||
@@ -56,6 +56,8 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
|
||||
final Player ally;
|
||||
final EquipmentSessions.Active aided;
|
||||
final Settings settings;
|
||||
UUID application;
|
||||
String endReason = "interrupted";
|
||||
boolean closed;
|
||||
State(AbilityExecution execution, Player guardian, Player ally,
|
||||
EquipmentSessions.Active aided, Settings settings) {
|
||||
@@ -67,14 +69,16 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
|
||||
closed = true;
|
||||
byGuardian.remove(guardian.getUniqueId(), this);
|
||||
byAlly.remove(ally.getUniqueId(), this);
|
||||
if (application != null) execution.effectEnded(application, endReason);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NativeTransfer {
|
||||
final Player guardian; final Entity attacker; final BigDecimal requested; final BigDecimal available;
|
||||
final State owner; final Player guardian; final Entity attacker; final BigDecimal requested; final BigDecimal available;
|
||||
BigDecimal accepted = BigDecimal.ZERO;
|
||||
NativeTransfer(Player guardian, Entity attacker, BigDecimal requested, BigDecimal available) {
|
||||
this.guardian = guardian; this.attacker = attacker; this.requested = requested; this.available = available;
|
||||
NativeTransfer(State owner, Player guardian, Entity attacker, BigDecimal requested, BigDecimal available) {
|
||||
this.owner = owner; this.guardian = guardian; this.attacker = attacker;
|
||||
this.requested = requested; this.available = available;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,11 +88,14 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
|
||||
private final EquipmentSessions sessions;
|
||||
private final PartyRoster parties;
|
||||
private final Damage damage;
|
||||
private final boolean nativeDamageMode;
|
||||
private final Consumer<Support> supports;
|
||||
private final Map<UUID, State> byGuardian = new java.util.HashMap<>();
|
||||
private final Map<UUID, State> byAlly = new java.util.HashMap<>();
|
||||
private int transferring;
|
||||
private NativeTransfer nativeTransfer;
|
||||
private record Reduction(State state, Entity attacker, double incoming, double accepted) { }
|
||||
private final Map<EntityDamageByEntityEvent, Reduction> pendingReductions = new java.util.IdentityHashMap<>();
|
||||
private org.bukkit.scheduler.BukkitTask sweepTask;
|
||||
private boolean closed;
|
||||
|
||||
@@ -102,7 +109,8 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
|
||||
this.settings = Objects.requireNonNull(settings);
|
||||
this.sessions = Objects.requireNonNull(sessions);
|
||||
this.parties = Objects.requireNonNull(parties);
|
||||
this.damage = damage == null ? this::nativeDamage : damage;
|
||||
this.nativeDamageMode = damage == null;
|
||||
this.damage = nativeDamageMode ? this::nativeDamage : damage;
|
||||
this.supports = Objects.requireNonNull(supports);
|
||||
}
|
||||
|
||||
@@ -153,7 +161,14 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
|
||||
}
|
||||
var state = new State(execution, guardian, ally, aided, captured);
|
||||
byGuardian.put(guardian.getUniqueId(), state); byAlly.put(ally.getUniqueId(), state);
|
||||
execution.own(state); execution.later(captured.duration(), execution::finish);
|
||||
execution.own(state);
|
||||
state.application = execution.effectApplied("guardian_bond", ally,
|
||||
Map.of("redirect_percent", captured.redirectPercent().toPlainString(),
|
||||
"range", Double.toString(captured.range()),
|
||||
"duration_seconds", seconds(captured.duration())));
|
||||
execution.later(captured.duration(), () -> {
|
||||
state.endReason = "expired"; execution.resolve("expired"); execution.finish();
|
||||
});
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
@@ -184,8 +199,30 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
|
||||
double remaining = BigDecimal.valueOf(incoming).subtract(accepted).max(BigDecimal.ZERO).doubleValue();
|
||||
if (!Double.isFinite(remaining)) return;
|
||||
event.setDamage(remaining);
|
||||
state.execution.report(AbilityReport.Type.PREVENTION, ally, accepted);
|
||||
supports.accept(new Support(attacker.getUniqueId(), state.execution.input().session().character(), state.aided, accepted));
|
||||
pendingReductions.put(event, new Reduction(state, attacker, incoming, accepted.doubleValue()));
|
||||
if (!nativeDamageMode) {
|
||||
// Injected test adapters return their final accepted result directly.
|
||||
state.execution.report(AbilityReport.Type.PREVENTION, ally, accepted);
|
||||
supports.accept(new Support(attacker.getUniqueId(), state.execution.input().session().character(), state.aided, accepted));
|
||||
}
|
||||
}
|
||||
|
||||
/** Only an uncancelled original hit can claim actual prevention. */
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void acceptedPrevention(EntityDamageByEntityEvent event) {
|
||||
var reduction = pendingReductions.remove(event);
|
||||
if (reduction == null || event.isCancelled() || !Double.isFinite(event.getFinalDamage())) return;
|
||||
double actual = Math.min(reduction.accepted(), Math.max(0, reduction.incoming() - event.getFinalDamage()));
|
||||
if (actual <= 0) return;
|
||||
var state = reduction.state();
|
||||
state.execution.acceptedPrevention(state.aided, state.ally, reduction.attacker(),
|
||||
reduction.incoming(), actual, event.getFinalDamage(), state.application);
|
||||
if (nativeDamageMode) {
|
||||
var amount = BigDecimal.valueOf(actual);
|
||||
state.execution.report(AbilityReport.Type.PREVENTION, state.ally, amount);
|
||||
supports.accept(new Support(reduction.attacker().getUniqueId(),
|
||||
state.execution.input().session().character(), state.aided, amount));
|
||||
}
|
||||
}
|
||||
|
||||
/** Capture accepted marked damage before synchronous fantasy-defeat restoration can replace native health. */
|
||||
@@ -193,7 +230,8 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
|
||||
Objects.requireNonNull(guardian); Objects.requireNonNull(attacker); Objects.requireNonNull(requested);
|
||||
double amount = requested.doubleValue(); double health = effectiveHealth(guardian);
|
||||
if (!Double.isFinite(amount) || amount <= 0 || !Double.isFinite(health) || health <= 0) return BigDecimal.ZERO;
|
||||
var transfer = new NativeTransfer(guardian, attacker, requested, BigDecimal.valueOf(health));
|
||||
var transfer = new NativeTransfer(byGuardian.get(guardian.getUniqueId()), guardian, attacker,
|
||||
requested, BigDecimal.valueOf(health));
|
||||
nativeTransfer = transfer;
|
||||
try { guardian.damage(amount, attacker); }
|
||||
finally { nativeTransfer = null; }
|
||||
@@ -206,6 +244,17 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
|
||||
if (transfer == null || event.getEntity() != transfer.guardian || attacker(event) != transfer.attacker
|
||||
|| event.isCancelled() || !Double.isFinite(event.getFinalDamage()) || event.getFinalDamage() <= 0) return;
|
||||
transfer.accepted = BigDecimal.valueOf(event.getFinalDamage()).min(transfer.requested).min(transfer.available);
|
||||
if (transfer.owner != null && transfer.accepted.signum() > 0)
|
||||
transfer.owner.execution.acceptedDamage(transfer.guardian, transfer.requested.doubleValue(),
|
||||
transfer.accepted.doubleValue(), transfer.attacker,
|
||||
Map.of("damage_kind", "guardian_bond_redirection"));
|
||||
}
|
||||
|
||||
/** The guardian's accepted native hit has one richer bond-owned damage record. */
|
||||
public boolean ownsAcceptedRedirect(org.bukkit.event.entity.EntityDamageEvent event) {
|
||||
var transfer = nativeTransfer;
|
||||
return transfer != null && event instanceof EntityDamageByEntityEvent hit
|
||||
&& hit.getEntity() == transfer.guardian && attacker(hit) == transfer.attacker;
|
||||
}
|
||||
|
||||
/** True only during synchronous dispatch of Guardian Bond's marked redirected damage. */
|
||||
@@ -230,7 +279,8 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
|
||||
}
|
||||
|
||||
private void breakBond(State state) {
|
||||
state.close(); state.execution.finish();
|
||||
state.endReason = "eligibility_changed";
|
||||
state.close(); state.execution.resolve("interrupted"); state.execution.finish();
|
||||
}
|
||||
|
||||
private static boolean eligible(Player guardian, Player ally, double range) {
|
||||
@@ -268,7 +318,9 @@ public final class GuardianBond implements AbilityEffect, Listener, AutoCloseabl
|
||||
@Override public void close() {
|
||||
if (closed) return; closed = true;
|
||||
if (sweepTask != null) sweepTask.cancel();
|
||||
for (var state : List.copyOf(byGuardian.values())) state.execution.finish();
|
||||
byGuardian.clear(); byAlly.clear();
|
||||
for (var state : List.copyOf(byGuardian.values())) {
|
||||
state.endReason = "shutdown"; state.execution.resolve("interrupted"); state.execution.finish();
|
||||
}
|
||||
byGuardian.clear(); byAlly.clear(); pendingReductions.clear();
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ public final class AbilityCastEventBridge {
|
||||
data.put("application_id", application.toString());
|
||||
if (target != null) {
|
||||
data.put("target_entity_id", target.getUniqueId().toString());
|
||||
data.put("target_type", target.getType().name());
|
||||
data.put("target_type", entityType(target));
|
||||
var place = target.getLocation();
|
||||
if (place != null) data.put("target_location", locationData(place));
|
||||
}
|
||||
@@ -108,12 +108,12 @@ public final class AbilityCastEventBridge {
|
||||
data.put("requested_amount", requested);
|
||||
data.put("actual_amount", actual);
|
||||
data.put("target_entity_id", target.getUniqueId().toString());
|
||||
data.put("target_type", target.getType().name());
|
||||
data.put("target_type", entityType(target));
|
||||
var targetAtApplication = target.getLocation();
|
||||
if (targetAtApplication != null) data.put("target_location", locationData(targetAtApplication));
|
||||
if (target instanceof org.bukkit.entity.Player player) data.put("target_player_id", player.getUniqueId().toString());
|
||||
data.put("direct_entity_id", direct.getUniqueId().toString());
|
||||
data.put("direct_entity_type", direct.getType().name());
|
||||
data.put("direct_entity_type", entityType(direct));
|
||||
emit(cast, "combat.damage", "accepted", data, application);
|
||||
} catch (RuntimeException ignored) { /* Native damage remains authoritative even when auditing fails. */ }
|
||||
}
|
||||
@@ -165,6 +165,35 @@ public final class AbilityCastEventBridge {
|
||||
} catch (RuntimeException ignored) { /* Native cleanse remains authoritative. */ }
|
||||
}
|
||||
|
||||
public void prevented(Cast cast, org.bukkit.entity.Player ally, UUID aidedCharacter,
|
||||
UUID aidedSession, Entity attacker, double incoming, double actual,
|
||||
double remaining, UUID bondApplication) {
|
||||
if (cast == null || !Double.isFinite(actual) || actual <= 0) return;
|
||||
try {
|
||||
var data = new LinkedHashMap<String, Object>();
|
||||
var application = UUID.randomUUID();
|
||||
data.put("application_id", application.toString());
|
||||
data.put("causation_id", cast.actionId().toString());
|
||||
if (bondApplication != null) data.put("bond_application_id", bondApplication.toString());
|
||||
data.put("target_player_id", ally.getUniqueId().toString());
|
||||
data.put("target_character_id", aidedCharacter.toString());
|
||||
data.put("target_session_id", aidedSession.toString());
|
||||
data.put("target_location", locationData(ally.getLocation()));
|
||||
data.put("attacker_entity_id", attacker.getUniqueId().toString());
|
||||
data.put("attacker_entity_type", entityType(attacker));
|
||||
data.put("incoming_amount", decimal(incoming));
|
||||
data.put("actual_amount", decimal(actual));
|
||||
data.put("remaining_amount", decimal(remaining));
|
||||
emit(cast, "combat.prevented", "accepted", data, application);
|
||||
} catch (RuntimeException ignored) { /* Prevention authority belongs to the native damage event. */ }
|
||||
}
|
||||
|
||||
private static String entityType(Entity entity) {
|
||||
if (entity instanceof org.bukkit.entity.Player) return "PLAYER";
|
||||
var type = entity.getType();
|
||||
return type == null ? "unavailable" : type.name();
|
||||
}
|
||||
|
||||
public void effectEnded(Cast cast, UUID application, String effectId, String reason) {
|
||||
if (cast == null) return;
|
||||
emit(cast, "effect.ended", "ended", Map.of("application_id", application.toString(),
|
||||
|
||||
@@ -70,10 +70,11 @@ public final class GuardianBondSettings implements AutoCloseable {
|
||||
public String value(String key) { return effectiveValues.get(key); }
|
||||
public GuardianBond.Settings settings() { return parse(effectiveValues); }
|
||||
|
||||
public synchronized void publish(Update update) {
|
||||
public synchronized boolean publish(Update update) {
|
||||
Objects.requireNonNull(update);
|
||||
if (update.sequence() <= publishedSequence) return;
|
||||
if (update.sequence() <= publishedSequence) return false;
|
||||
parse(update.values()); effectiveValues = update.values(); publishedSequence = update.sequence();
|
||||
return true;
|
||||
}
|
||||
|
||||
public CompletionStage<Update> set(String key, String value) {
|
||||
|
||||
@@ -1327,8 +1327,11 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
|
||||
private void guardianBondConfiguration(CommandSender sender, String[] args) {
|
||||
String key = args[1];
|
||||
var actor = abilityConfigEvents == null ? null : abilityConfigEvents.capture(sender);
|
||||
String before = guardianBondSettings.value(key);
|
||||
if (args.length == 2) {
|
||||
sender.sendMessage(key + "=" + guardianBondSettings.value(key) + " (applies to future Guardian Bond casts only)"); return;
|
||||
if (abilityConfigEvents != null) abilityConfigEvents.read(actor, "guardian_bond", key, before);
|
||||
sender.sendMessage(key + "=" + before + " (applies to future Guardian Bond casts only)"); return;
|
||||
}
|
||||
java.util.concurrent.CompletionStage<games.dmg.fantasystick.persistence.GuardianBondSettings.Update> operation;
|
||||
try { operation = guardianBondSettings.set(key, args[2]); }
|
||||
@@ -1340,8 +1343,9 @@ public final class EquipmentAdminCommand implements TabExecutor, AutoCloseable {
|
||||
if (closed) return;
|
||||
try { nextTick.accept(() -> {
|
||||
if (closed) return;
|
||||
if (failure == null) {
|
||||
guardianBond.configure(saved.settings()); abilities.replace(guardianBond.definition()); guardianBondSettings.publish(saved);
|
||||
if (failure == null && guardianBondSettings.publish(saved)) {
|
||||
guardianBond.configure(saved.settings()); abilities.replace(guardianBond.definition());
|
||||
if (abilityConfigEvents != null) abilityConfigEvents.changed(actor, "guardian_bond", key, before, saved.value());
|
||||
}
|
||||
if (!sender.hasPermission(PERMISSION)
|
||||
|| sender instanceof org.bukkit.entity.Player player && !player.isOnline()) return;
|
||||
|
||||
@@ -68,6 +68,65 @@ class GuardianBondNativeTest {
|
||||
var expired = hit(enemy, ally.player, 10); fixture.dispatcher.accept(expired);
|
||||
assertEquals(10, expired.getDamage(), 1.0e-9);
|
||||
}
|
||||
var records = events(data);
|
||||
for (var type : List.of("ability.learned", "ability.assigned", "ability.cast", "ability.resolved",
|
||||
"effect.applied", "effect.ended", "combat.prevented", "combat.damage"))
|
||||
assertEquals(1, records.stream().filter(event -> event.get("type").getAsString().endsWith(type)
|
||||
&& event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("guardian_bond")).count(), type);
|
||||
var cast = records.stream().filter(event -> event.get("type").getAsString().endsWith("ability.cast")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("guardian_bond")).findFirst().orElseThrow();
|
||||
var prevented = records.stream().filter(event -> event.get("type").getAsString().endsWith("combat.prevented")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("guardian_bond")).findFirst().orElseThrow();
|
||||
assertEquals("3", prevented.getAsJsonObject("data").get("actual_amount").getAsString());
|
||||
assertEquals(cast.getAsJsonObject("data").get("action_id").getAsString(),
|
||||
prevented.getAsJsonObject("data").get("action_id").getAsString());
|
||||
}
|
||||
|
||||
private static List<com.google.gson.JsonObject> events(Path root) throws Exception {
|
||||
var values = new ArrayList<com.google.gson.JsonObject>();
|
||||
try (var files = java.nio.file.Files.walk(root.resolve("events"))) {
|
||||
for (var file : files.filter(path -> path.toString().endsWith(".jsonl")).toList())
|
||||
for (var line : java.nio.file.Files.readAllLines(file))
|
||||
if (!line.isBlank()) values.add(com.google.gson.JsonParser.parseString(line).getAsJsonObject());
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
@Test void laterCancelledOriginalHitCannotClaimAcceptedPrevention() throws Exception {
|
||||
try (var fixture = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
fixture.plugin.setEnabled(true);
|
||||
var guardian = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
var ally = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
prepareGuardian(data, guardian, true);
|
||||
fixture.players.put(guardian.core.id, guardian.player);
|
||||
fixture.players.put(ally.core.id, ally.player);
|
||||
fixture.activate(guardian); fixture.activate(ally);
|
||||
party(fixture, guardian, ally); aim(guardian, ally, 8, true);
|
||||
nativeDamage(fixture, guardian);
|
||||
class Veto implements org.bukkit.event.Listener {
|
||||
@org.bukkit.event.EventHandler(priority = org.bukkit.event.EventPriority.HIGHEST)
|
||||
public void cancel(EntityDamageByEntityEvent event) {
|
||||
if (event.getEntity() == ally.player) event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
fixture.dispatcher.register(new Veto(), fixture.plugin);
|
||||
use(fixture, guardian); drain(fixture, 2);
|
||||
var enemy = mock(org.bukkit.entity.Zombie.class);
|
||||
when(enemy.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
when(enemy.getType()).thenReturn(org.bukkit.entity.EntityType.ZOMBIE);
|
||||
var original = hit(enemy, ally.player, 10); fixture.dispatcher.accept(original);
|
||||
assertTrue(original.isCancelled());
|
||||
assertEquals(7, guardian.player.getHealth(), 1.0e-9);
|
||||
}
|
||||
var observed = events(data);
|
||||
assertEquals(0, observed.stream().filter(event -> event.get("type").getAsString().endsWith("combat.prevented")
|
||||
&& event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("guardian_bond")).count());
|
||||
assertEquals(1, observed.stream().filter(event -> event.get("type").getAsString().endsWith("combat.damage")
|
||||
&& event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("guardian_bond")).count(),
|
||||
"The guardian's separately accepted native damage remains truthful");
|
||||
}
|
||||
|
||||
@Test void lethalRedirectUsesNormalDefeatAndOnlyRemovesGuardianAvailableHealth() throws Exception {
|
||||
@@ -92,7 +151,10 @@ class GuardianBondNativeTest {
|
||||
}
|
||||
};
|
||||
use(fixture, guardian); drain(fixture, 2); guardian.core.health = 1; ally.core.health = 9;
|
||||
var hit = hit(mock(org.bukkit.entity.Zombie.class), ally.player, 10); fixture.dispatcher.accept(hit);
|
||||
var attacker = mock(org.bukkit.entity.Zombie.class);
|
||||
when(attacker.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
when(attacker.getType()).thenReturn(org.bukkit.entity.EntityType.ZOMBIE);
|
||||
var hit = hit(attacker, ally.player, 10); fixture.dispatcher.accept(hit);
|
||||
assertEquals(List.of("guardian"), defeatOrder,
|
||||
"Redirected damage reaches normal guardian defeat before original damage resumes");
|
||||
assertEquals(9, hit.getDamage(), 1.0e-9, "Overkill cannot delete damage the guardian could not accept");
|
||||
@@ -107,6 +169,11 @@ class GuardianBondNativeTest {
|
||||
}
|
||||
assertEquals(List.of("guardian", "ally"), defeatOrder, "Both characters may be defeated in the approved order");
|
||||
}
|
||||
var ownerDamage = events(data).stream().filter(event -> event.get("type").getAsString().endsWith("combat.damage")
|
||||
&& event.getAsJsonObject("data").has("ability_id")
|
||||
&& event.getAsJsonObject("data").get("ability_id").getAsString().equals("guardian_bond")).toList();
|
||||
assertEquals(1, ownerDamage.size(), "A lethal redirect still has one accepted guardian damage record");
|
||||
assertEquals(1.0, ownerDamage.getFirst().getAsJsonObject("data").get("actual_amount").getAsDouble());
|
||||
}
|
||||
|
||||
@Test void actualProtectionCreatesOnePersistedSupportShareForTheAidedDirectContributor() throws Exception {
|
||||
@@ -132,6 +199,25 @@ class GuardianBondNativeTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test void cancelledOriginalNativeHitCannotCreateEffectiveSupport() {
|
||||
var context = new Context(true);
|
||||
when(context.guardian.getHealth()).thenReturn(10.0);
|
||||
doAnswer(call -> {
|
||||
var redirected = hit(call.getArgument(1), context.guardian, (Double) call.getArgument(0));
|
||||
context.bond.acceptedRedirect(redirected);
|
||||
return null;
|
||||
}).when(context.guardian).damage(anyDouble(), any(Entity.class));
|
||||
context.activate();
|
||||
var cancelled = hit(context.enemy, context.ally, 10);
|
||||
context.bond.redirect(cancelled); cancelled.setCancelled(true);
|
||||
context.bond.acceptedPrevention(cancelled);
|
||||
assertTrue(context.supports.isEmpty(), "A later-vetoed original hit cannot create support credit");
|
||||
var accepted = hit(context.enemy, context.ally, 10);
|
||||
context.bond.redirect(accepted); context.bond.acceptedPrevention(accepted);
|
||||
assertEquals(1, context.supports.size());
|
||||
assertEquals(0, new BigDecimal("3").compareTo(context.supports.getFirst().amount()));
|
||||
}
|
||||
|
||||
@Test void conservedRoutingUsesPostRecipientAndPostGuardianMitigationCancellationAndLethalCaps() {
|
||||
var context = new Context(); var requested = new ArrayList<BigDecimal>();
|
||||
var mitigation = new games.dmg.fantasystick.ability.CombatMitigation();
|
||||
@@ -268,6 +354,19 @@ class GuardianBondNativeTest {
|
||||
nativeDamage(fixture, guardian); use(fixture, guardian); drain(fixture, 2);
|
||||
var future = hit(mock(org.bukkit.entity.Zombie.class), ally.player, 10); fixture.dispatcher.accept(future);
|
||||
assertEquals(5, future.getDamage(), 1.0e-9); assertEquals(180L, fixture.delayedTicks.getLast());
|
||||
try (var channel = java.nio.channels.FileChannel.open(data.resolve(".config.lock"),
|
||||
java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.WRITE);
|
||||
var lock = channel.lock()) {
|
||||
assertTrue(lock.isValid());
|
||||
fixture.command.execute(sender.port, "fstickadmin",
|
||||
new String[]{"config", "guardian-bond-range-blocks", "17"});
|
||||
var failed = fixture.tasks.poll(5, TimeUnit.SECONDS);
|
||||
assertNotNull(failed); failed.run();
|
||||
}
|
||||
assertEquals("16", ((games.dmg.fantasystick.persistence.GuardianBondSettings)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(fixture.plugin, "guardianBondSettings"))
|
||||
.value("guardian-bond-range-blocks"));
|
||||
assertTrue(sender.messages.getLast().contains("save failed"));
|
||||
fixture.command.execute(sender.port, "fantasystick", new String[]{"config", "guardian-bond-duration-seconds", "0"});
|
||||
assertTrue(sender.messages.getLast().contains("invalid"));
|
||||
fixture.command.execute(sender.port, "fantasystick", new String[]{"config", "guardian-bond-range-blocks", "16", "extra"});
|
||||
@@ -281,6 +380,18 @@ class GuardianBondNativeTest {
|
||||
assertEquals(0, new BigDecimal("9").compareTo(afterRestart.parameters().get("duration-seconds")));
|
||||
assertEquals(new BigDecimal("50"), afterRestart.parameters().get("redirect-percent"));
|
||||
}
|
||||
var config = events(data).stream().filter(event -> event.get("type").getAsString().endsWith("config.read")
|
||||
|| event.get("type").getAsString().endsWith("config.changed"))
|
||||
.filter(event -> event.getAsJsonObject("data").has("key")
|
||||
&& event.getAsJsonObject("data").get("key").getAsString().startsWith("guardian-bond-")).toList();
|
||||
assertEquals(5, config.size());
|
||||
assertEquals("14", config.getFirst().getAsJsonObject("data").get("after").getAsString());
|
||||
assertEquals("console", config.getFirst().getAsJsonObject("data").get("actor_kind").getAsString());
|
||||
var range = config.stream().filter(event -> event.get("type").getAsString().endsWith("config.changed")
|
||||
&& event.getAsJsonObject("data").get("key").getAsString().equals("guardian-bond-range-blocks"))
|
||||
.findFirst().orElseThrow();
|
||||
assertEquals("14", range.getAsJsonObject("data").get("before").getAsString());
|
||||
assertEquals("16", range.getAsJsonObject("data").get("after").getAsString());
|
||||
for (String invalid : List.of("guardian-bond-cooldown-seconds: -1\n", "guardian-bond-range-blocks: 0\n",
|
||||
"guardian-bond-duration-seconds: 0\n", "guardian-bond-redirect-percent: 101\n")) {
|
||||
var directory = data.resolve(Integer.toHexString(invalid.hashCode())); java.nio.file.Files.createDirectories(directory);
|
||||
@@ -299,7 +410,8 @@ class GuardianBondNativeTest {
|
||||
GuardianBond.Damage damage = (target, attacker, amount) -> amount; final GuardianBond bond;
|
||||
final EquipmentSessions.Active guardianActive; final EquipmentSessions.Active allyActive;
|
||||
final PartyRoster.Party party;
|
||||
Context() {
|
||||
Context() { this(false); }
|
||||
Context(boolean nativeAdapter) {
|
||||
var guardianId = UUID.randomUUID(); var allyId = UUID.randomUUID();
|
||||
when(guardian.getUniqueId()).thenReturn(guardianId); when(ally.getUniqueId()).thenReturn(allyId);
|
||||
when(enemy.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
@@ -314,8 +426,10 @@ class GuardianBondNativeTest {
|
||||
when(parties.partyOf(guardianId)).thenReturn(java.util.Optional.of(party)); when(parties.partyOf(allyId)).thenReturn(java.util.Optional.of(party));
|
||||
when(input.player()).thenReturn(guardian); when(input.target()).thenReturn(ally); when(input.session()).thenReturn(guardianActive);
|
||||
when(execution.input()).thenReturn(input); when(execution.stillCurrent()).thenReturn(true);
|
||||
bond = new GuardianBond(GuardianBond.Settings.defaults(), sessions, parties,
|
||||
(target, attacker, amount) -> damage.apply(target, attacker, amount), supports::add);
|
||||
bond = nativeAdapter
|
||||
? new GuardianBond(GuardianBond.Settings.defaults(), sessions, parties, supports::add)
|
||||
: new GuardianBond(GuardianBond.Settings.defaults(), sessions, parties,
|
||||
(target, attacker, amount) -> damage.apply(target, attacker, amount), supports::add);
|
||||
when(execution.definition()).thenReturn(bond.definition());
|
||||
}
|
||||
void activate() { bond.activate(execution); }
|
||||
|
||||
Reference in new issue
Block a user