feat(earnings): gate rewards by user progress

This commit is contained in:
simoleo89
2026-06-15 21:49:45 +02:00
parent 766d8d67d3
commit 22b05b4e52
5 changed files with 59 additions and 6 deletions
+7 -2
View File
@@ -117,11 +117,16 @@ INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
('earnings.daily_gift.native.enabled', '0', 'Use native hotel subsystem data for daily gift earnings claims when available.'),
('earnings.games.native.enabled', '0', 'Use native hotel subsystem data for games earnings claims when available.'),
('earnings.achievements.native.enabled', '0', 'Use native hotel subsystem data for achievements earnings claims when available.'),
('earnings.achievements.native.enabled', '1', 'Use achievement score thresholds for achievements earnings claims.'),
('earnings.marketplace.native.enabled', '1', 'Use marketplace sold item payouts for marketplace earnings claims.'),
('earnings.hc_payday.native.enabled', '1', 'Use unclaimed HC payday logs for HC payday earnings claims.'),
('earnings.level_progress.native.enabled', '0', 'Use native hotel subsystem data for level progress earnings claims when available.'),
('earnings.level_progress.native.enabled', '1', 'Use talent track levels for level progress earnings claims.'),
('earnings.donations.native.enabled', '0', 'Use native hotel subsystem data for donations earnings claims when available.'),
('earnings.bonus_bag.native.enabled', '0', 'Use native hotel subsystem data for bonus bag earnings claims when available.'),
('earnings.mystery_boxes.native.enabled', '0', 'Use native hotel subsystem data for mystery boxes earnings claims when available.'),
('earnings.club_job.native.enabled', '0', 'Use native hotel subsystem data for club and job earnings claims when available.');
INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
('earnings.achievements.min_score', '1', 'Minimum achievement score required before achievements earnings can be claimed.'),
('earnings.achievements.score.step', '100', 'Achievement score bucket size used to prevent repeated claims for the same progress band.'),
('earnings.level_progress.min_level', '1', 'Minimum citizenship/helper talent level required before level progress earnings can be claimed.');
@@ -517,8 +517,12 @@ public final class Emulator {
Emulator.config.register(prefix + "item_id", "0");
Emulator.config.register(prefix + "item.quantity", "1");
Emulator.config.register(prefix + "hc.days", "0");
Emulator.config.register(prefix + "native.enabled", (category.equals("marketplace") || category.equals("hc_payday")) ? "1" : "0");
Emulator.config.register(prefix + "native.enabled", (category.equals("marketplace") || category.equals("hc_payday") || category.equals("achievements") || category.equals("level_progress")) ? "1" : "0");
}
Emulator.config.register("earnings.achievements.min_score", "1");
Emulator.config.register("earnings.achievements.score.step", "100");
Emulator.config.register("earnings.level_progress.min_level", "1");
}
public static RCONServer getRconServer() {
@@ -95,7 +95,11 @@ public class EarningsCenterManager {
return new EarningsClaimResult(category, EarningsClaimResult.Status.NO_REWARD, buildEntry(habbo, userId, category, now));
}
String periodKey = periodKey(now, definition.cooldownSeconds());
if (!isEligibleForProgressClaim(habbo, category)) {
return new EarningsClaimResult(category, EarningsClaimResult.Status.NO_REWARD, buildEntry(habbo, userId, category, now));
}
String periodKey = periodKey(habbo, category, now, definition.cooldownSeconds());
try {
if (!this.claims.recordClaim(userId, category.getKey(), periodKey, now)) {
@@ -143,7 +147,11 @@ public class EarningsCenterManager {
return new EarningsEntry(category, true, claimable, 0, definition.rewards());
}
String periodKey = periodKey(now, definition.cooldownSeconds());
if (!isEligibleForProgressClaim(habbo, category)) {
return new EarningsEntry(category, true, false, 0, definition.rewards());
}
String periodKey = periodKey(habbo, category, now, definition.cooldownSeconds());
try {
claimable = !this.claims.hasClaim(userId, category.getKey(), periodKey);
@@ -186,6 +194,25 @@ public class EarningsCenterManager {
return this.config.getBoolean(CONFIG_PREFIX + category.getKey() + ".native.enabled", true);
}
private boolean isEligibleForProgressClaim(Habbo habbo, EarningsCategory category) {
if (!nativeEnabled(category) || habbo == null || habbo.getHabboStats() == null) {
return true;
}
if (category == EarningsCategory.ACHIEVEMENTS) {
int minimumScore = Math.max(1, this.config.getInt(CONFIG_PREFIX + category.getKey() + ".min_score", 1));
return habbo.getHabboStats().getAchievementScore() >= minimumScore;
}
if (category == EarningsCategory.LEVEL_PROGRESS) {
int minimumLevel = Math.max(0, this.config.getInt(CONFIG_PREFIX + category.getKey() + ".min_level", 1));
int highestLevel = Math.max(habbo.getHabboStats().citizenshipLevel, habbo.getHabboStats().helpersLevel);
return highestLevel >= minimumLevel;
}
return true;
}
private void addReward(List<EarningsReward> rewards, String type, int amount, int pointsType) {
int clampedAmount = Math.min(Math.max(0, amount), MAX_CONFIGURED_REWARD);
if (clampedAmount > 0) {
@@ -229,7 +256,19 @@ public class EarningsCenterManager {
return (int) (this.clock.instant().getEpochSecond());
}
private String periodKey(int now, int cooldownSeconds) {
private String periodKey(Habbo habbo, EarningsCategory category, int now, int cooldownSeconds) {
if (nativeEnabled(category) && habbo != null && habbo.getHabboStats() != null) {
if (category == EarningsCategory.ACHIEVEMENTS) {
int scoreStep = Math.max(1, this.config.getInt(CONFIG_PREFIX + category.getKey() + ".score.step", 100));
return "score:" + (habbo.getHabboStats().getAchievementScore() / scoreStep);
}
if (category == EarningsCategory.LEVEL_PROGRESS) {
int highestLevel = Math.max(habbo.getHabboStats().citizenshipLevel, habbo.getHabboStats().helpersLevel);
return "level:" + highestLevel;
}
}
return String.valueOf(now / cooldownSeconds);
}
+2
View File
@@ -82,6 +82,8 @@ For `points`, `pointsType` carries the currency type. For `badge`, `data` carrie
- `marketplace`: sold marketplace offers waiting for payout
- `hc_payday`: unclaimed rows in `logs_hc_payday`
- `achievements`: configured rewards gated by achievement score buckets
- `level_progress`: configured rewards gated by citizenship/helper talent level
## Result Status
@@ -67,6 +67,7 @@ Add emulator settings with safe defaults:
The feature defaults off so existing hotels do not receive surprise economy changes after deploying the jar.
Marketplace and HC payday default to native integrations once the feature is enabled, because both already have server-side claim ledgers.
Achievements and level progress use native eligibility by default: achievement score buckets and talent-track levels decide when the configured reward may be claimed.
## Packet Contract
@@ -94,6 +95,8 @@ Composer format is intentionally simple and renderer-friendly: category key, ena
- `claim all` processes only claimable rows and returns per-category results.
- Marketplace claims use the existing marketplace sold-offer payout path.
- HC payday claims use existing unclaimed `logs_hc_payday` rows.
- Achievement claims can be scoped to score buckets via `earnings.achievements.score.step`.
- Level progress claims can be scoped to the current highest citizenship/helper level.
## Tests