Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d98a2a80a3 | ||
|
|
7a85eea769 |
No files matched your search
@@ -162,8 +162,8 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
var passiveNames = passiveDefinitions.stream().collect(java.util.stream.Collectors.toMap(
|
||||
games.dmg.fantasystick.passive.PassiveDefinition::id,
|
||||
games.dmg.fantasystick.passive.PassiveDefinition::name));
|
||||
var passiveDetails = passiveDefinitions.stream()
|
||||
.map(definition -> passiveDetailPages(record, definition, passiveNames, passiveView)).toList();
|
||||
var placeholderPassiveDetails = passiveDefinitions.stream()
|
||||
.map(definition -> passiveDetailPages(record, definition, passiveNames, passiveView, 1)).toList();
|
||||
var placeholderTargets = definitions.stream().collect(java.util.stream.Collectors.toMap(
|
||||
games.dmg.fantasystick.ability.AbilityDefinition::id, ignored -> 1));
|
||||
var passivePlaceholderTargets = passiveDefinitions.stream().collect(java.util.stream.Collectors.toMap(
|
||||
@@ -174,10 +174,15 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
int passiveCount = passiveSlotPages(record, passiveDefinitions, passivePlaceholderTargets).size();
|
||||
var placeholderIndex = indexPages(definitions, placeholderTargets);
|
||||
int indexCount = placeholderIndex.pages().size();
|
||||
int passiveIndexCount = passiveIndexPages(passiveDefinitions, passivePlaceholderTargets).size();
|
||||
var placeholderPassiveIndex = passiveIndexPages(passiveDefinitions, passivePlaceholderTargets);
|
||||
int passiveIndexCount = placeholderPassiveIndex.pages().size();
|
||||
int indexStart = pages.size() + activeCount + passiveCount;
|
||||
var indexTargets = new java.util.HashMap<String, Integer>();
|
||||
placeholderIndex.relativePageByAbility().forEach((id, page) -> indexTargets.put(id, indexStart + page + 1));
|
||||
int passiveIndexStart = indexStart + indexCount;
|
||||
var passiveIndexTargets = new java.util.HashMap<String, Integer>();
|
||||
placeholderPassiveIndex.relativePageByPassive().forEach((id, page) ->
|
||||
passiveIndexTargets.put(id, passiveIndexStart + page + 1));
|
||||
var targets = new java.util.HashMap<String, Integer>();
|
||||
int target = pages.size() + activeCount + passiveCount + indexCount + passiveIndexCount + 1;
|
||||
for (int index = 0; index < definitions.size(); index++) {
|
||||
@@ -187,15 +192,18 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
var passiveTargets = new java.util.HashMap<String, Integer>();
|
||||
for (int index = 0; index < passiveDefinitions.size(); index++) {
|
||||
passiveTargets.put(passiveDefinitions.get(index).id(), target);
|
||||
target += passiveDetails.get(index).size();
|
||||
target += placeholderPassiveDetails.get(index).size();
|
||||
}
|
||||
var index = indexPages(definitions, targets);
|
||||
var details = definitions.stream()
|
||||
.map(definition -> abilityPages(record, definition, abilityView, indexTargets.get(definition.id()))).toList();
|
||||
var passiveIndex = passiveIndexPages(passiveDefinitions, passiveTargets);
|
||||
var passiveDetails = passiveDefinitions.stream().map(definition -> passiveDetailPages(record, definition,
|
||||
passiveNames, passiveView, passiveIndexTargets.get(definition.id()))).toList();
|
||||
pages.addAll(activePages(record, definitions, targets));
|
||||
pages.addAll(passiveSlotPages(record, passiveDefinitions, passiveTargets));
|
||||
pages.addAll(index.pages());
|
||||
pages.addAll(passiveIndexPages(passiveDefinitions, passiveTargets));
|
||||
pages.addAll(passiveIndex.pages());
|
||||
details.forEach(pages::addAll);
|
||||
passiveDetails.forEach(pages::addAll);
|
||||
}
|
||||
@@ -293,12 +301,17 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
var relativePages = new java.util.HashMap<String, Integer>();
|
||||
for (var category : games.dmg.fantasystick.ability.AbilityDefinition.Category.values()) {
|
||||
var grouped = definitions.stream().filter(definition -> definition.category() == category).toList();
|
||||
for (int start = 0; start < grouped.size(); start += 11) {
|
||||
int cursor = 0;
|
||||
while (cursor < grouped.size()) {
|
||||
var builder = new PageBuilder(); builder.text("Ability index"); builder.text(category.name());
|
||||
int relativePage = pages.size();
|
||||
for (var definition : grouped.subList(start, Math.min(start + 11, grouped.size()))) {
|
||||
int usedLines = 0;
|
||||
while (cursor < grouped.size()) {
|
||||
var definition = grouped.get(cursor);
|
||||
int linkLines = BookPagination.lines(definition.name()).size();
|
||||
if (usedLines > 0 && usedLines + linkLines > 11) break;
|
||||
relativePages.put(definition.id(), pages.size());
|
||||
builder.link("", definition.name(), targets.get(definition.id()));
|
||||
relativePages.put(definition.id(), relativePage);
|
||||
usedLines += linkLines; cursor++;
|
||||
}
|
||||
pages.addAll(builder.pages());
|
||||
}
|
||||
@@ -306,21 +319,33 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
return new AbilityIndex(pages, relativePages);
|
||||
}
|
||||
|
||||
private static java.util.List<Component> passiveIndexPages(
|
||||
private record PassiveIndex(java.util.List<Component> pages, Map<String, Integer> relativePageByPassive) {
|
||||
private PassiveIndex { pages = java.util.List.copyOf(pages); relativePageByPassive = Map.copyOf(relativePageByPassive); }
|
||||
}
|
||||
|
||||
private static PassiveIndex passiveIndexPages(
|
||||
java.util.List<games.dmg.fantasystick.passive.PassiveDefinition> definitions, Map<String, Integer> targets) {
|
||||
var pages = new java.util.ArrayList<Component>();
|
||||
for (int start = 0; start < definitions.size(); start += 11) {
|
||||
var pages = new java.util.ArrayList<Component>(); var relativePages = new java.util.HashMap<String, Integer>();
|
||||
int cursor = 0;
|
||||
while (cursor < definitions.size()) {
|
||||
var builder = new PageBuilder(); builder.text("Passive index"); builder.blank();
|
||||
for (var definition : definitions.subList(start, Math.min(start + 11, definitions.size()))) {
|
||||
int usedLines = 0;
|
||||
while (cursor < definitions.size()) {
|
||||
var definition = definitions.get(cursor);
|
||||
int linkLines = BookPagination.lines(definition.name()).size();
|
||||
if (usedLines > 0 && usedLines + linkLines > 11) break;
|
||||
relativePages.put(definition.id(), pages.size());
|
||||
builder.link("", definition.name(), targets.get(definition.id()));
|
||||
usedLines += linkLines; cursor++;
|
||||
}
|
||||
pages.addAll(builder.pages());
|
||||
}
|
||||
return java.util.List.copyOf(pages);
|
||||
return new PassiveIndex(pages, relativePages);
|
||||
}
|
||||
|
||||
private static java.util.List<Component> passiveDetailPages(CharacterRecord record,
|
||||
games.dmg.fantasystick.passive.PassiveDefinition definition, Map<String, String> names, PassiveView view) {
|
||||
games.dmg.fantasystick.passive.PassiveDefinition definition, Map<String, String> names, PassiveView view,
|
||||
int indexPage) {
|
||||
String requirements = definition.requirements().entrySet().stream().sorted(Map.Entry.comparingByKey())
|
||||
.map(entry -> entry.getKey() + " " + entry.getValue()).collect(java.util.stream.Collectors.joining(", "));
|
||||
if (!definition.prerequisites().isEmpty()) {
|
||||
@@ -341,6 +366,8 @@ public final class CharacterBookController implements Consumer<ControlInput>, Au
|
||||
else if (prerequisites && stats && points && slot) builder.component(passiveButton("Learn", PASSIVE_UNLOCK, view, definition.id()));
|
||||
else builder.text(state);
|
||||
builder.blank(); definition.description().forEach(builder::text); builder.blank(); builder.text(state);
|
||||
builder.component(Component.text("[Back to index]", net.kyori.adventure.text.format.NamedTextColor.DARK_BLUE)
|
||||
.clickEvent(net.kyori.adventure.text.event.ClickEvent.changePage(indexPage)));
|
||||
return builder.pages();
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ public final class BasicAttack implements Consumer<ControlInput>, Listener, Auto
|
||||
static BigInteger damage(BigInteger strength) {
|
||||
Objects.requireNonNull(strength);
|
||||
if (strength.signum() < 0) throw new IllegalArgumentException("Strength must not be negative");
|
||||
return strength.subtract(BigInteger.TEN).divide(BigInteger.TWO).max(BigInteger.ONE);
|
||||
return strength.subtract(BigInteger.TEN).divide(BigInteger.TWO).add(BigInteger.ONE).max(BigInteger.ONE);
|
||||
}
|
||||
|
||||
/** MONITOR observes the synchronous native result; cancelled/zero damage never spends cadence. */
|
||||
|
||||
@@ -28,6 +28,7 @@ class BasicAttackNativeTest {
|
||||
var basic = new games.dmg.fantasystick.combat.BasicAttack(sessions, clock);
|
||||
var target = mock(org.bukkit.entity.LivingEntity.class);
|
||||
var cancelled = new java.util.concurrent.atomic.AtomicBoolean(true);
|
||||
var zero = new java.util.concurrent.atomic.AtomicBoolean(false);
|
||||
doAnswer(call -> {
|
||||
var source = mock(org.bukkit.damage.DamageSource.class);
|
||||
when(source.getCausingEntity()).thenReturn(a.player);
|
||||
@@ -35,6 +36,7 @@ class BasicAttackNativeTest {
|
||||
var event = new org.bukkit.event.entity.EntityDamageByEntityEvent(a.player, target,
|
||||
org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK, source, (Double) call.getArgument(0));
|
||||
event.setCancelled(cancelled.get());
|
||||
if (zero.get()) event.setDamage(0);
|
||||
basic.acceptedDamage(event);
|
||||
return null;
|
||||
}).when(target).damage(anyDouble(), same(a.player));
|
||||
@@ -43,12 +45,13 @@ class BasicAttackNativeTest {
|
||||
games.dmg.fantasystick.session.ControlInput.Gesture.ATTACK, target, 0, 1,
|
||||
() -> sessions.current(active));
|
||||
basic.accept(input); // Cancelled: no cadence spent.
|
||||
cancelled.set(false);
|
||||
basic.accept(input); // Accepted at t=0.
|
||||
cancelled.set(false); zero.set(true);
|
||||
basic.accept(input); // Zero final damage: no cadence spent.
|
||||
zero.set(false); basic.accept(input); // Accepted at t=0.
|
||||
clock.advance(java.time.Duration.ofMillis(999)); basic.accept(input);
|
||||
verify(target, times(2)).damage(1.0, a.player);
|
||||
clock.advance(java.time.Duration.ofMillis(1)); basic.accept(input);
|
||||
verify(target, times(3)).damage(1.0, a.player);
|
||||
clock.advance(java.time.Duration.ofMillis(1)); basic.accept(input);
|
||||
verify(target, times(4)).damage(1.0, a.player);
|
||||
a.core.inventory.setItemInOffHand(null); f.changed(a);
|
||||
clock.advance(java.time.Duration.ofSeconds(1)); basic.accept(input);
|
||||
verifyNoMoreInteractions(target);
|
||||
@@ -88,6 +91,32 @@ class BasicAttackNativeTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test void packagedBasicStickUsesBasePlusStrengthModifierAcrossApprovedBoundaries() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
int[] strengths = {0, 9, 10, 11, 12, 13, 14, 15};
|
||||
double[] expected = {1, 1, 1, 1, 2, 2, 3, 3};
|
||||
for (int index = 0; index < strengths.length; index++) {
|
||||
var actor = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
withStrength(data, actor, java.math.BigInteger.valueOf(strengths[index]));
|
||||
f.players.put(actor.core.id, actor.player); f.activate(actor);
|
||||
actor.core.inventory.getInventory().setSelectedSlot(1);
|
||||
var target = mock(org.bukkit.entity.LivingEntity.class);
|
||||
var attack = new io.papermc.paper.event.player.PrePlayerAttackEntityEvent(actor.player, target, true);
|
||||
f.dispatcher.accept(attack);
|
||||
assertTrue(attack.isCancelled()); verify(target).damage(expected[index], actor.player);
|
||||
actor.core.inventory.setItemInOffHand(null); f.changed(actor); f.awaitCharacterAndReceipt(actor);
|
||||
}
|
||||
var actor = new FantasyStickLifecycleNativeTest.Actor(data);
|
||||
withStrength(data, actor, java.math.BigInteger.TEN.pow(1000));
|
||||
f.players.put(actor.core.id, actor.player); f.activate(actor);
|
||||
actor.core.inventory.getInventory().setSelectedSlot(1);
|
||||
var target = mock(org.bukkit.entity.LivingEntity.class);
|
||||
f.dispatcher.accept(new io.papermc.paper.event.player.PrePlayerAttackEntityEvent(actor.player, target, true));
|
||||
verify(target).damage(Double.MAX_VALUE, actor.player);
|
||||
}
|
||||
}
|
||||
|
||||
@Test void packagedBasicStickInputDealsOneLowStrengthHitWithoutVanillaContinuation() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
@@ -106,6 +135,19 @@ class BasicAttackNativeTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static void withStrength(Path data, FantasyStickLifecycleNativeTest.Actor actor,
|
||||
java.math.BigInteger strength) throws Exception {
|
||||
var repository = new games.dmg.fantasystick.persistence.YamlCharacterRepository(data);
|
||||
var old = repository.load(actor.core.record.id()).orElseThrow();
|
||||
var stats = new java.util.EnumMap<>(old.stats());
|
||||
stats.put(games.dmg.fantasystick.progression.Stat.STR, strength);
|
||||
repository.replace(new games.dmg.fantasystick.persistence.CharacterRecord(old.id(), old.name(),
|
||||
old.revision().add(java.math.BigInteger.ONE), stats, old.level(), old.experience(), old.unspentPoints(),
|
||||
old.health(), old.learned(), old.loadout(), old.passives(), old.cooldowns(), old.restrictedKills(),
|
||||
false, null, null, old.coins(), old.coinsInitialized(), old.rewardReceipts(), old.pvpCoinEscrow(), old.gear()),
|
||||
old.revision());
|
||||
}
|
||||
|
||||
private static final class MutableClock extends Clock {
|
||||
private Instant now;
|
||||
MutableClock(Instant now) { this.now = now; }
|
||||
|
||||
@@ -35,6 +35,28 @@ class QuickerRegenerationNativeTest {
|
||||
assertTrue(active > 0 && active < passive && passive < activeIndex && activeIndex < passiveIndex && passiveIndex < party);
|
||||
assertTrue(pages.get(passive).getString().contains("1: No Passive"));
|
||||
assertTrue(pages.get(passiveIndex).getString().replace("\n", "").contains("Quicker Regeneration I"));
|
||||
var indexPageByPassive = new java.util.HashMap<String, Integer>();
|
||||
var detailStartByPassive = new java.util.HashMap<String, Integer>();
|
||||
var passiveRegistry = (games.dmg.fantasystick.passive.PassiveRegistry)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "passiveRegistry");
|
||||
for (var definition : passiveRegistry.definitions()) {
|
||||
for (int page = passiveIndex; page < pages.size() && pages.get(page).getString().startsWith("Passive index"); page++) {
|
||||
var target = changePageTo(pages.get(page), pages, definition.name());
|
||||
if (target != null) { indexPageByPassive.put(definition.name(), page);
|
||||
detailStartByPassive.put(definition.name(), target.page() - 1); break; }
|
||||
}
|
||||
}
|
||||
assertEquals(passiveRegistry.definitions().size(), detailStartByPassive.size());
|
||||
for (var entry : detailStartByPassive.entrySet()) {
|
||||
int end = party;
|
||||
for (int other : detailStartByPassive.values()) if (other > entry.getValue()) end = Math.min(end, other);
|
||||
var backPages = new java.util.ArrayList<Integer>();
|
||||
for (int candidate = entry.getValue(); candidate < end; candidate++)
|
||||
if (changePageNamed(pages.get(candidate), "[Back to index]") != null) backPages.add(candidate);
|
||||
assertEquals(java.util.List.of(end - 1), backPages, entry.getKey());
|
||||
assertEquals(indexPageByPassive.get(entry.getKey()) + 1,
|
||||
changePageNamed(pages.get(end - 1), "[Back to index]").page(), entry.getKey());
|
||||
}
|
||||
var jump = changePageTo(pages.get(passiveIndex), pages, "Quicker Regeneration I"); assertNotNull(jump);
|
||||
String detail = pages.get(jump.page() - 1).getString();
|
||||
assertTrue(detail.replaceAll("\\s+", "").startsWith("QuickerRegenerationICON12,WIS12[Learn]"), detail);
|
||||
@@ -74,6 +96,50 @@ class QuickerRegenerationNativeTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test void multiPagePassiveIndexAndWrappedDetailsReturnToTheirExactContainingPage() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
var registry = (games.dmg.fantasystick.passive.PassiveRegistry)
|
||||
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "passiveRegistry");
|
||||
for (int index = 0; index < 12; index++) {
|
||||
String suffix = String.format(java.util.Locale.ROOT, "%02d", index);
|
||||
registry.register(new games.dmg.fantasystick.passive.PassiveDefinition("synthetic_" + suffix,
|
||||
"Synthetic Passive " + suffix, java.util.Map.of(), java.util.Set.of(), BigInteger.ONE,
|
||||
List.of(index == 0 ? "Wrapped detail ".repeat(80) : "Synthetic passive detail.")));
|
||||
}
|
||||
var a = new FantasyStickLifecycleNativeTest.Actor(data); f.players.put(a.core.id, a.player); f.activate(a);
|
||||
var before = storage(f).find(a.core.record.id()).get().orElseThrow(); PartyBookNativeTest.open(f, a);
|
||||
var pages = nativePages(a); assertTrue(pages.size() <= 100);
|
||||
int passiveIndex = first(pages, "Passive index"), party = first(pages, "Party");
|
||||
var indexPageByPassive = new java.util.HashMap<String, Integer>();
|
||||
var detailStartByPassive = new java.util.HashMap<String, Integer>();
|
||||
int indexPages = 0;
|
||||
for (int page = passiveIndex; page < pages.size() && pages.get(page).getString().startsWith("Passive index"); page++) indexPages++;
|
||||
for (var definition : registry.definitions()) {
|
||||
for (int page = passiveIndex; page < passiveIndex + indexPages; page++) {
|
||||
var target = changePageTo(pages.get(page), pages, definition.name());
|
||||
if (target != null) { indexPageByPassive.put(definition.name(), page);
|
||||
detailStartByPassive.put(definition.name(), target.page() - 1); break; }
|
||||
}
|
||||
}
|
||||
assertTrue(indexPages >= 2); assertEquals(registry.definitions().size(), detailStartByPassive.size());
|
||||
boolean wrapped = false;
|
||||
for (var entry : detailStartByPassive.entrySet()) {
|
||||
int end = party;
|
||||
for (int other : detailStartByPassive.values()) if (other > entry.getValue()) end = Math.min(end, other);
|
||||
wrapped |= end - entry.getValue() > 1;
|
||||
var backPages = new java.util.ArrayList<Integer>();
|
||||
for (int candidate = entry.getValue(); candidate < end; candidate++)
|
||||
if (changePageNamed(pages.get(candidate), "[Back to index]") != null) backPages.add(candidate);
|
||||
assertEquals(java.util.List.of(end - 1), backPages, entry.getKey());
|
||||
assertEquals(indexPageByPassive.get(entry.getKey()) + 1,
|
||||
changePageNamed(pages.get(end - 1), "[Back to index]").page(), entry.getKey());
|
||||
}
|
||||
assertTrue(wrapped, "At least one passive detail must span native pages");
|
||||
assertEquals(before, storage(f).find(before.id()).get().orElseThrow());
|
||||
}
|
||||
}
|
||||
|
||||
@Test void unknownFullPassiveSlotsRemainVisibleAndRefuseBeforeSpending() throws Exception {
|
||||
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
||||
f.plugin.setEnabled(true);
|
||||
@@ -144,6 +210,15 @@ class QuickerRegenerationNativeTest {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private static net.minecraft.network.chat.ClickEvent.ChangePage changePageNamed(
|
||||
net.minecraft.network.chat.Component component, String label) {
|
||||
if (component.getString().equals(label)
|
||||
&& component.getStyle().getClickEvent() instanceof net.minecraft.network.chat.ClickEvent.ChangePage page) return page;
|
||||
for (var child : component.getSiblings()) {
|
||||
var found = changePageNamed(child, label); if (found != null) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private static net.minecraft.network.chat.ClickEvent.Custom custom(List<net.minecraft.network.chat.Component> pages, String id) {
|
||||
for (var page : pages) { var found = custom(page, id); if (found != null) return found; }
|
||||
throw new AssertionError("Missing custom action " + id);
|
||||
|
||||
@@ -7,13 +7,14 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
class BasicAttackTest {
|
||||
@Test void followsTheApprovedStrengthFormulaWithoutAStatCap() {
|
||||
assertEquals(BigInteger.ONE, BasicAttack.damage(BigInteger.ZERO));
|
||||
assertEquals(BigInteger.ONE, BasicAttack.damage(BigInteger.TEN));
|
||||
assertEquals(BigInteger.ONE, BasicAttack.damage(BigInteger.valueOf(11)));
|
||||
assertEquals(BigInteger.ONE, BasicAttack.damage(BigInteger.valueOf(12)));
|
||||
assertEquals(BigInteger.TWO, BasicAttack.damage(BigInteger.valueOf(14)));
|
||||
for (int strength : new int[]{0, 9, 10, 11})
|
||||
assertEquals(BigInteger.ONE, BasicAttack.damage(BigInteger.valueOf(strength)), "STR " + strength);
|
||||
for (int strength : new int[]{12, 13})
|
||||
assertEquals(BigInteger.TWO, BasicAttack.damage(BigInteger.valueOf(strength)), "STR " + strength);
|
||||
for (int strength : new int[]{14, 15})
|
||||
assertEquals(BigInteger.valueOf(3), BasicAttack.damage(BigInteger.valueOf(strength)), "STR " + strength);
|
||||
var huge = BigInteger.TEN.pow(1000);
|
||||
assertEquals(huge.subtract(BigInteger.TEN).divide(BigInteger.TWO), BasicAttack.damage(huge));
|
||||
assertEquals(huge.subtract(BigInteger.TEN).divide(BigInteger.TWO).add(BigInteger.ONE), BasicAttack.damage(huge));
|
||||
assertThrows(IllegalArgumentException.class, () -> BasicAttack.damage(BigInteger.valueOf(-1)));
|
||||
assertThrows(NullPointerException.class, () -> BasicAttack.damage(null));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user