diff --git a/ACTIVITY-LORE.md b/ACTIVITY-LORE.md new file mode 100644 index 0000000..9a49d7a --- /dev/null +++ b/ACTIVITY-LORE.md @@ -0,0 +1,80 @@ +# Activity descriptions + +Activity `description` lines support three local placeholders: + +| Placeholder | Config value | Meaning | +| --- | --- | --- | +| `%every%` | `every` | Count required for one completion/award | +| `%points%` | `points` | Points earned per completion | +| `%daily-cap%` | `daily-cap` | Maximum awards per day; zero means uncapped | + +Values come from the loaded activity definition whenever the GUI renders. +After changing settings, reload ActivityTF and reopen the menu. Single strings +and lists both work; colours and unknown placeholders retain their existing +behaviour. These placeholders do not require PlaceholderAPI. + +`every` uses the tracked activity's unit: minutes for playtime, Denar for market +sales/card profit, XP for professions, output items for vanilla crafting, +and completed recipes for MMOItems stations. Station output stack sizes do +not affect credit. Configured `station-actions` aliases retain their credit +amounts (for example, the bulk Coal recipe counts as 16 standard crafts). + +The daily cap limits awards, not raw actions or points. The shared daily bar, +voting share and weekly bar may limit the points actually credited further. +Tasks must be revealed before their actions count. + +## Existing server configs + +The plugin does not overwrite existing descriptions. To prepare the audited +wording for an existing config, install PyYAML and run: + +```sh +python tools/update-activity-descriptions.py config.yml config-updated.yml +``` + +Review the resulting diff before deployment. This writes a separate file and +changes only active activity descriptions, preserving commented-out activities, +other settings, rewards and state. It refuses unknown activity types rather +than inventing instructions. Rerunning it is safe, but replaces custom active +descriptions with the audited templates. + +## Main audit (2026-09-30) + +All 37 active activities were checked against their event handlers and the +live MMOItems recipes. Commented activities were excluded. No activity amounts, +reward pools or Act System values change. + +| Activities | Correct explanation/unit | +| --- | --- | +| vote | Real votes; `/vote` links remain | +| playtime | Active minutes; idle minutes excluded when AFK checks are enabled | +| geiger | Collect the source with a Trace Detector; finding it alone does not count | +| archaeology_find | Recover finds, rather than merely start/finish a project | +| instrument | Notes played | +| market_sale | Denar earned, per award; old 100 was the daily total (50 twice) | +| casino_win | Positive card-game profit above stake before tax; old 200 was the daily total (100 twice) | +| ic_chat | Substantial in-character messages; short/repetitive/OOC chat excluded | +| injured | Receive an injury as the target, rather than injure someone else | +| vehicle_build | Finished vehicles at construction stations | +| battle_joined | Participation credited when a faction battle ends | +| advcraft_item | Completed Advanced Crafting items or alloys; five per award, two awards/day | +| cook_dish | Finished dishes at pots/stations/boards; trough feed is separate | +| profession_upgrade | Purchased profession upgrades | +| profession_crafter, profession_forager, profession_herborist | Profession XP; fixes Forager's `2x50` typo | +| ingot_flint, ingot_coal | Standard recipe crafts at a Blast Furnace; bulk Coal aliases still count | +| tool_iron_pickaxe, tool_iron_axe, tool_iron_hoe, tool_iron_shovel | Named recipe crafts at a Crafting Table | +| forester_arrow, forester_oak_log, forester_spruce_log | Named recipe crafts at a Fletching Table | +| alchemy_coal, alchemy_powder | Named recipe crafts at a Brewing Stand | +| instrument_lute | Lute recipe crafts at a Jukebox | +| research_scribe_paper | Unknown Research Paper recipe crafts at a Cartography Table | +| medicine_herb_mixture, medicine_splint | Willow-Bark Tincture/Splint recipe crafts at a Medicine Station | +| engineer_fuel | Arcane Fuel recipe crafts at an Engineer Station | +| fishing_rod | Basic Fishing Rod recipe only; other rods do not count | +| fishing_iron_hook | Iron Hook recipe crafts at a Fishing Station | +| animal_universal_feed | Collect finished trough feed, with an empty-hand right click | +| animal_llama | Llama Spawn Egg recipe crafts at an Animal Station | + +Recipe wording avoids stale output totals: Flint/Coal produce four per standard +craft, forester recipes produce 64, powder produces two, tincture/splint produce +eight and fuel produces 16. The old Splint lore said one item, while its recipe +produces eight. Placeholder craft counts remain correct if those outputs change. diff --git a/README.md b/README.md index d23c960..4f65186 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ Available activities depend on the gameplay plugins and activity definitions in ## Documentation +[Activity lore placeholders and config migration](ACTIVITY-LORE.md) + [Project documentation](https://github.com/TF-Minecraft/Docs/blob/main/projects/ActivityTF/README.md) Technical documentation is maintained in [TF-Minecraft/Docs](https://github.com/TF-Minecraft/Docs). diff --git a/src/main/java/net/tfminecraft/activitytf/gui/ActivityGui.java b/src/main/java/net/tfminecraft/activitytf/gui/ActivityGui.java index d0c026a..9e7d71e 100644 --- a/src/main/java/net/tfminecraft/activitytf/gui/ActivityGui.java +++ b/src/main/java/net/tfminecraft/activitytf/gui/ActivityGui.java @@ -232,7 +232,10 @@ private ItemStack activityItem(ActivityConfiguration config, Messages messages, static List activityLore(Messages messages, ActivityDef def, int count) { List lore = new ArrayList<>(); for (String line : def.description()) { - lore.add(Utils.colorize(line)); + lore.add(Utils.colorize(line + .replace("%every%", Integer.toString(def.every())) + .replace("%points%", Integer.toString(def.points())) + .replace("%daily-cap%", Integer.toString(def.dailyCap())))); } String progress = progressBar(def, count); if (progress != null) { diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 7a6ecf3..c21d417 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -113,12 +113,20 @@ reset: # stays a blank grey dye, so a description never gives away what it is. # Anything that is neither a string nor a list is reported at load and the # activity simply shows no description. +# Amount placeholders are replaced when the revealed task is rendered: +# %every% = actions per completion (minutes, Denar, XP or crafts as above) +# %points% = activity points awarded per completion +# %daily-cap% = maximum completions per day (0 means no activity cap) +# These are local lore placeholders, not PlaceholderAPI expansions. Use +# %every% for completed station recipes, not the number of output items. activities: vote: display: "#e6ca40&lVoting" description: - - "&7Vote for the server on any listed site." + - "&7Cast &a%every% &7votes for the server." - "&7Links: &f/vote" + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: v.paper every: 1 points: 1 @@ -128,26 +136,41 @@ activities: - "sudo %player% votelist" geiger: display: "#e6ca40&lRadioactive Sources" + description: + - "&7Collect &a%every% &7arcane sources with a Trace Detector." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: SPYGLASS every: 3 points: 1 daily-cap: 1 archaeology_find: display: "#e6ca40&lArchaeology" - description: "&7Brush suspicious sand and gravel for finds." + description: + - "&7Recover &a%every% &7archaeological finds." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: BRUSH every: 3 points: 1 daily-cap: 2 instrument: display: "#e6ca40&lMusical Instruments" + description: + - "&7Play &a%every% &7notes with an instrument." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: NOTE_BLOCK every: 20 points: 1 daily-cap: 1 playtime: display: "#e6ca40&lTime Played" - description: "&7Simply stay online and active." + description: + - "&7Play actively for &a%every% &7minutes." + - "&7Idle minutes do not count when AFK checks are enabled." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: CLOCK every: 60 # 60 counted minutes => 1 point points: 1 @@ -155,7 +178,10 @@ activities: # MarketBlock: denar earned selling to the market block ('every' is denar) market_sale: display: "#e6ca40&lMarket Sales" - description: "&7Earn denar selling goods to a market block." + description: + - "&7Earn &a%every% &7Denar selling to a Market Block." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: EMERALD every: 25 # 25 denar earned => 1 point points: 1 @@ -163,12 +189,21 @@ activities: # Games: denar won at the card tables ('every' is denar, net of stake, before tax) casino_win: display: "#e6ca40&lCasino Winnings" + description: + - "&7Win &a%every% &7Denar in card-game profit." + - "&7Counts profit above your stake, before tax." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: GOLD_NUGGET every: 25 points: 1 daily-cap: 2 craft_diamond_block: display: "#e6ca40&lCraft Diamond Blocks" + description: + - "&7Craft &a%every% &7Diamond Block items." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: DIAMOND_BLOCK every: 3 points: 1 @@ -176,6 +211,10 @@ activities: craft: DIAMOND_BLOCK craft_golden_carrot: display: "#e6ca40&lCraft Golden Carrots" + description: + - "&7Craft &a%every% &7Golden Carrot items." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: v.golden_carrot every: 16 points: 1 @@ -183,6 +222,10 @@ activities: craft: GOLDEN_CARROT craft_anvil: display: "#e6ca40&lCraft Anvils" + description: + - "&7Craft &a%every% &7Anvil items." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: ANVIL every: 1 points: 1 @@ -191,6 +234,11 @@ activities: # RPCharacters: in-character chat messages ic_chat: display: "#e6ca40&lRoleplay Chat" + description: + - "&7Send &a%every% &7substantial in-character messages." + - "&7Short or repetitive messages do not count." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: WRITABLE_BOOK every: 30 points: 1 @@ -198,6 +246,10 @@ activities: # RPCharacters: injured (as the target, not the attacker) injured: display: "#e6ca40&lInjure Yourself" + description: + - "&7Receive &a%every% &7roleplay injuries." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: BONE every: 1 points: 5 @@ -205,6 +257,10 @@ activities: # InteractibleFurniture: furniture placed in the world furniture_place: display: "#e6ca40&lPlace Furniture" + description: + - "&7Place &a%every% &7pieces of furniture." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: BARREL every: 5 points: 1 @@ -212,6 +268,10 @@ activities: # VFBuilders: a vehicle finished at a construction station vehicle_build: display: "#e6ca40&lBuild Vehicles" + description: + - "&7Finish &a%every% &7vehicles at a construction station." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: MINECART every: 1 points: 1 @@ -219,6 +279,11 @@ activities: # SimpleFactions: every participant of every ended battle battle_joined: display: "#e6ca40&lFaction Battles" + description: + - "&7Participate in &a%every% &7faction battles." + - "&7Credit is awarded when the battle ends." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: IRON_SWORD every: 1 points: 1 @@ -226,6 +291,10 @@ activities: # AdvancedCrafting: items and alloys crafted at its stations advcraft_item: display: "#e6ca40&lAdvanced Crafting" + description: + - "&7Complete &a%every% &7Advanced Crafting items or alloys." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: SMITHING_TABLE every: 5 points: 1 @@ -233,7 +302,11 @@ activities: # Cooking: dishes finished at pots, stations and crafting boards cook_dish: display: "#e6ca40&lCook Dishes" - description: "&7Finish dishes at a cooking pot, station or board." + description: + - "&7Finish &a%every% &7dishes at cooking pots, stations or boards." + - "&7Trough feed counts toward its own activity." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: COOKED_BEEF every: 5 points: 1 @@ -241,6 +314,10 @@ activities: # RPCharacters: profession upgrades purchased profession_upgrade: display: "#e6ca40&lProfession Upgrade" + description: + - "&7Purchase &a%every% &7profession upgrades." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: EXPERIENCE_BOTTLE every: 1 points: 1 @@ -249,6 +326,10 @@ activities: # they show in /activity but can never be filled. profession_crafter: display: "#e6ca40&lCrafter XP" + description: + - "&7Earn &a%every% &7Crafter profession XP." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: CRAFTING_TABLE every: 500 # 500 profession XP → 1 point points: 1 @@ -256,6 +337,10 @@ activities: profession: crafter profession_forager: display: "#e6ca40&lForager XP" + description: + - "&7Earn &a%every% &7Forager profession XP." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: WHEAT every: 500 points: 1 @@ -263,6 +348,10 @@ activities: profession: forager profession_herborist: display: "#e6ca40&lHerborist XP" + description: + - "&7Earn &a%every% &7Herborist profession XP." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: FERN every: 500 points: 1 @@ -271,6 +360,11 @@ activities: # Ingot station ingot_flint: display: "#e6ca40&lExtract Flint" + description: + - "&7Complete the Flint recipe &a%every% &7times." + - "&7Use a Blast Furnace and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: FLINT every: 3 points: 1 @@ -278,6 +372,11 @@ activities: station: ingot-station/flint ingot_coal: display: "#e6ca40&lMake Coal" + description: + - "&7Complete &a%every% &7standard Coal crafts at a Blast Furnace." + - "&7The bulk Coal recipe also counts toward this task." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: COAL every: 3 points: 1 @@ -288,6 +387,11 @@ activities: # Tool station tool_iron_pickaxe: display: "#e6ca40&lForge Iron Pickaxes" + description: + - "&7Complete the Iron Pickaxe recipe &a%every% &7times." + - "&7Use a Crafting Table and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: IRON_PICKAXE every: 1 points: 1 @@ -295,6 +399,11 @@ activities: station: tool-station/iron-pickaxe tool_iron_axe: display: "#e6ca40&lForge Iron Axes" + description: + - "&7Complete the Iron Hatchet recipe &a%every% &7times." + - "&7Use a Crafting Table and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: IRON_AXE every: 1 points: 1 @@ -303,6 +412,11 @@ activities: # Block station block_andesite: display: "#e6ca40&lCut Andesite" + description: + - "&7Complete the Andesite recipe &a%every% &7times." + - "&7Use a Stonecutter and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: ANDESITE every: 2 points: 1 @@ -310,6 +424,11 @@ activities: station: block-station/andesite block_clay: display: "#e6ca40&lCut Clay" + description: + - "&7Complete the Clay recipe &a%every% &7times." + - "&7Use a Stonecutter and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: CLAY every: 2 points: 1 @@ -318,6 +437,11 @@ activities: # Forester station forester_arrow: display: "#e6ca40&lFletch Arrows" + description: + - "&7Complete the Arrow recipe &a%every% &7times." + - "&7Use a Fletching Table and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: ARROW every: 2 points: 1 @@ -325,6 +449,11 @@ activities: station: forester-station/arrow forester_string: display: "#e6ca40&lSpin String" + description: + - "&7Complete the String recipe &a%every% &7times." + - "&7Use a Fletching Table and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: STRING every: 2 points: 1 @@ -333,6 +462,11 @@ activities: # Alchemy station alchemy_minor_health: display: "#e6ca40&lBrew Minor Health" + description: + - "&7Complete the Minor Health Potion recipe &a%every% &7times." + - "&7Use a Brewing Stand and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: POTION every: 2 points: 1 @@ -340,6 +474,11 @@ activities: station: alchemy-station/minor-health alchemy_powder: display: "#e6ca40&lGrind Alchemy Powder" + description: + - "&7Complete the Alchemy Powder recipe &a%every% &7times." + - "&7Use a Brewing Stand and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: GLOWSTONE_DUST every: 2 points: 1 @@ -348,6 +487,11 @@ activities: # Instrument station instrument_iron_lute: display: "#e6ca40&lBuild Iron Lutes" + description: + - "&7Complete the Iron Lute recipe &a%every% &7times." + - "&7Use a Jukebox and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: NOTE_BLOCK every: 1 points: 1 @@ -355,6 +499,11 @@ activities: station: instrument-station/iron-lute instrument_steel_lute: display: "#e6ca40&lBuild Steel Lutes" + description: + - "&7Complete the Steel Lute recipe &a%every% &7times." + - "&7Use a Jukebox and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: JUKEBOX every: 1 points: 1 @@ -363,6 +512,11 @@ activities: # Research station research_scribe_paper: display: "#e6ca40&lScribe Paper" + description: + - "&7Complete the Scribe Paper recipe &a%every% &7times." + - "&7Use a Cartography Table and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: PAPER every: 2 points: 1 @@ -370,6 +524,11 @@ activities: station: research-station/scribe-paper research_bronze: display: "#e6ca40&lResearch Bronze" + description: + - "&7Complete the Research Bronze recipe &a%every% &7times." + - "&7Use a Cartography Table and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: COPPER_INGOT every: 1 points: 1 @@ -378,6 +537,11 @@ activities: # Medicine station medicine_herb_mixture: display: "#e6ca40&lMix Herbs" + description: + - "&7Complete the Herb Mixture recipe &a%every% &7times." + - "&7Use a Medicine Station and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: FERN every: 2 points: 1 @@ -385,6 +549,11 @@ activities: station: medicine-station/herb-mixture medicine_splint: display: "#e6ca40&lMake Splints" + description: + - "&7Complete the Splint recipe &a%every% &7times." + - "&7Use a Medicine Station and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: STICK every: 1 points: 1 @@ -393,6 +562,11 @@ activities: # Engineer station engineer_bullet_box: display: "#e6ca40&lPack Bullet Boxes" + description: + - "&7Complete the Bullet Box recipe &a%every% &7times." + - "&7Use a Engineer Station and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: IRON_NUGGET every: 1 points: 1 @@ -400,6 +574,11 @@ activities: station: engineer-station/bullet-box engineer_fuel: display: "#e6ca40&lRefine Fuel" + description: + - "&7Complete the Arcane Fuel recipe &a%every% &7times." + - "&7Use a Engineer Station and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: COAL_BLOCK every: 2 points: 1 @@ -408,6 +587,11 @@ activities: # Fishing station fishing_rod: display: "#e6ca40&lMake Fishing Rods" + description: + - "&7Complete the Basic Fishing Rod recipe &a%every% &7times." + - "&7Use a Fishing Station and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: FISHING_ROD every: 1 points: 1 @@ -415,6 +599,11 @@ activities: station: fishing-station/fishing-rod fishing_iron_hook: display: "#e6ca40&lForge Iron Hooks" + description: + - "&7Complete the Iron Hook recipe &a%every% &7times." + - "&7Use a Fishing Station and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: TRIPWIRE_HOOK every: 2 points: 1 @@ -423,6 +612,11 @@ activities: # Magic station magic_basic_handle: display: "#e6ca40&lCarve Basic Handles" + description: + - "&7Complete the Basic Handle recipe &a%every% &7times." + - "&7Use a Magic Station and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: STICK every: 1 points: 1 @@ -430,6 +624,11 @@ activities: station: magic-station/basic-handle magic_iron_core: display: "#e6ca40&lCast Iron Cores" + description: + - "&7Complete the Iron Core recipe &a%every% &7times." + - "&7Use a Magic Station and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: IRON_INGOT every: 1 points: 1 @@ -438,14 +637,23 @@ activities: # Cooking trough: fill with wheat/vegetables, then empty-hand click to mix. animal_universal_feed: display: "#e6ca40&lMix Universal Feed" + description: + - "&7Fill a trough with wheat or vegetables." + - "&7Collect feed &a%every% &7times with an empty-hand right click." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: WHEAT every: 2 points: 1 daily-cap: 1 - description: "Fill a trough with wheat or vegetables and collect the feed." # Animal station animal_whistle: display: "#e6ca40&lCraft Animal Whistles" + description: + - "&7Complete the Animal Whistle recipe &a%every% &7times." + - "&7Use a Animal Station and claim queued crafts." + - "&7Earn &a%points% &7activity points per completion." + - "&7" material: GOAT_HORN every: 1 points: 1 diff --git a/src/test/java/net/tfminecraft/activitytf/config/DefaultResourcesTest.java b/src/test/java/net/tfminecraft/activitytf/config/DefaultResourcesTest.java index d23520f..0eb158f 100644 --- a/src/test/java/net/tfminecraft/activitytf/config/DefaultResourcesTest.java +++ b/src/test/java/net/tfminecraft/activitytf/config/DefaultResourcesTest.java @@ -136,6 +136,16 @@ void theShippedMilestonesParseAsNumbers() { assertEquals(List.of(10, 20), config.getIntegerList("bar.milestones")); } + @Test + void everyShippedActivityExplainsItsConfiguredAmountAndPoints() { + ConfigurationSection activities = load("config.yml").getConfigurationSection("activities"); + for (String id : activities.getKeys(false)) { + String description = String.join(" ", activities.getStringList(id + ".description")); + assertTrue(description.contains("%every%"), id + " lacks its action amount"); + assertTrue(description.contains("%points%"), id + " lacks its point award"); + } + } + @Test void everyRewardPoolEntryIsUsable() { YamlConfiguration config = load("config.yml"); diff --git a/src/test/java/net/tfminecraft/activitytf/gui/ActivityGuiTest.java b/src/test/java/net/tfminecraft/activitytf/gui/ActivityGuiTest.java index f07e240..1150ac8 100644 --- a/src/test/java/net/tfminecraft/activitytf/gui/ActivityGuiTest.java +++ b/src/test/java/net/tfminecraft/activitytf/gui/ActivityGuiTest.java @@ -215,6 +215,31 @@ private static ActivityDef described(List description) { return new ActivityDef("a", "&aA", Material.PAPER, null, 25, 1, 5, List.of(), description); } + @Test + void descriptionAmountsFollowTheCurrentActivityDefinition() { + List description = List.of( + "&7Earn %points% points per %every% actions; %daily-cap% awards/day.", + "%every%/%every% %unknown%"); + ActivityDef original = new ActivityDef("a", "A", Material.PAPER, null, + 25, 1, 2, List.of(), description); + ActivityDef changed = new ActivityDef("a", "A", Material.PAPER, null, + 60, 5, 3, List.of(), description); + assertEquals(Utils.colorize("&7Earn 1 points per 25 actions; 2 awards/day."), + ActivityGui.activityLore(shippedMessages(), original, 0).getFirst()); + assertEquals(Utils.colorize("&7Earn 5 points per 60 actions; 3 awards/day."), + ActivityGui.activityLore(shippedMessages(), changed, 0).getFirst()); + assertEquals("60/60 %unknown%", + ActivityGui.activityLore(shippedMessages(), changed, 0).get(1)); + assertEquals(description, changed.description()); + } + + @Test + void uncappedDescriptionsShowTheConfiguredZeroCap() { + ActivityDef def = new ActivityDef("a", "A", Material.PAPER, null, + 1, 1, 0, List.of(), List.of("%every% %points% %daily-cap%")); + assertEquals("1 1 0", ActivityGui.activityLore(shippedMessages(), def, 0).getFirst()); + } + @Test void theDescriptionSitsAboveTheProgressAndTodayLines() { Messages messages = shippedMessages(); diff --git a/tools/test_activity_descriptions.py b/tools/test_activity_descriptions.py new file mode 100644 index 0000000..2b634d5 --- /dev/null +++ b/tools/test_activity_descriptions.py @@ -0,0 +1,73 @@ +import importlib.util +from pathlib import Path +import unittest + +import yaml + +spec = importlib.util.spec_from_file_location( + "lore", Path(__file__).with_name("update-activity-descriptions.py")) +lore = importlib.util.module_from_spec(spec) +spec.loader.exec_module(lore) + + +class MigrationTest(unittest.TestCase): + def test_preserves_commented_activities_settings_and_custom_rewards(self): + source = '''reset: + hour: 3 +activities: + vote: + display: "Voting" + description: "Old explanation" + every: 2 + points: 7 + daily-cap: 0 + # furniture_place: + # description: "Keep this comment" + # every: 99 + profession_forager: + description: + - "Typo 2x50" + - "" + profession: forager + every: 250 +rewards: + multiplier: 2 +''' + updated = lore.update(source) + self.assertEqual(lore.update(updated), updated) + comments = lambda text: [line for line in text.splitlines() if line.lstrip().startswith("#")] + self.assertEqual(comments(source), comments(updated)) + parsed = yaml.safe_load(updated) + self.assertEqual(parsed["activities"]["vote"]["every"], 2) + self.assertEqual(parsed["activities"]["vote"]["points"], 7) + self.assertEqual(parsed["activities"]["vote"]["daily-cap"], 0) + self.assertIn("%every%", parsed["activities"]["profession_forager"]["description"][0]) + self.assertEqual(parsed["rewards"], {"multiplier": 2}) + + def test_rejects_unknown_activity_instead_of_misdescribing_it(self): + with self.assertRaisesRegex(ValueError, "No audited description"): + lore.update("activities:\n custom:\n every: 1\n") + + def test_accepts_supported_station_formats(self): + for station in [" TOOL-STATION / IRON-PICKAXE ", "tool-station"]: + with self.subTest(station=station): + source = "activities:\n custom:\n station: " + repr(station) + "\n every: 2\n" + description = yaml.safe_load(lore.update(source))["activities"]["custom"]["description"] + self.assertIn("Crafting Table", " ".join(description)) + self.assertIn("%every%", " ".join(description)) + + def test_replaces_complete_yaml_values_and_keeps_comments(self): + for value in ['"Old"', '\n - "Old"\n # Keep this comment\n - "Another"', + '|\n Old multiline\n explanation', '>\n Old folded\n explanation']: + with self.subTest(value=value): + source = 'activities:\n vote:\n display: Voting\n description: ' + value + '\n every: 2\n' + updated = lore.update(source) + parsed = yaml.safe_load(updated)["activities"]["vote"] + self.assertEqual(parsed["every"], 2) + self.assertNotIn("Old", updated) + if "# Keep this comment" in source: + self.assertIn("# Keep this comment", updated) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/update-activity-descriptions.py b/tools/update-activity-descriptions.py new file mode 100644 index 0000000..b5b0079 --- /dev/null +++ b/tools/update-activity-descriptions.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Prepare audited activity lore without rewriting settings or YAML comments. + +Requires PyYAML. Writes a separate output file; never edits the input in place. +""" +import argparse +import json +import re +from pathlib import Path + +import yaml + + +ACTIONS = { + "vote": ["&7Cast &a%every% &7votes for the server.", "&7Links: &f/vote"], + "playtime": ["&7Play actively for &a%every% &7minutes.", "&7Idle minutes do not count when AFK checks are enabled."], + "geiger": ["&7Collect &a%every% &7arcane sources with a Trace Detector."], + "archaeology_find": ["&7Recover &a%every% &7archaeological finds."], + "instrument": ["&7Play &a%every% &7notes with an instrument."], + "market_sale": ["&7Earn &a%every% &7Denar selling to a Market Block."], + "casino_win": ["&7Win &a%every% &7Denar in card-game profit.", "&7Counts profit above your stake, before tax."], + "ic_chat": ["&7Send &a%every% &7substantial in-character messages.", "&7Short or repetitive messages do not count."], + "injured": ["&7Receive &a%every% &7roleplay injuries."], + "furniture_place": ["&7Place &a%every% &7pieces of furniture."], + "vehicle_build": ["&7Finish &a%every% &7vehicles at a construction station."], + "battle_joined": ["&7Participate in &a%every% &7faction battles.", "&7Credit is awarded when the battle ends."], + "advcraft_item": ["&7Complete &a%every% &7Advanced Crafting items or alloys."], + "cook_dish": ["&7Finish &a%every% &7dishes at cooking pots, stations or boards.", "&7Trough feed counts toward its own activity."], + "profession_upgrade": ["&7Purchase &a%every% &7profession upgrades."], + "animal_universal_feed": ["&7Fill a trough with wheat or vegetables.", "&7Collect feed &a%every% &7times with an empty-hand right click."], +} + +# Recipe names and station locations, checked against Main's active recipes. +# Count completed recipes, not output stack sizes. Bulk aliases retain the +# configured station-actions credit; the updater never changes those values. +STATIONS = { + "ingot-station": "Blast Furnace", + "tool-station": "Crafting Table", + "block-station": "Stonecutter", + "forester-station": "Fletching Table", + "alchemy-station": "Brewing Stand", + "instrument-station": "Jukebox", + "research-station": "Cartography Table", + "medicine-station": "Medicine Station", + "engineer-station": "Engineer Station", + "fishing-station": "Fishing Station", + "magic-station": "Magic Station", + "animal-station": "Animal Station", +} +RECIPE_NAMES = { + "iron-axe": "Iron Hatchet", + "minor-health": "Minor Health Potion", + "r-rare-research-paper": "Unknown Research Paper", + "r-bronze": "Research Bronze", + "antibiotics": "Willow-Bark Tincture", + "fuel": "Arcane Fuel", + "fishing-rod": "Basic Fishing Rod", + "llama-egg": "Llama Spawn Egg", +} + + +def description(activity_id, activity): + if activity_id in ACTIONS: + lines = ACTIONS[activity_id] + elif activity.get("profession"): + name = activity["profession"].replace("_", " ").title() + lines = [f"&7Earn &a%every% &7{name} profession XP."] + elif activity.get("station"): + station, separator, recipe = activity["station"].strip().lower().partition("/") + station, recipe = station.strip(), recipe.strip() + if station not in STATIONS: + raise ValueError(f"No audited station location for {station}") + if separator: + name = RECIPE_NAMES.get(recipe, recipe.replace("-", " ").title()) + lines = [f"&7Complete the {name} recipe &a%every% &7times.", + f"&7Use a {STATIONS[station]} and claim queued crafts."] + else: + lines = [f"&7Complete &a%every% &7recipes at a {STATIONS[station]}.", + "&7Claim queued crafts to receive credit."] + if activity_id == "ingot_coal" and "ingot-station/coal-64" in activity.get("station-actions", {}): + lines = ["&7Complete &a%every% &7standard Coal crafts at a Blast Furnace.", + "&7The bulk Coal recipe also counts toward this task."] + elif activity.get("craft"): + name = activity["craft"].removeprefix("v.").replace("_", " ").title() + lines = [f"&7Craft &a%every% &7{name} items."] + else: + raise ValueError(f"No audited description for {activity_id}") + return [*lines, "&7Earn &a%points% &7activity points per completion.", "&7"] + + +def remove_description(block): + node = yaml.compose(block) + for key, value in node.value: + if key.value != "description": + continue + start = block.rfind("\n", 0, key.start_mark.index) + 1 + end = value.end_mark.index + last_start = block.rfind("\n", 0, end) + 1 + if block[last_start:end].strip(): + newline = block.find("\n", end) + end = len(block) if newline < 0 else newline + 1 + else: + end = last_start + comments = "".join(line for line in block[start:end].splitlines(keepends=True) + if line.lstrip().startswith("#")) + return block[:start] + comments + block[end:] + return block + + +def update(source): + config = yaml.safe_load(source) + activities = config["activities"] + section = re.search(r"(?ms)^activities:\s*\n(.*?)(?=^[^\s#]|\Z)", source) + if section is None: + raise ValueError("Cannot locate activities section") + body = section[1] + headers = list(re.finditer(r"(?m)^ ([a-zA-Z0-9_-]+):[^\n]*\n", body)) + if {m[1] for m in headers} != set(activities): + raise ValueError("Activity indentation is unsupported; expected two spaces") + for index in reversed(range(len(headers))): + header = headers[index] + end = headers[index + 1].start() if index + 1 < len(headers) else len(body) + block = body[header.end():end] + # Parsed node marks cover strings, block scalars and either list indent. + block = remove_description(block) + lore = " description:\n" + "".join( + " - " + json.dumps(line, ensure_ascii=False) + "\n" + for line in description(header[1], activities[header[1]])) + display = re.search(r"(?m)^ display:[^\n]*\n", block) + insert = display.end() if display else 0 + block = block[:insert] + lore + block[insert:] + body = body[:header.end()] + block + body[end:] + result = source[:section.start(1)] + body + source[section.end(1):] + # Verify that no setting outside activity descriptions changed. + updated = yaml.safe_load(result) + for activity in updated["activities"].values(): + activity.pop("description", None) + for activity in config["activities"].values(): + activity.pop("description", None) + if updated != config: + raise ValueError("Unexpected non-description change") + return result + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("input", type=Path) + parser.add_argument("output", type=Path) + args = parser.parse_args() + if args.input.resolve() == args.output.resolve(): + parser.error("Input and output must be separate files") + args.output.write_text(update(args.input.read_text(encoding="utf-8")), encoding="utf-8")