Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8fed8998b |
No files matched your search
@@ -335,7 +335,8 @@ public final class CombatExperience implements Listener, AutoCloseable {
|
||||
} else if (award.status() == CharacterStorage.CombatAwardStatus.UNCHANGED) {
|
||||
player.sendMessage("No fantasy XP: this target's configured reward is zero.");
|
||||
}
|
||||
if (operation.coins.signum() > 0 && award.status() != CharacterStorage.CombatAwardStatus.DUPLICATE) {
|
||||
if (operation.coins.signum() > 0 && award.status() != CharacterStorage.CombatAwardStatus.DUPLICATE
|
||||
&& award.status() != CharacterStorage.CombatAwardStatus.SPACING_BLOCKED) {
|
||||
player.sendMessage("Earned " + operation.coins + " character coins.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,7 +378,7 @@ public final class CharacterStorage implements AutoCloseable {
|
||||
var saved = new CharacterRecord(old.id(), old.name(), old.revision().add(BigInteger.ONE), old.stats(),
|
||||
level, experience, old.unspentPoints().add(BigInteger.valueOf(gained)), old.health(), old.learned(),
|
||||
old.loadout(), old.passives(), old.cooldowns(), history, false, old.deathId(), old.diedAt(),
|
||||
old.coins().add(coinReward), true, receipts,old.pvpCoinEscrow(),old.gear());
|
||||
old.coins().add(eligible ? coinReward : BigInteger.ZERO), true, receipts,old.pvpCoinEscrow(),old.gear());
|
||||
repository.replace(saved, old.revision());
|
||||
return new CombatAward(status, Optional.of(saved), gained);
|
||||
});
|
||||
|
||||
@@ -39,6 +39,108 @@ class CombatExperienceNativeTest {
|
||||
games.dmg.fantasystick.party.CombatExperience.coinTarget(witch));
|
||||
}
|
||||
|
||||
@Test void tooCloseHostileDeathDiscardsItsAllocatedCoinsAndPublishesNoCoinFeedback() throws Exception {
|
||||
java.nio.file.Files.writeString(data.resolve("config.yml"),
|
||||
"hostile-coin-drop-min: 10\nhostile-coin-drop-max: 10\n");
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var actor = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
f.players.put(actor.core.id, actor.player); f.activate(actor);
|
||||
var world = mock(org.bukkit.World.class); when(world.getUID()).thenReturn(UUID.randomUUID());
|
||||
var first = mob(EntityType.ZOMBIE, new Location(world, 0, 64, 0));
|
||||
hit(f, actor.player, first, false); die(f, first); settle(f);
|
||||
var eligible = record(f, actor);
|
||||
int earnedCoinMessages = (int) actor.messages.stream().filter(message -> message.contains("character coins")).count();
|
||||
assertEquals(1, earnedCoinMessages);
|
||||
|
||||
var near = mob(EntityType.ZOMBIE, new Location(world, 1, 64, 0));
|
||||
hit(f, actor.player, near, false); die(f, near); settle(f);
|
||||
var blocked = record(f, actor);
|
||||
assertEquals(eligible.experience(), blocked.experience());
|
||||
assertEquals(eligible.coins(), blocked.coins(), "Spacing-blocked combat coins are discarded");
|
||||
assertEquals(earnedCoinMessages,
|
||||
actor.messages.stream().filter(message -> message.contains("character coins")).count());
|
||||
assertTrue(actor.messages.stream().anyMatch(message -> message.contains("too close")));
|
||||
assertEquals(new java.math.BigDecimal("1.0"), blocked.restrictedKills().values().iterator().next().x(),
|
||||
"Blocked attempts still advance the spacing reference");
|
||||
}
|
||||
}
|
||||
|
||||
@Test void everyRestrictedMobCoinClassUsesTheExistingExactAndCrossWorldSpacingGeometry() throws Exception {
|
||||
java.nio.file.Files.writeString(data.resolve("config.yml"),
|
||||
"hostile-coin-drop-min: 12\nhostile-coin-drop-max: 12\n"
|
||||
+ "boss-coin-drop-min: 12\nboss-coin-drop-max: 12\n");
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var types = java.util.List.of(EntityType.ZOMBIE, EntityType.BEE, EntityType.WITCH,
|
||||
EntityType.WITHER, EntityType.WARDEN, EntityType.ENDER_DRAGON);
|
||||
for (var type : types) {
|
||||
var actor = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
f.players.put(actor.core.id, actor.player); f.activate(actor);
|
||||
var firstWorld = mock(org.bukkit.World.class); when(firstWorld.getUID()).thenReturn(UUID.randomUUID());
|
||||
var otherWorld = mock(org.bukkit.World.class); when(otherWorld.getUID()).thenReturn(UUID.randomUUID());
|
||||
var initial = record(f, actor);
|
||||
var first = mob(type, new Location(firstWorld, 0, 64, 0));
|
||||
hit(f, actor.player, first, false); die(f, first); settle(f);
|
||||
var eligible = record(f, actor);
|
||||
var expectedCoins = type == EntityType.BEE ? java.math.BigInteger.ZERO : java.math.BigInteger.valueOf(12);
|
||||
assertEquals(initial.coins().add(expectedCoins), eligible.coins(), type + " first kill");
|
||||
int coinMessages = (int) actor.messages.stream().filter(message -> message.contains("character coins")).count();
|
||||
|
||||
var near = mob(type, new Location(firstWorld, 1, 64, 0));
|
||||
hit(f, actor.player, near, false); die(f, near); settle(f);
|
||||
var blocked = record(f, actor);
|
||||
assertEquals(eligible.experience(), blocked.experience(), type + " near XP");
|
||||
assertEquals(eligible.coins(), blocked.coins(), type + " near coins");
|
||||
assertEquals(coinMessages,
|
||||
actor.messages.stream().filter(message -> message.contains("character coins")).count(), type + " feedback");
|
||||
assertEquals(new java.math.BigDecimal("1.0"), blocked.restrictedKills().get(firstWorld.getUID()).x());
|
||||
|
||||
var exact = mob(type, new Location(firstWorld, 4, 64, 0));
|
||||
hit(f, actor.player, exact, false); die(f, exact); settle(f);
|
||||
var boundary = record(f, actor);
|
||||
assertTrue(boundary.experience().compareTo(blocked.experience()) > 0, type + " exact distance XP");
|
||||
assertEquals(blocked.coins().add(expectedCoins), boundary.coins(), type + " exact distance coins");
|
||||
|
||||
var acrossWorld = mob(type, new Location(otherWorld, 0, 64, 0));
|
||||
hit(f, actor.player, acrossWorld, false); die(f, acrossWorld); settle(f);
|
||||
var independent = record(f, actor);
|
||||
assertTrue(independent.experience().compareTo(boundary.experience()) > 0, type + " cross-world XP");
|
||||
assertEquals(boundary.coins().add(expectedCoins), independent.coins(), type + " cross-world coins");
|
||||
assertEquals(2, independent.restrictedKills().size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test void partyRecipientsApplyIndependentSpacingWithoutRedistributingBlockedShares() throws Exception {
|
||||
java.nio.file.Files.writeString(data.resolve("config.yml"),
|
||||
"hostile-coin-drop-min: 20\nhostile-coin-drop-max: 20\n");
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var a = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
var b = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
f.players.put(a.core.id, a.player); f.players.put(b.core.id, b.player); f.activate(a); f.activate(b);
|
||||
var world = mock(org.bukkit.World.class); when(world.getUID()).thenReturn(UUID.randomUUID());
|
||||
var history = mob(EntityType.ZOMBIE, new Location(world, 0, 64, 0));
|
||||
hit(f, a.player, history, false); die(f, history); settle(f);
|
||||
var beforeA = record(f, a); var beforeB = record(f, b);
|
||||
|
||||
var parties = PartyLifecycleNativeTest.parties(f); var party = parties.create(a.core.id).orElseThrow();
|
||||
assertTrue(parties.accept(b.core.id, parties.invite(a.core.id, party.id(), b.core.id).orElseThrow().id()));
|
||||
var shared = mob(EntityType.ZOMBIE, new Location(world, 1, 64, 0));
|
||||
hit(f, a.player, shared, false); die(f, shared); settle(f); settle(f);
|
||||
var afterA = record(f, a); var afterB = record(f, b);
|
||||
assertEquals(beforeA.experience(), afterA.experience());
|
||||
assertEquals(beforeA.coins(), afterA.coins(), "A's blocked half is discarded");
|
||||
assertEquals(beforeB.experience().add(java.math.BigInteger.valueOf(5)), afterB.experience());
|
||||
assertEquals(beforeB.coins().add(java.math.BigInteger.TEN), afterB.coins(), "B retains only B's half");
|
||||
assertEquals(new java.math.BigDecimal("1.0"), afterA.restrictedKills().get(world.getUID()).x());
|
||||
assertEquals(new java.math.BigDecimal("1.0"), afterB.restrictedKills().get(world.getUID()).x());
|
||||
assertTrue(a.messages.stream().anyMatch(message -> message.contains("too close")));
|
||||
assertTrue(b.messages.stream().anyMatch(message -> message.contains("10 character coins")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test void packagedWitchPoolFlowsThroughExactSoloAndPartyAllocationWithOrdinaryCoins() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
@@ -229,10 +331,11 @@ class CombatExperienceNativeTest {
|
||||
var original = new games.dmg.fantasystick.crafting.CharacterFactory().create(progression);
|
||||
var active = new games.dmg.fantasystick.session.EquipmentSessions.Active(UUID.randomUUID(), UUID.randomUUID(), original.id());
|
||||
var attacker = mock(org.bukkit.entity.Player.class);
|
||||
when(attacker.getUniqueId()).thenReturn(active.player());
|
||||
when(attacker.getUniqueId()).thenReturn(active.player()); when(attacker.isOnline()).thenReturn(true);
|
||||
when(sessions.active(active.player())).thenReturn(java.util.Optional.of(active));
|
||||
when(sessions.activeCharacter(active.character())).thenReturn(java.util.Optional.of(active));
|
||||
when(sessions.current(active)).thenReturn(true);
|
||||
when(sessions.acknowledgeProgress(eq(active), any(), any())).thenReturn(true);
|
||||
when(storage.find(original.id())).thenReturn(java.util.concurrent.CompletableFuture.completedFuture(java.util.Optional.of(original)));
|
||||
var failed = new java.util.concurrent.CompletableFuture<games.dmg.fantasystick.persistence.CharacterStorage.CombatAward>();
|
||||
failed.completeExceptionally(new java.io.IOException("simulated write failure"));
|
||||
@@ -257,13 +360,15 @@ class CombatExperienceNativeTest {
|
||||
service.damage(new EntityDamageByEntityEvent(attacker, target, DamageCause.ENTITY_ATTACK, damageSource, 1));
|
||||
service.mobDeath(new org.bukkit.event.entity.EntityDeathEvent(target, damageSource, new ArrayList<>(), 0));
|
||||
verify(storage, times(1)).awardCombatReward(eq(original.id()), any(), any(), any(), any(), any(), any(), any());
|
||||
assertEquals(1, failures.size());
|
||||
assertEquals(1, failures.size()); verify(attacker, never()).sendMessage(anyString());
|
||||
service.configure(service.rules().withHardPool(java.math.BigInteger.valueOf(25)));
|
||||
service.retryPending();
|
||||
verify(storage, times(2)).awardCombatReward(eq(original.id()), any(), any(), any(), any(), any(), any(), any());
|
||||
verify(attacker, times(1)).sendMessage(contains("character coins"));
|
||||
service.mobDeath(new org.bukkit.event.entity.EntityDeathEvent(target, damageSource, new ArrayList<>(), 0));
|
||||
service.retryPending();
|
||||
verify(storage, times(2)).awardCombatReward(eq(original.id()), any(), any(), any(), any(), any(), any(), any());
|
||||
verify(attacker, times(1)).sendMessage(contains("character coins"));
|
||||
service.close();
|
||||
}
|
||||
|
||||
|
||||
@@ -135,6 +135,54 @@ class CharacterStorageTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void restrictedSpacingAtomicallyDiscardsCoinsWithoutDisablingOtherZeroXpCoinAwards() throws Exception {
|
||||
var repository = new YamlCharacterRepository(data);
|
||||
var progression = new games.dmg.fantasystick.progression.ProgressionRules(BigInteger.ZERO, BigInteger.TEN, 30);
|
||||
var rewards = games.dmg.fantasystick.progression.CombatRewardRules.defaults();
|
||||
var base = new games.dmg.fantasystick.crafting.CharacterFactory().create(progression);
|
||||
var live = new CharacterRecord(base.id(), base.name(), base.revision(), base.stats(), base.level(),
|
||||
base.experience(), base.unspentPoints(), base.health(), base.learned(), base.loadout(), base.passives(),
|
||||
base.cooldowns(), base.restrictedKills(), false, null, null, BigInteger.valueOf(20), true, java.util.Set.of());
|
||||
var world = UUID.randomUUID();
|
||||
var first = new games.dmg.fantasystick.progression.CombatRewardRules.KillPosition(world,
|
||||
java.math.BigDecimal.ZERO, java.math.BigDecimal.ZERO, java.math.BigDecimal.ZERO);
|
||||
var near = new games.dmg.fantasystick.progression.CombatRewardRules.KillPosition(world,
|
||||
java.math.BigDecimal.ONE, java.math.BigDecimal.ZERO, java.math.BigDecimal.ZERO);
|
||||
try (var storage = new CharacterStorage(repository, Clock.systemUTC())) {
|
||||
storage.create(live).join();
|
||||
var eligible = storage.awardCombatReward(live.id(), live.revision(), UUID.randomUUID(), BigInteger.TEN,
|
||||
BigInteger.valueOf(6), progression, rewards, first).join();
|
||||
assertEquals(CharacterStorage.CombatAwardStatus.APPLIED, eligible.status());
|
||||
assertEquals(BigInteger.valueOf(26), eligible.record().orElseThrow().coins());
|
||||
|
||||
var blockedId = UUID.randomUUID();
|
||||
var blocked = storage.awardCombatReward(live.id(), eligible.record().orElseThrow().revision(), blockedId,
|
||||
BigInteger.TEN, BigInteger.valueOf(7), progression, rewards, near).join();
|
||||
assertEquals(CharacterStorage.CombatAwardStatus.SPACING_BLOCKED, blocked.status());
|
||||
var afterBlocked = blocked.record().orElseThrow();
|
||||
assertEquals(eligible.record().orElseThrow().experience(), afterBlocked.experience());
|
||||
assertEquals(BigInteger.valueOf(26), afterBlocked.coins(), "Spacing discards rather than escrows the allocated coin share");
|
||||
assertEquals(near, afterBlocked.restrictedKills().get(world), "Blocked awards advance the persisted spacing chain");
|
||||
assertTrue(afterBlocked.rewardReceipts().contains(blockedId));
|
||||
assertEquals(afterBlocked, storage.awardCombatReward(live.id(), afterBlocked.revision(), blockedId,
|
||||
BigInteger.TEN, BigInteger.valueOf(7), progression, rewards, near).join().record().orElseThrow());
|
||||
|
||||
var zeroXp = storage.awardCombatReward(live.id(), afterBlocked.revision(), UUID.randomUUID(), BigInteger.ZERO,
|
||||
BigInteger.valueOf(4), progression, rewards, null).join();
|
||||
assertEquals(CharacterStorage.CombatAwardStatus.APPLIED, zeroXp.status());
|
||||
assertEquals(BigInteger.valueOf(30), zeroXp.record().orElseThrow().coins(),
|
||||
"A non-spacing zero-XP result remains coin-eligible");
|
||||
|
||||
var cappedRules = new games.dmg.fantasystick.progression.ProgressionRules(BigInteger.ZERO, BigInteger.TEN, 1);
|
||||
var capped = storage.awardCombatReward(live.id(), zeroXp.record().orElseThrow().revision(), UUID.randomUUID(),
|
||||
BigInteger.TEN, BigInteger.valueOf(5), cappedRules, rewards, null).join();
|
||||
assertEquals(CharacterStorage.CombatAwardStatus.AT_CAP, capped.status());
|
||||
assertEquals(BigInteger.valueOf(35), capped.record().orElseThrow().coins(),
|
||||
"Maximum-level status alone must not suppress coins");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void stableCoinDebitIsAtomicIdempotentAndNeverOverdraws() throws Exception {
|
||||
var repository = new YamlCharacterRepository(data);
|
||||
|
||||
Reference in new issue
Block a user