Resonance Implementation
On the implementation side, resonance does one thing only: it folds site input and player input into ResonanceResult. It does not own site lifecycle, and it does not generate tooltip text directly.
Verified Forge Boundaries
The following APIs are already verified and can define the implementation boundary:
| Topic | Verified API | Conclusion |
|---|---|---|
| tooltip construction | ItemTooltipEvent(@NotNull ItemStack, @Nullable Player, List<Component>, TooltipFlag) | tooltip allows player == null |
| tooltip read | ItemTooltipEvent.getItemStack(), getToolTip(), getFlags() | tooltip can read saved snapshots directly and append text |
| null-player tooltip path | ItemTooltipEvent.getEntity() | startup and some client paths cannot depend on a player object |
This means resonance results must enter the snapshot first, then tooltip reads the snapshot. Tooltip cannot query live runtime.
Current Type Contract
public record SiteProfile(
String id,
SitePressure pressure,
int baseStability,
float guardianMultiplier
) {}
public record RelicLoadout(
RelicTendency tendency,
BuildPosture posture,
CivilizationLean lean,
int identificationLevel
) {}
public record ResonanceResult(
ResonanceState state,
String patternKey
) {}SiteProfile and RelicLoadout keep input compact. ResonanceResult keeps output compact. That prevents the resolver signature from bloating as the system expands.
File Boundaries
| File | Minimum responsibility |
|---|---|
SitePressure | site-pressure enum |
RelicTendency, BuildPosture, CivilizationLean | player-input enums |
ResonanceState | result-state enum |
SiteProfile | site input object |
RelicLoadout | player input object |
ResonanceResult | compact output object |
ResonanceResolver | single evaluation entry point |
Pure Evaluation Constraints
ResonanceResolver.resolve(site, loadout) should remain a pure function.
public final class ResonanceResolver {
private ResonanceResolver() {
}
public static ResonanceResult resolve(SiteProfile site, RelicLoadout loadout) {
if (site.pressure() == SitePressure.CONTAMINATION
&& loadout.tendency() == RelicTendency.FILTER) {
return new ResonanceResult(ResonanceState.TUNED, "contamination.cleanse");
}
if (site.pressure() == SitePressure.CONTAMINATION
&& loadout.tendency() == RelicTendency.SUNDER
&& loadout.posture() == BuildPosture.BREACH) {
return new ResonanceResult(ResonanceState.OVERLOADED, "contamination.burst");
}
return new ResonanceResult(ResonanceState.DORMANT, "generic.idle");
}
}Keeping it pure gives us three concrete benefits:
- unit tests can cover it directly,
- runtime, recovery, and tooltip all read the same result,
- the client does not need a private copy of resonance logic.
Downstream Consumption Order
The implementation order stays fixed:
- activation or site startup computes
ResonanceResult, ActiveSiteRuntimereads the result and applies site consequences,- recovery writes
stateandpatternKeyintoRecoveredRelicSnapshot, RelicTooltipViewonly reads the snapshot and optional long-term knowledge.
In this sequence, only runtime and recovery may interpret the result. Tooltip only formats it.
Minimum Test Matrix
| Site profile | Loadout | Expected result |
|---|---|---|
CONTAMINATION | FILTER + STABILIZE + MECHANICAL + 0 | TUNED + contamination.cleanse |
CONTAMINATION | SUNDER + BREACH + ARCANE + 1 | OVERLOADED + contamination.burst |
| fallback | any unsupported combination | DORMANT + generic.idle |
tooltip with player == null | saved snapshot | still renders minimum text |
Implementation Red Lines
- do not serialize live runtime objects directly into relics,
- do not let tooltip recalculate resonance,
- do not let the resolver read players, worlds, or the runtime registry,
- do not make
patternKeya temporary string that only one UI page understands.