diff --git a/CHANGELOG.md b/CHANGELOG.md index 23091d9..2d62f76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,12 @@ Each release should be readable in two passes: ### Player-Facing -- No unreleased player-facing changes yet. +- Journeyman cartographers now sell an Air Temple Explorer Map for 12 emeralds and a compass after the world reaches Stage 2. The map marks the nearest Air Temple with an exact X. ### Technical -- No unreleased technical changes yet. +- Added a data-driven Air Temple map-destination tag and a server-authoritative cartographer interaction hook. The hook preserves existing saved offers, supports already-generated cartographers in upgraded worlds, and avoids duplicate map trades. +- Added a GameTest covering Stage 1 gating, Stage 2 availability, preservation of existing saved offers, map marking, trade cost, and duplicate prevention. ## 0.2.1 - 2026-07-20 diff --git a/README.md b/README.md index 68f959e..6c7b3fb 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Only the locations linked above are official downloads. Version settings are def The current build includes the survival spine, Water and Air progression, staged world changes, Aender foundations, recipe discovery, and the Retold mob-behavior framework. Major unfinished areas include: -- natural Air Temple discovery and further Gale Core tuning +- wider seed, upgraded-world, server, and multiplayer verification of the cartographer Air Temple map, plus further Gale Core tuning - Fire and Earth element paths - original replacements for provisional Aender assets and naming - Aender travel networks and late-game rewards diff --git a/ROADMAP.md b/ROADMAP.md index 75b1a6a..9817a54 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,7 +4,7 @@ This concise public roadmap may change as features are designed and tested. Deta ## Now -- Give the Air Temple a natural, in-world discovery path. +- Verify and refine the initial Stage 2 cartographer explorer-map path for finding Air Temples in fresh and upgraded worlds. - Continue tuning and testing the Gale Core encounter and Air Element reward. - Verify Aender portals, terrain regeneration, stabilizers, and progression in fresh worlds, upgraded worlds, dedicated servers, and multiplayer. - Replace provisional Aender naming and visual assets as final designs become available. diff --git a/docs/internal/design_implementation_status.md b/docs/internal/design_implementation_status.md index 4ed6ecc..5701c81 100644 --- a/docs/internal/design_implementation_status.md +++ b/docs/internal/design_implementation_status.md @@ -208,7 +208,7 @@ Largest missing or partial design areas: | Strongholds kept to 3 | Not implemented / unclear | Still planned. | | Villages spawn farther/scarcer | Not implemented / unclear | `NoVillageNearWorldSpawnMixin` exists, but full village scarcity design not confirmed here. | | Jungle/desert pyramids as boss tombs | Deferred/TBD | Maybe still planned, but design is not final. | -| Four element temples/challenges | Partial | Water/ocean path exists. Air has a locatable Stage 2 temple with floating islands, wind hazard, Breezes, and a WIP Gale Core boss that drops the Air Element. GameTests cover activation eligibility, damage aggro, the one-way Phase 2 transition, disengagement, state serialization, and duplicate-spawn repair; the encounter is playable but not finished or tuned in-game. Fire and earth temples are missing. Element order should be free. | +| Four element temples/challenges | Partial | Water/ocean path exists. Air has a locatable Stage 2 temple with floating islands, wind hazard, Breezes, and a WIP Gale Core boss that drops the Air Element. Stage 2 journeyman cartographers gain a provisional explorer-map trade with an exact temple marker, including cartographers whose offers already existed before the stage change. GameTests cover the trade's stage gate and uniqueness plus boss activation, damage aggro, the one-way Phase 2 transition, disengagement, state serialization, and duplicate-spawn repair. The basic in-game map path was developer-verified on 2026-07-22; wider seed, upgraded-world retrogen, dedicated-server, and multiplayer verification remains. The encounter is playable but not finished or tuned in-game. Fire and earth temples are missing. Element order should be free. | | Sunflowers point to significant structure | Not implemented | ## Ocean Monument And Water Element diff --git a/docs/internal/retold_design_risks.md b/docs/internal/retold_design_risks.md index 920535e..7ba5e5c 100644 --- a/docs/internal/retold_design_risks.md +++ b/docs/internal/retold_design_risks.md @@ -7,7 +7,8 @@ | Area | Risk / open item | Current note | | --- | --- | --- | | Progression | Stage 1 Wither/Nether star End access gating is undecided. | Do not implement until developer decides. | -| Elements | Fire and Earth still need full item/challenge paths. | Water has the ocean monument path. Air has a WIP Air Temple/Gale Core acquisition path; deterministic GameTests cover its critical state transitions, but encounter pacing, readability, and balance still need in-game tuning. | +| Elements | Fire and Earth still need full item/challenge paths. | Water has the ocean monument path. Air has a WIP Air Temple/Gale Core acquisition path and a provisional Stage 2 cartographer map with an exact marker. Deterministic GameTests cover the map trade's stage gate and uniqueness plus critical encounter state transitions. The basic in-game map path was developer-verified on 2026-07-22; wider seed, upgraded-world retrogen, dedicated-server, and multiplayer verification remains. Encounter pacing, readability, and balance still need in-game tuning. | +| Air Temple discovery | An explorer map could select a delayed Air Temple start that retrogen later suppresses because an upgraded-world chunk was edited. The initial synchronous structure search and biome-preview rendering may also cause a noticeable interaction stall. | Basic cartographer interaction and exact-map behavior were developer-verified on 2026-07-22. Failed searches are throttled for five minutes per cartographer. In wider fresh and upgraded test worlds, measure the first interaction and confirm the marker leads to a generated tower, including a world where a nearby deferred start is permanently skipped. | | Aender | The new horizontal portal, 8:1 scaling, counterpart creation, and volatile arrival preparation need fresh-world, existing-world, rapid re-entry, high-view-distance, and multiplayer verification. | Deterministic GameTests now cover shape lifecycle, scaling, counterpart creation/indexing, stability serialization, and stable-versus-volatile regeneration policy. The GameTest server does not load the custom Aender dimension, so real cross-dimension and multiplayer verification remains required; `dev_aender_portal_frame` is explicitly a provisional block name/design. | | Aender | A separate in-dimension teleportation network and late-game rewards are still planned. | Cross-dimension travel is implemented; remaining travel/reward design needs clarification before code. | | End | End City/outer End removal needs fresh-world and existing-world verification. | Data/mixins exist, but generated-world behavior should be tested. | diff --git a/docs/internal/retold_mod_system.md b/docs/internal/retold_mod_system.md index c3b994e..bfe50b2 100644 --- a/docs/internal/retold_mod_system.md +++ b/docs/internal/retold_mod_system.md @@ -222,6 +222,7 @@ Technical owners: - `AirTemplePiece` - `AirTempleIslandGenerator` - `AirTempleTowerGenerator` +- `AirTempleDiscoveryEvents` - `AirTempleWindEvents` - `AirTempleWindData` - `AirTempleBreezeSpawner` @@ -233,6 +234,8 @@ Technical owners: Current behavior: - `/locate structure retold:air_temple` finds the structure. +- After Stage 2 begins, journeyman-or-higher cartographers sell an Air Temple Explorer Map for 12 emeralds and a compass. It marks the nearest Air Temple with an exact X. +- The map trade is appended on server-side interaction so saved cartographers from before the stage transition keep their existing offers and gain the trade without a profession reroll. - It generates in `frozen_peaks`, `jagged_peaks`, and `stony_peaks`. - It builds a floating island, satellite islands, carved crater, and open tuff/copper tower. - It creates a large horizontal wind zone around the island and tower. @@ -253,11 +256,11 @@ Gale Core state: - If the boss or target leaves the combat area, the boss clears aggro and returns to its stored tower-top home. - Gale Core-owned wind-charge impacts do not deal health damage, but they crack and eventually break nearby breakable blocks. - The block-breaking splash skips air, fluids, block entities, unbreakable blocks, and very hard blocks, and breaks blocks without drops. -- GameTests cover activation eligibility, damage-triggered aggro, the one-way Phase 2 transition, disengagement and reactivation, state serialization, and duplicate-spawn repair. +- GameTests cover the cartographer trade's stage gate, exact marker, cost, and duplicate prevention, plus activation eligibility, damage-triggered aggro, the one-way Phase 2 transition, disengagement and reactivation, state serialization, and duplicate-spawn repair. Current limitation: -The Air challenge has a playable boss/reward spine, but it is still WIP. Tune attacks, movement, pacing, phase readability, block-breaking rules, telegraphs, tower layout, Breeze spawns, and traversal readability before treating the encounter as final. +The Air challenge has a playable boss/reward spine and an initial discovery path, but it is still WIP. The basic cartographer interaction and exact-map behavior were developer-verified in-game on 2026-07-22. The provisional path still needs wider seed, upgraded-world retrogen, dedicated-server, and multiplayer verification. Tune attacks, movement, pacing, phase readability, block-breaking rules, telegraphs, tower layout, Breeze spawns, and traversal readability before treating the encounter as final. ## Aender Dimension diff --git a/docs/internal/retold_roadmap.md b/docs/internal/retold_roadmap.md index 3cda84c..836fbec 100644 --- a/docs/internal/retold_roadmap.md +++ b/docs/internal/retold_roadmap.md @@ -22,7 +22,7 @@ Retold is still built around: These are the strongest next design-aligned areas: 1. Finish the four-element progression model. -2. Add missing Fire and Earth element item/challenge paths, and continue tuning the Air Temple/Gale Core path. +2. Add missing Fire and Earth element item/challenge paths, verify the initial cartographer Air Temple discovery map, and continue tuning the Air Temple/Gale Core path. 3. Decide whether Stage 1 needs Wither/Nether star End portal activation. 4. Add remaining Aender in-dimension teleportation and late-game travel/building rewards. 5. Replace the provisional `dev_aender_portal_frame` name/assets when the final portal-frame design is chosen. diff --git a/src/main/java/cz/xefensor/retold/gametest/RetoldGameTests.java b/src/main/java/cz/xefensor/retold/gametest/RetoldGameTests.java index 5b8e6bc..7bd5d47 100644 --- a/src/main/java/cz/xefensor/retold/gametest/RetoldGameTests.java +++ b/src/main/java/cz/xefensor/retold/gametest/RetoldGameTests.java @@ -16,6 +16,7 @@ import cz.xefensor.retold.stage.RetoldStageRuntime; import cz.xefensor.retold.stage.RetoldWorldData; import cz.xefensor.retold.stage.RetoldWorldStage; +import cz.xefensor.retold.worldgen.air.RetoldAirTempleDiscoveryGameTests; import cz.xefensor.retold.worldgen.air.RetoldGaleCoreGameTests; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; @@ -94,6 +95,7 @@ public static void register(RegisterGameTestsEvent event) { RetoldAenderGameTests.register(event, environment); RetoldAiSightCacheGameTests.register(event, environment); + RetoldAirTempleDiscoveryGameTests.register(event, environment); RetoldGaleCoreGameTests.register(event, environment); } diff --git a/src/main/java/cz/xefensor/retold/module/RetoldWorldgenModule.java b/src/main/java/cz/xefensor/retold/module/RetoldWorldgenModule.java index ba4952f..dde58eb 100644 --- a/src/main/java/cz/xefensor/retold/module/RetoldWorldgenModule.java +++ b/src/main/java/cz/xefensor/retold/module/RetoldWorldgenModule.java @@ -2,6 +2,7 @@ import cz.xefensor.retold.worldgen.RetoldWorldSpawnCache; import cz.xefensor.retold.worldgen.RetoldWorldgenRegistries; +import cz.xefensor.retold.worldgen.air.AirTempleDiscoveryEvents; import cz.xefensor.retold.worldgen.air.GaleCoreAttackEvents; import cz.xefensor.retold.worldgen.air.wind.AirTempleWindEvents; import cz.xefensor.retold.worldgen.delayed.RetoldAttachments; @@ -23,6 +24,7 @@ public static void registerModBus(IEventBus modEventBus) { public static void registerGameBus(IEventBus gameEventBus) { gameEventBus.register(RetoldWorldSpawnCache.class); + gameEventBus.register(AirTempleDiscoveryEvents.class); gameEventBus.register(AirTempleWindEvents.class); gameEventBus.register(GaleCoreAttackEvents.class); gameEventBus.register(RetoldChunkEditEvents.class); diff --git a/src/main/java/cz/xefensor/retold/worldgen/air/AirTempleDiscoveryEvents.java b/src/main/java/cz/xefensor/retold/worldgen/air/AirTempleDiscoveryEvents.java new file mode 100644 index 0000000..cad0a7b --- /dev/null +++ b/src/main/java/cz/xefensor/retold/worldgen/air/AirTempleDiscoveryEvents.java @@ -0,0 +1,187 @@ +package cz.xefensor.retold.worldgen.air; + +import cz.xefensor.retold.Retold; +import cz.xefensor.retold.stage.RetoldWorldData; +import cz.xefensor.retold.stage.RetoldWorldStage; +import net.minecraft.core.BlockPos; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.TagKey; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.npc.villager.Villager; +import net.minecraft.world.entity.npc.villager.VillagerProfession; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.MapItem; +import net.minecraft.world.item.component.MapDecorations; +import net.minecraft.world.item.trading.ItemCost; +import net.minecraft.world.item.trading.MerchantOffer; +import net.minecraft.world.item.trading.MerchantOffers; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.saveddata.maps.MapDecorationTypes; +import net.minecraft.world.level.saveddata.maps.MapItemSavedData; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent; +import net.neoforged.neoforge.event.server.ServerStartingEvent; + +import java.util.Map; +import java.util.Optional; +import java.util.WeakHashMap; + +public final class AirTempleDiscoveryEvents { + static final String MAP_DECORATION_KEY = "retold:air_temple"; + private static final String MAP_NAME_KEY = "filled_map.retold.air_temple"; + private static final int MIN_CARTOGRAPHER_LEVEL = 3; + private static final int MAP_SEARCH_RADIUS = 100; + private static final int FAILED_SEARCH_RETRY_TICKS = 6000; + private static final int EMERALD_COST = 12; + private static final int MAX_USES = 4; + private static final int VILLAGER_XP = 10; + private static final float PRICE_MULTIPLIER = 0.2F; + private static final byte MAP_SCALE = 2; + private static final TagKey AIR_TEMPLE_MAP_DESTINATIONS = + TagKey.create( + Registries.STRUCTURE, + Identifier.fromNamespaceAndPath( + Retold.MODID, + "on_air_temple_maps" + ) + ); + private static final Map FAILED_SEARCH_RETRY_AT = + new WeakHashMap<>(); + + private AirTempleDiscoveryEvents() { + } + + @SubscribeEvent + public static void onEntityInteract( + PlayerInteractEvent.EntityInteract event + ) { + if (event.getHand() != InteractionHand.MAIN_HAND + || !(event.getLevel() instanceof ServerLevel level) + || !(event.getTarget() instanceof Villager villager)) { + return; + } + + // Offers may have been generated and saved before the world reached + // Stage 2. Add this trade lazily so existing cartographers keep their + // current offers and still gain the discovery path in upgraded worlds. + ensureExplorerMapOffer(level, villager); + } + + @SubscribeEvent + public static void onServerStarting(ServerStartingEvent event) { + FAILED_SEARCH_RETRY_AT.clear(); + } + + private static boolean ensureExplorerMapOffer( + ServerLevel level, + Villager villager + ) { + if (!isEligible(level, villager) || hasExplorerMapOffer(villager)) { + return false; + } + + long gameTime = level.getGameTime(); + if (gameTime < FAILED_SEARCH_RETRY_AT.getOrDefault(villager, 0L)) { + return false; + } + + BlockPos templePos = level.findNearestMapStructure( + AIR_TEMPLE_MAP_DESTINATIONS, + villager.blockPosition(), + MAP_SEARCH_RADIUS, + false + ); + if (templePos == null) { + FAILED_SEARCH_RETRY_AT.put( + villager, + gameTime + FAILED_SEARCH_RETRY_TICKS + ); + return false; + } + + FAILED_SEARCH_RETRY_AT.remove(villager); + return addExplorerMapOffer(level, villager, templePos); + } + + static boolean ensureExplorerMapOffer( + ServerLevel level, + Villager villager, + BlockPos templePos + ) { + if (!isEligible(level, villager) || hasExplorerMapOffer(villager)) { + return false; + } + + return addExplorerMapOffer(level, villager, templePos); + } + + private static boolean isEligible( + ServerLevel level, + Villager villager + ) { + return !villager.isBaby() + && villager.getVillagerData() + .profession() + .is(VillagerProfession.CARTOGRAPHER) + && villager.getVillagerData().level() + >= MIN_CARTOGRAPHER_LEVEL + && RetoldWorldData.get(level).getStage().getId() + >= RetoldWorldStage.STAGE_2.getId(); + } + + private static boolean hasExplorerMapOffer(Villager villager) { + for (MerchantOffer offer : villager.getOffers()) { + MapDecorations decorations = offer.getResult().getOrDefault( + DataComponents.MAP_DECORATIONS, + MapDecorations.EMPTY + ); + if (decorations.decorations().containsKey(MAP_DECORATION_KEY)) { + return true; + } + } + + return false; + } + + private static boolean addExplorerMapOffer( + ServerLevel level, + Villager villager, + BlockPos templePos + ) { + ItemStack map = MapItem.create( + level, + templePos.getX(), + templePos.getZ(), + MAP_SCALE, + true, + true + ); + MapItem.renderBiomePreviewMap(level, map); + MapItemSavedData.addTargetDecoration( + map, + templePos, + MAP_DECORATION_KEY, + MapDecorationTypes.TARGET_X + ); + map.set( + DataComponents.ITEM_NAME, + Component.translatable(MAP_NAME_KEY) + ); + + MerchantOffers offers = villager.getOffers(); + offers.add(new MerchantOffer( + new ItemCost(Items.EMERALD, EMERALD_COST), + Optional.of(new ItemCost(Items.COMPASS)), + map, + MAX_USES, + VILLAGER_XP, + PRICE_MULTIPLIER + )); + return true; + } +} diff --git a/src/main/java/cz/xefensor/retold/worldgen/air/RetoldAirTempleDiscoveryGameTests.java b/src/main/java/cz/xefensor/retold/worldgen/air/RetoldAirTempleDiscoveryGameTests.java new file mode 100644 index 0000000..fa20dab --- /dev/null +++ b/src/main/java/cz/xefensor/retold/worldgen/air/RetoldAirTempleDiscoveryGameTests.java @@ -0,0 +1,238 @@ +package cz.xefensor.retold.worldgen.air; + +import cz.xefensor.retold.Retold; +import cz.xefensor.retold.stage.RetoldStageManager; +import cz.xefensor.retold.stage.RetoldStageRuntime; +import cz.xefensor.retold.stage.RetoldWorldData; +import cz.xefensor.retold.stage.RetoldWorldStage; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.component.DataComponents; +import net.minecraft.gametest.framework.BuiltinTestFunctions; +import net.minecraft.gametest.framework.FunctionGameTestInstance; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.gametest.framework.TestData; +import net.minecraft.gametest.framework.TestEnvironmentDefinition; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityTypes; +import net.minecraft.world.entity.npc.villager.Villager; +import net.minecraft.world.entity.npc.villager.VillagerProfession; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.component.MapDecorations; +import net.minecraft.world.item.trading.ItemCost; +import net.minecraft.world.item.trading.MerchantOffer; +import net.minecraft.world.item.trading.MerchantOffers; +import net.minecraft.world.level.saveddata.maps.MapDecorationTypes; +import net.neoforged.neoforge.event.RegisterGameTestsEvent; + +import java.util.Objects; +import java.util.function.Consumer; + +public final class RetoldAirTempleDiscoveryGameTests { + private static final Identifier EMPTY_STRUCTURE = + Identifier.withDefaultNamespace("empty"); + + private RetoldAirTempleDiscoveryGameTests() { + } + + public static void register( + RegisterGameTestsEvent event, + Holder> environment + ) { + TestData>> testData = + new TestData<>(environment, EMPTY_STRUCTURE, 100, 0, true); + + event.registerTest( + id("air_temple_map_trade_is_stage_gated_and_unique"), + new InlineGameTest( + testData, + RetoldAirTempleDiscoveryGameTests::mapTradeIsStageGatedAndUnique + ) + ); + } + + private static void mapTradeIsStageGatedAndUnique( + GameTestHelper helper + ) { + ServerLevel level = helper.getLevel(); + RetoldWorldData data = RetoldWorldData.get(level); + RetoldWorldStage originalSavedStage = data.getStage(); + RetoldWorldStage originalRuntimeStage = + RetoldStageRuntime.getOverworldStage(); + Villager cartographer = createCartographer(level); + MerchantOffer existingOffer = cartographer.getOffers().getFirst(); + BlockPos templePos = helper.absolutePos(new BlockPos(8, 2, 8)); + + try { + data.setStage(RetoldWorldStage.STAGE_1); + RetoldStageRuntime.setOverworldStage(RetoldWorldStage.STAGE_1); + + helper.assertFalse( + AirTempleDiscoveryEvents.ensureExplorerMapOffer( + level, + cartographer, + templePos + ), + "A Stage 1 cartographer must not sell the Air Temple map" + ); + helper.assertValueEqual( + cartographer.getOffers().size(), + 1, + "Stage 1 must leave the cartographer's offers unchanged" + ); + helper.assertTrue( + cartographer.getOffers().getFirst() == existingOffer, + "Stage 1 must preserve the existing saved offer" + ); + + RetoldStageManager.setStage(level, RetoldWorldStage.STAGE_2); + helper.assertTrue( + AirTempleDiscoveryEvents.ensureExplorerMapOffer( + level, + cartographer, + templePos + ), + "A journeyman cartographer must gain the map in Stage 2" + ); + helper.assertValueEqual( + cartographer.getOffers().size(), + 2, + "The cartographer must retain its offer and gain one map" + ); + helper.assertTrue( + cartographer.getOffers().getFirst() == existingOffer, + "Adding the map must preserve the existing saved offer" + ); + + MerchantOffer offer = cartographer.getOffers().getLast(); + assertOffer(helper, offer, templePos); + + helper.assertFalse( + AirTempleDiscoveryEvents.ensureExplorerMapOffer( + level, + cartographer, + templePos + ), + "Repeated interaction must not add another map offer" + ); + helper.assertValueEqual( + cartographer.getOffers().size(), + 2, + "The Air Temple map offer must remain unique" + ); + helper.succeed(); + } finally { + cartographer.discard(); + data.setStage(originalSavedStage); + RetoldStageRuntime.setOverworldStage(originalRuntimeStage); + } + } + + private static Villager createCartographer(ServerLevel level) { + Villager villager = Objects.requireNonNull( + EntityTypes.VILLAGER.create( + level, + EntitySpawnReason.COMMAND + ) + ); + villager.setVillagerData( + villager.getVillagerData() + .withProfession( + level.registryAccess(), + VillagerProfession.CARTOGRAPHER + ) + .withLevel(3) + ); + MerchantOffers offers = new MerchantOffers(); + offers.add(new MerchantOffer( + new ItemCost(Items.PAPER, 24), + new ItemStack(Items.EMERALD), + 16, + 2, + 0.05F + )); + villager.setOffers(offers); + return villager; + } + + private static void assertOffer( + GameTestHelper helper, + MerchantOffer offer, + BlockPos templePos + ) { + helper.assertValueEqual( + offer.getBaseCostA().getCount(), + 12, + "The Air Temple map must cost 12 emeralds" + ); + helper.assertTrue( + offer.getBaseCostA().is(Items.EMERALD), + "The primary cost must be emeralds" + ); + helper.assertTrue( + offer.getCostB().is(Items.COMPASS), + "The secondary cost must be a compass" + ); + + ItemStack result = offer.getResult(); + MapDecorations decorations = result.getOrDefault( + DataComponents.MAP_DECORATIONS, + MapDecorations.EMPTY + ); + helper.assertTrue( + result.is(Items.FILLED_MAP), + "The cartographer must sell a filled map" + ); + helper.assertTrue( + decorations.decorations().containsKey( + AirTempleDiscoveryEvents.MAP_DECORATION_KEY + ), + "The map must carry the exact Air Temple marker" + ); + MapDecorations.Entry marker = Objects.requireNonNull( + decorations.decorations().get( + AirTempleDiscoveryEvents.MAP_DECORATION_KEY + ) + ); + helper.assertValueEqual( + marker.type(), + MapDecorationTypes.TARGET_X, + "The Air Temple marker must use the exact-target X" + ); + helper.assertValueEqual( + marker.x(), + (double) templePos.getX(), + "The marker X coordinate must match the Air Temple" + ); + helper.assertValueEqual( + marker.z(), + (double) templePos.getZ(), + "The marker Z coordinate must match the Air Temple" + ); + } + + private static Identifier id(String path) { + return Identifier.fromNamespaceAndPath(Retold.MODID, path); + } + + private static final class InlineGameTest + extends FunctionGameTestInstance { + private final Consumer test; + + private InlineGameTest( + TestData>> testData, + Consumer test + ) { + super(BuiltinTestFunctions.ALWAYS_PASS, testData); + this.test = test; + } + + @Override + public void run(GameTestHelper helper) { + test.accept(helper); + } + } +} diff --git a/src/main/resources/assets/retold/lang/en_us.json b/src/main/resources/assets/retold/lang/en_us.json index f7fda77..9379288 100644 --- a/src/main/resources/assets/retold/lang/en_us.json +++ b/src/main/resources/assets/retold/lang/en_us.json @@ -8,6 +8,7 @@ "block.retold.dev_aender_portal_frame": "Development Aender Portal Frame", "entity.retold.aender_eye": "Aender Eye", "entity.retold.gale_core": "Gale Core", + "filled_map.retold.air_temple": "Air Temple Explorer Map", "item.retold.air_element": "Air Element", "item.retold.air_element.tooltip": "A restless breath bound into form.", "item.retold.water_element": "Water Element", diff --git a/src/main/resources/data/retold/tags/worldgen/structure/on_air_temple_maps.json b/src/main/resources/data/retold/tags/worldgen/structure/on_air_temple_maps.json new file mode 100644 index 0000000..d86bc0d --- /dev/null +++ b/src/main/resources/data/retold/tags/worldgen/structure/on_air_temple_maps.json @@ -0,0 +1,5 @@ +{ + "values": [ + "retold:air_temple" + ] +}