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
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/internal/design_implementation_status.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion docs/internal/retold_design_risks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
7 changes: 5 additions & 2 deletions docs/internal/retold_mod_system.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ Technical owners:
- `AirTemplePiece`
- `AirTempleIslandGenerator`
- `AirTempleTowerGenerator`
- `AirTempleDiscoveryEvents`
- `AirTempleWindEvents`
- `AirTempleWindData`
- `AirTempleBreezeSpawner`
Expand All @@ -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.
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/internal/retold_roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Structure> AIR_TEMPLE_MAP_DESTINATIONS =
TagKey.create(
Registries.STRUCTURE,
Identifier.fromNamespaceAndPath(
Retold.MODID,
"on_air_temple_maps"
)
);
private static final Map<Villager, Long> 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;
}
}
Loading
Loading