Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ private void handleCheck(CommandSender sender, String[] args) {
// stand in for a placeholder that has not been filled yet
sender.sendMessage(data.isRevealed(id)
? messages().get("admin.check-task-revealed", "%slot%", slot + 1, "%count%", count,
"%points%", def == null ? 0 : def.worth(count), "%activity%", display)
"%points%", def == null ? 0 : def.rawWorth(count), "%activity%", display)
: messages().get("admin.check-task-hidden", "%slot%", slot + 1, "%activity%", display));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1175,16 +1175,17 @@ private void warnIfVoteShareMisfits() {
+ " - on a day it is not drawn, the reserved share cannot be earned.");
}
int reserved = dailyMax - nonVoteDailyMax();
if (vote.dailyCap() > 0 && vote.dailyCap() < reserved) {
plugin.getLogger().warning("activities.vote.daily-cap " + vote.dailyCap() + " is below the "
if (vote.dailyCap() > 0 && vote.capPoints() < reserved) {
plugin.getLogger().warning("activities.vote.daily-cap " + vote.dailyCap() + " (" + vote.capPoints()
+ " points) is below the "
+ reserved + " points bar.vote-share keeps for voting - the rest of that share is never earned.");
}
}

// ====================================
// A player can only earn from the TASKS_PER_DAY activities drawn for them,
// so the daily ceiling is the worst draw they can get: the lowest
// TASKS_PER_DAY daily-caps, itself capped by bar.daily-max. Seven days of
// TASKS_PER_DAY daily-caps in points, itself capped by bar.daily-max. Seven days of
// that is the weekly ceiling - if it is under the first milestone, an
// unlucky week can never be claimed on.
//
Expand All @@ -1197,7 +1198,7 @@ private void warnIfBarUnreachable() {
List<Integer> caps = new ArrayList<>();
for (ActivityDef def : activities.values()) {
if (def.dailyCap() > 0) {
caps.add(def.dailyCap());
caps.add(def.capPoints());
}
}

Expand Down Expand Up @@ -1241,8 +1242,8 @@ static String unreachableWarning(List<Integer> dailyCaps, int activityCount, int
}

// ====================================
// The arithmetic above, pure so it can be tested: the daily-caps of the
// capped activities (in any order), how many activities are loaded in
// The arithmetic above, pure so it can be tested: the daily-caps, in
// points, of the capped activities (in any order), how many activities are loaded in
// total, and bar.daily-max. Package-private for the test.
// ====================================
static long dailyCeiling(List<Integer> dailyCaps, int activityCount, int dailyMax) {
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/tfmc/justin/activity/gui/ActivityGui.java
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ static List<String> activityLore(Messages messages, ActivityDef def, int count)
lore.add(messages.get("gui.activity-lore-progress", "%bar%", Utils.colorize(progress)));
}
lore.add(def.dailyCap() > 0
? messages.get("gui.activity-lore-today-capped", "%today%", def.worth(count), "%cap%", def.dailyCap())
? messages.get("gui.activity-lore-today-capped", "%today%", def.worth(count), "%cap%", def.capPoints())
: messages.get("gui.activity-lore-today", "%today%", def.worth(count)));
return lore;
}
Expand All @@ -260,7 +260,7 @@ static List<String> activityLore(Messages messages, ActivityDef def, int count)
// Bukkit inventory.
// ====================================
static String progressBar(ActivityDef def, int count) {
if (def.dailyCap() > 0 && def.worth(count) >= def.dailyCap()) {
if (def.dailyCap() > 0 && def.worth(count) >= def.capPoints()) {
return Bar.render(1, 1, PROGRESS_BAR_LENGTH);
}
if (def.every() > 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public String onRequest(OfflinePlayer player, String params) {
// at least one award earned today
int today = currentWeek && data.dayKey().equals(keys.day())
? def.worth(data.count(def.id())) : 0;
boolean done = def.dailyCap() > 0 ? today >= def.dailyCap() : today > 0;
boolean done = def.dailyCap() > 0 ? today >= def.capPoints() : today > 0;
return Utils.colorize(config.messages().raw(done ? "placeholder.done" : "placeholder.not-done"));
}

Expand Down
11 changes: 9 additions & 2 deletions src/main/java/tfmc/justin/activity/models/ActivityDef.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

// ====================================
// every: actions per award. points: awarded each time 'every' is met.
// dailyCap: most points this activity can add in one day, 0 = unlimited.
// dailyCap: most awards ('every' met) that count in one day, 0 = unlimited;
// capPoints() is the same limit in points.
// iconPath: the item path the GUI icon comes from - either the TLibs
// m.<type>.<id> form or the normalized ItemsAdder ia.<namespace:id> one,
// which is what routes ActivityGui.fromPath to the right hook - or null when
Expand Down Expand Up @@ -42,7 +43,13 @@ public ActivityDef(String id, String display, Material icon, String iconPath, in
// What a day's action count is worth in points, after every and dailyCap
public int worth(int count) {
int raw = rawWorth(count);
return dailyCap > 0 ? Math.min(raw, dailyCap) : raw;
return dailyCap > 0 ? Math.min(raw, capPoints()) : raw;
}

// dailyCap in points - the most worth() can reach in a day. Saturates like
// rawWorth. Only meaningful when dailyCap > 0.
public int capPoints() {
return (int) Math.min(Integer.MAX_VALUE, (long) dailyCap * points);
}

// ====================================
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/tfmc/justin/activity/models/PlayerData.java
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ public RecordResult record(int amount, ActivityDef def, int max, int dailyMax, i
// Either the activity has nothing more to give today, or the count
// is simply part-way to its next point - which is a plain success
return new RecordResult(0, 0,
def.dailyCap() > 0 && def.worth(before) >= def.dailyCap()
def.dailyCap() > 0 && def.worth(before) >= def.capPoints()
? Recorded.ACTIVITY_CAP : Recorded.RECORDED);
}
int pointsBefore = points;
Expand Down
14 changes: 9 additions & 5 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ reset:

# Seven of these are drawn at random for each player every day and shown in
# /activity; a task earns nothing until the player clicks it to reveal it.
# Every 'every' actions → earn 'points'. 'daily-cap' = most points one
# activity can add per day (0 = no cap). Bar caps at bar.max; bar.daily-max
# Every 'every' actions → earn 'points'. 'daily-cap' = how many times per day
# that can happen (0 = no cap), so a day is worth at most daily-cap × points
# (every: 1, points: 5, daily-cap: 1 → the first action gives 5, later ones
# that day give nothing). Bar caps at bar.max; bar.daily-max
# caps what can reach it on any single day, first, and bar.vote-share keeps part
# of that day for voting only. All are bypassed by
# /activity add <player> <activity> <count> --force, which is staff-only and
Expand All @@ -15,7 +17,8 @@ reset:
# remaining slots are filled at random as usual. Marking more than seven
# activities guaranteed is reported at load - the draw would then be seven of
# them and nothing else could ever be drawn.
# vote: one point per vote site, and always one of the day's tasks.
# vote: one point per vote site (every: 1, points: 1), daily-cap = how many
# sites count per day, and always one of the day's tasks.
# playtime: 'every' counts minutes online - see the playtime section at the
# bottom, and read what afk-minutes does and does not do before relying on it.
# material: the GUI icon, and craft: the item this activity counts crafts of.
Expand Down Expand Up @@ -66,7 +69,8 @@ reset:
# 'every' is XP, not actions. Fractional XP is carried over between gains
# rather than lost. Two activities naming the same profession is a config
# error and only the last one is fed. Always set a daily-cap on these:
# profession XP is unbounded, unlike the one-per-action activities.
# profession XP is unbounded, unlike the one-per-action activities. Here
# daily-cap counts awards, i.e. at most daily-cap × every XP counts a day.
# station: optional, MMOItems only. Counts crafts made at an MMOItems
# crafting station, written as either
# ingot-station any recipe crafted at that station
Expand Down Expand Up @@ -193,7 +197,7 @@ activities:
material: BONE
every: 1
points: 5
daily-cap: 5
daily-cap: 1
# InteractibleFurniture: furniture placed in the world
furniture_place:
display: "#e6ca40&lPlace Furniture"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,13 +494,21 @@ private static List<String> milestoneCrossedBy(int barMax, int startPoints, Send

@Test
void checkPrintsEveryFieldOfThePlayersDay() {
ActivityManager manager = manager();
// daily-cap 1 makes worth(3) = 1, distinct from rawWorth(3) = 3, so
// the "before any cap" text below is checked against the uncapped
// figure the message promises rather than the capped one - which
// 'check' must read off config's own def, not merely echo the count
ActivityManager manager = TestManagers.manager(
new ActivityDef("vote", "Vote", Material.PAPER, null, 1, 1, 1),
new ActivityDef("quest", "Quest", Material.PAPER, null, 2, 1, 0));
TestManagers.messages(manager);
TestManagers.rerollsPerDay(manager, 1);
UUID player = UUID.randomUUID();
manager.reveal(player, manager.tasks(player).tasks().indexOf("vote"));
PlayerData data = manager.getStore().peek(player);
// Straight through the POJO: crediting a point through the manager
// would announce it, and that needs a running server
data.record(3, new ActivityDef("vote", "Vote", Material.PAPER, null, 2, 1, 0), 50, 10, List.of(10, 20));
data.record(3, new ActivityDef("vote", "Vote", Material.PAPER, null, 1, 1, 1), 50, 10, List.of(10, 20));

Sender sender = admin();
command(manager, player, "Steve").onCommand(sender.bukkit, null, "activity", new String[] {"check", "Steve"});
Expand All @@ -521,7 +529,7 @@ void checkPrintsEveryFieldOfThePlayersDay() {
assertTrue(out.contains("Vote"), out);
assertTrue(out.contains("revealed"), out);
assertTrue(out.contains("count 3"), out);
assertTrue(out.contains("that count is worth 1 points before any cap"), out);
assertTrue(out.contains("that count is worth 3 points before any cap"), out);
// and every slot of the draw is listed
assertEquals(data.tasks().size(),
sender.sent.stream().filter(line -> line.contains("revealed") || line.contains("hidden")).count());
Expand Down
14 changes: 14 additions & 0 deletions src/test/java/tfmc/justin/activity/gui/ActivityGuiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,20 @@ void theDescriptionSitsAboveTheProgressAndTodayLines() {
messages.get("gui.activity-lore-today-capped", "%today%", 0, "%cap%", 5)), lore);
}

// daily-cap counts awards, so the lore's cap is shown in points
// (daily-cap × points) alongside today's points, and the bar reads full
@Test
void theTodayLineShowsTheCapInPoints() {
Messages messages = shippedMessages();
ActivityDef def = activity("injured", 1, 5, 1);

List<String> lore = ActivityGui.activityLore(messages, def, 3);

assertEquals(List.of(
messages.get("gui.activity-lore-progress", "%bar%", Utils.colorize(Bar.render(1, 1, 20))),
messages.get("gui.activity-lore-today-capped", "%today%", 5, "%cap%", 5)), lore);
}

// No description configured - the lore is exactly what it has always been
@Test
void anActivityWithoutADescriptionRendersTheSameTwoLines() {
Expand Down
38 changes: 38 additions & 0 deletions src/test/java/tfmc/justin/activity/models/ActivityDefTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,42 @@ void theFullConstructorRoundTripsIconPath() {

assertEquals("m.item.ballot", def.iconPath());
}

// daily-cap counts awards, not points: a day is worth min(count / every,
// dailyCap) * points
@Test
void dailyCapCountsAwardsNotPoints() {
ActivityDef def = new ActivityDef("injured", "Injured", Material.BONE, null, 1, 5, 1);

assertEquals(5, def.worth(1));
assertEquals(5, def.worth(3));
assertEquals(5, def.capPoints());
}

@Test
void dailyCapWithEveryAboveOne() {
ActivityDef def = new ActivityDef("x", "X", Material.PAPER, null, 2, 3, 2);

assertEquals(0, def.worth(1));
assertEquals(3, def.worth(2));
assertEquals(3, def.worth(3));
assertEquals(6, def.worth(4));
assertEquals(6, def.worth(5));
}

@Test
void zeroDailyCapIsUnlimited() {
ActivityDef def = new ActivityDef("x", "X", Material.PAPER, null, 1, 5, 0);

assertEquals(500, def.worth(100));
}

@Test
void capPointsAndWorthSaturateInsteadOfOverflowing() {
ActivityDef def = new ActivityDef("x", "X", Material.PAPER, null, 1, 1_000_000, 1_000_000);

assertEquals(Integer.MAX_VALUE, def.capPoints());
assertEquals(Integer.MAX_VALUE, def.worth(Integer.MAX_VALUE));
assertEquals(1_000_000, def.worth(1));
}
}
20 changes: 20 additions & 0 deletions src/test/java/tfmc/justin/activity/models/PlayerDataTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,26 @@ void anActivityAtItsOwnDailyCapSaysSo() {
assertEquals(Recorded.ACTIVITY_CAP, record(data, VOTE, 1).outcome());
}

// A cap in points > 1 per award: worth() must compare against capPoints(),
// not the raw award count, or a cap of 1 would let a second every:1 award
// through when its points happen to be more than 1.
@Test
void anActivityCapWithMultiPointAwardsStopsAtItsCapPoints() {
ActivityDef cappedAtOne =
new ActivityDef("richcap", "Rich Cap", Material.STONE, null, 1, 5, 1);
PlayerData data = data();

assertEquals(5, record(data, cappedAtOne, 1).pointsAwarded());
assertEquals(Recorded.ACTIVITY_CAP, record(data, cappedAtOne, 1).outcome());

ActivityDef cappedAtTwo =
new ActivityDef("richcap2", "Rich Cap Two", Material.STONE, null, 1, 5, 2);
PlayerData data2 = data();

assertEquals(5, record(data2, cappedAtTwo, 1).pointsAwarded());
assertEquals(5, record(data2, cappedAtTwo, 1).pointsAwarded());
}

@Test
void aSpentDailyBudgetSaysSo() {
PlayerData data = data();
Expand Down
Loading