Repository navigation
Stories and Upgrades
Developer Guide | JSON + KubeJS Tutorial | Java Addon Tutorial | Assets and Localization
This guide adds a seven-chapter story to an already registered addon god. It uses the current Forge 1.20.1 narrative API and the same shrine menus used by the 18 built-in gods. Aster below is example addon content, not another built-in god.
Start with the template in the JSON + KubeJS Tutorial or its complete source example. Its installation must supply all three definitions on both client and server:
config/divinity_engine/content/example_pantheon/gods/aster.json
config/divinity_engine/content/example_pantheon/blessings/guiding_breath.json
config/divinity_engine/content/example_pantheon/trials/trial_of_shelter.json
These register example_pantheon:aster, its automatic aster_statue,
example_pantheon:guiding_breath, and Aster's trial. The combined template also
needs kubejs/server_scripts/aster.js for its blessing behavior, plus its client
resource pack. The JSON-only alternative
uses fixed actions without KubeJS. Install only one registration alternative:
they intentionally share IDs.
| Input | Installed at | What it creates | Reload boundary |
|---|---|---|---|
| Startup content JSON | Instance config/divinity_engine/content/ on both sides |
God, statue, blessing, bounded trial | Full restart of both sides |
| Narrative datapack | Server world datapacks/
|
Stories, chapter text, presence, relationships | Server datapack loading/reloading |
| Resource pack | Client resourcepacks/, enabled in Options |
Language, models, textures | Client resource reload |
| Java addon | Matching mod jar in mods/
|
Registered content and custom runtime handlers | Restart |
A narrative file does not register its host god, implement offering rules,
or create a trial. offering_examples is guidance, not an acceptance whitelist.
Aster's actual startup god definition accepts bread and amethyst shards, so the
story below lists those items. Custom objective handlers and arbitrary god/trial
lifecycle behavior require Java; the KubeJS custom callback API covers content
blessings, not every narrative, god, or trial event.
For singleplayer, the full paths are:
<instance>/saves/<world>/datapacks/aster-story/pack.mcmeta
<instance>/saves/<world>/datapacks/aster-story/data/example_pantheon/divine_narratives/road_and_refuge.json
On a dedicated server, replace <instance>/saves/<world> with the server's
configured world directory. If distributed as a ZIP, pack.mcmeta and data/
must be directly at its root, not inside an extra aster-story/ directory.
aster-story/pack.mcmeta:
{
"pack": {
"pack_format": 15,
"description": "Aster: Road and Refuge; requires the Aster content template"
}
}aster-story/data/example_pantheon/divine_narratives/road_and_refuge.json:
{
"version": 2,
"kind": "story",
"id": "example_pantheon:road_and_refuge",
"god": "example_pantheon:aster",
"title": "Road and Refuge",
"request": "Make an accepted offering, then decide how to welcome a traveler.",
"branches": {
"a": {
"name": "Shelter",
"objective": "divinity_engine:donation",
"condition": "",
"target": 2,
"item": "minecraft:bread",
"deed": "Contribute two bread at Aster's statue",
"reward": {
"id": "example_pantheon:road_shelter",
"blessing": "example_pantheon:guiding_breath",
"description": "Records Shelter for Guiding Breath; this pack installs no gameplay modifier"
},
"ending": "You made a place where a traveler could rest."
},
"b": {
"name": "Wayfinding",
"objective": "divinity_engine:donation",
"condition": "",
"target": 1,
"item": "minecraft:amethyst_shard",
"deed": "Contribute one amethyst shard at Aster's statue",
"reward": {
"id": "example_pantheon:road_wayfinding",
"blessing": "example_pantheon:guiding_breath",
"description": "Records Wayfinding for Guiding Breath; this pack installs no gameplay modifier"
},
"ending": "You marked a road without choosing another traveler's destination."
}
},
"chronicle": {
"premise": "A traveler needs both a place to stop and the freedom to continue.",
"offering_examples": ["minecraft:bread", "minecraft:amethyst_shard"],
"witness": {
"objective": "divinity_engine:observe",
"condition": "day_sky",
"target": 100,
"deed": "Observe open daytime sky for 100 consecutive server ticks"
},
"reckoning": {
"objective": "divinity_engine:observe",
"condition": "day_sky",
"target": 100,
"deed": "After your donation, observe open daytime sky for another 100 consecutive ticks"
},
"passages": {
"call": ["Begin here, then drop an offering Aster accepts in front of the statue."],
"witness": ["Stand beneath open daytime sky. Watch for one hundred consecutive ticks."],
"testimony": ["Shelter asks for two bread. Wayfinding asks for one amethyst shard. Both endings record a permanent marker, not an automatic numeric bonus."],
"deed_a": ["Use Contribute requested items at Aster's statue to donate two bread."],
"deed_b": ["Use Contribute requested items at Aster's statue to donate one amethyst shard."],
"reckoning_a": ["Observe the open daytime sky again. Shelter should not become a cage."],
"reckoning_b": ["Observe the open daytime sky again. Guidance should not become a command."],
"trial": ["Enter and win a new Aster trial after this reckoning. An older victory does not count."],
"ending_a": ["Return to the statue and claim Shelter. A place to rest is now part of your story."],
"ending_b": ["Return to the statue and claim Wayfinding. A road remains open beyond your story."]
},
"dialogue": {
"call": "First, show that you have come to listen.",
"witness": "Look beyond the roof you know.",
"testimony": "Will you prepare a refuge or mark a road?",
"deed_a": "Bring enough to share.",
"deed_b": "Leave a sign, not an order.",
"reckoning_a": "A refuge must have an open door.",
"reckoning_b": "A path must allow another choice.",
"trial": "Now meet the trial with what you have learned.",
"ending_a": "Your shelter is remembered.",
"ending_b": "Your guidance is remembered."
}
}
}This complete example uses only registered built-in objective types. The two
donations are separate from the opening ordinary offering. 100 is five seconds
at 20 server ticks per second; interruption resets that observation attempt.
The second observation starts only when Reckoning becomes active. Neither
daylight task requires a special location or staring at a particular screen pixel.
Narratives use version: 2; startup definitions use schema_version: 1.
Do not move this story into config/divinity_engine/content, or put god startup
JSON under data/.../divine_narratives. The explicit story id is its save
identity; the filename is not a substitute for that field. Keep branch keys
a/b, god ID, story ID, and reward IDs stable after release.
Use a disposable world with the host content and datapack installed. No player commands are needed for this progression:
- Place Aster's statue from the statue creative tab for testing, then interact to contact Aster. Reopen that same statue's blessings menu.
- Select Quests, choose Road and Refuge if multiple stories are listed, and press Begin quest.
- Drop an accepted opening offering in front of the statue after beginning. Complete the daytime Witness objective.
- Return within six blocks of the same loaded statue. Use Speak about quest to open Shrine Conversation, preview both paths, and confirm one permanent choice.
- Hold the required items in inventory and use Contribute requested items. Dropping items as ordinary offerings does not fulfill this explicit donation action.
- Complete Reckoning, then use Enter Trial from the matching statue-opened menu and win a fresh trial. Failure permits retry without undoing completed chapters.
- Return and press Claim ending and upgrade. Read the earned chapter and marker in the Codex; revisit Powers and Bond for eligibility and progress.
The server checks the actual open shrine menu, bound god, contact, proximity, and current story state. A remotely opened menu or client-side button state cannot authorize a quest or trial. Conversation preview expires after 1,200 server ticks; a stale confirmation must be previewed again.
Current built-in gods and addon gods with a registered story use relationship proofs. Do not copy the old blanket requirement of 50 favor before starting a story. Addons without a story can retain compatibility favor requirements. Adding a story therefore changes how the host god is managed; test its blessing unlock sequence as well as the story. For multiple stories, the compatibility primary-story lookup prefers a story whose ID equals the god ID, then sorts matching story IDs. Do not change that effective primary accidentally in a save.
BlessingSpecialization stores a reward ID, blessing ID, and description. The
JSON reward object saves that identity at completion and supplies UI text.
It does not grant/equip the blessing, execute an action, alter cooldowns, or
interpret words such as "heal +1" as code. The built-in 36 branch upgrades work
because their blessing callbacks explicitly consult the specialization system.
The Aster story above deliberately promises only markers. To implement a real Shelter bonus, add code to the actual healing mechanic and then update the reward description to match it. Choose either your Java blessing implementation or the existing content-blessing server callback; do not register a second blessing with the same ID or apply the base heal twice. The JSON-only action format has no specialization-conditional action.
Here is a complete Java eligibility helper, at
src/main/java/example/pantheon/upgrade/AsterUpgradeGate.java in a Java addon:
package example.pantheon.upgrade;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.nightshade.divinity_engine.util.divinity.narrative.BlessingSpecializations;
import net.nightshade.divinity_engine.util.divinity.narrative.DivineNarrativeManager;
public final class AsterUpgradeGate {
private static final ResourceLocation GOD =
new ResourceLocation("example_pantheon", "aster");
private static final ResourceLocation BLESSING =
new ResourceLocation("example_pantheon", "guiding_breath");
private static final ResourceLocation REWARD =
new ResourceLocation("example_pantheon", "road_shelter");
private AsterUpgradeGate() {}
public static boolean shelterActive(ServerPlayer player) {
return player != null
&& player.serverLevel().getServer().isSameThread()
&& player.isAlive() && !player.isRemoved()
&& !player.hasDisconnected() && !player.isSpectator()
&& DivineNarrativeManager.hasSpecialization(player, REWARD)
&& BlessingSpecializations.hasEligibleBlessing(player, GOD, BLESSING);
}
}Call this from your server-side healing callback. For example, choosing
AsterUpgradeGate.shelterActive(player) ? 3.0F : 2.0F as that callback's heal
amount implements a one-health-point Shelter bonus only when the callback
actually uses the value. This helper alone installs no effect or listener.
Preserve the ability's water check, health-change/success check, eligibility,
and ordinary cooldown dispatch described in the
JSON + KubeJS Tutorial and
Java Addon Tutorial.
For the JSON + KubeJS starter, no extra Java addon is necessary for this
particular upgrade. Replace its existing aster.js listener with the following
server script. Do not keep both listeners: both would receive the event.
Java.loadClass accesses the installed public Java API from trusted KubeJS code;
it does not register another god or change the JSON declaration.
const AsterServerPlayer = Java.loadClass('net.minecraft.server.level.ServerPlayer')
const AsterId = Java.loadClass('net.minecraft.resources.ResourceLocation')
const AsterNarrative = Java.loadClass('net.nightshade.divinity_engine.util.divinity.narrative.DivineNarrativeManager')
const AsterEligibility = Java.loadClass('net.nightshade.divinity_engine.util.divinity.narrative.BlessingSpecializations')
const AsterEffects = Java.loadClass('net.minecraft.world.effect.MobEffects')
const AsterEffectInstance = Java.loadClass('net.minecraft.world.effect.MobEffectInstance')
const asterGodId = AsterId.fromNamespaceAndPath('example_pantheon', 'aster')
const asterBlessingId = AsterId.fromNamespaceAndPath('example_pantheon', 'guiding_breath')
const asterShelterId = AsterId.fromNamespaceAndPath('example_pantheon', 'road_shelter')
const asterWayfindingId = AsterId.fromNamespaceAndPath('example_pantheon', 'road_wayfinding')
DivinityEvents.blessing('example_pantheon:guiding_breath', event => {
if (event.hook !== 'pressed') return
const player = event.owner
if (!(player instanceof AsterServerPlayer) || !player.isInWater()) return
const before = player.getHealth()
if (before >= player.getMaxHealth()) return
const eligible = AsterEligibility.hasEligibleBlessing(player, asterGodId, asterBlessingId)
const shelter = eligible && AsterNarrative.hasSpecialization(player, asterShelterId)
const wayfinding = eligible && AsterNarrative.hasSpecialization(player, asterWayfindingId)
player.heal(shelter ? 3 : 2)
if (player.getHealth() <= before) return
if (wayfinding) player.addEffect(new AsterEffectInstance(AsterEffects.MOVEMENT_SPEED, 60, 0))
event.markSuccessful()
})Shelter now heals three HP instead of two; Wayfinding keeps the two-HP heal and grants Speed I for 60 ticks after a successful heal. Both preserve the water condition and normal cooldown. Change the two story reward descriptions to these actual effects before anyone accepts the story, and update the blessing description. The complete story above remains an honest marker-only example until this optional script replaces its behavior. Test both paths with fresh players because accepted story requirements and descriptions are saved. This code is source-checked guidance, not a claim of a completed Rhino gameplay test.
The two public queries serve different purposes: hasSpecialization(player, rewardId) checks an earned completed reward; hasEligibleBlessing(player, godId, blessingId) checks equipped slots, correct bound god, contact, and current
relationship eligibility. A saved reward alone is not permission to apply a
passive bonus while unequipped or abandoned. Do not use the built-in-only
isActive(player, godPath, branch) lookup for arbitrary addon reward IDs.
Use per-player/instance state, never mutable counters on singleton definitions. Own and remove any transient attributes or potion leases your upgrade adds. Reconnecting must not duplicate modifiers; reading a saved cooldown must not apply a reduction again. Keep cleanup independent of whether the blessing is still in a slot.
The checked-in Connected Pantheon Java module contains full builder stories and a real custom objective handler:
-
ConnectedPantheonAddon.java belongs at
src/main/java/example/pantheon/connected/ConnectedPantheonAddon.javain the host addon. -
StarChartEvents.java belongs at
src/main/java/example/pantheon/connected/StarChartEvents.java. -
Its translations belong at
src/main/resources/assets/example_pantheon/lang/en_us.json.
These are narrative modules, not standalone runnable mods. Their host must
register example_pantheon:vesper, a matching statue, accepted paper/amethyst
offerings, and a Vesper trial. The
equivalent full datapack
also needs that host. Its first_star story additionally needs the registered
example_pantheon:star_chart objective and Java handler; its last_lantern
story uses standard objectives but still needs Vesper and her trial. None of
these example reward markers automatically modifies Solar Ward or Twilight Veil.
Call ConnectedPantheonAddon.register() once in the addon constructor before
common setup. It submits DivineNarrativeDeferredRegister and registers the
Forge listener. Do not call the engine's NarrativeBootstrap, or add a second
automatic subscriber for StarChartEvents.
For your own definitions, use new DivineNarrativeDeferredRegister(namespace),
declare story(path, supplier), then submit() before setup. Build stories with
DivineStoryDefinition.builder(godId).id(storyId) and both branches. Expanded
StoryChronicle requires Witness, Reckoning, and the ten passage keys shown
above. The deferred registration key is authoritative over the builder ID.
Register custom StoryObjectiveType values before story construction/JSON
decoding. Registration supplies metadata, not an event listener. The example
handler verifies a successful server compass-crafting notification, then calls
DivineNarrativeManager.recordObjective(player, type, condition, amount) and
sync(player). That public reporting hook trusts the caller's evidence; it
does not prove that crafting or another claimed action happened. Never accept
arbitrary progress amounts from client packets. For cancelable events, wait for
the final outcome and revalidate the original chapter before awarding credit.
The detailed authoring guide explains these
event and snapshot contracts; use this page's current menu/proof flow for play.
Datapack definitions can override Java defaults by stable story ID. Two distinct visible JSON files declaring the same story ID are rejected. Malformed narrative reloads retain the last valid definitions; inspect the error rather than assuming the new file loaded. Accepted stories pin their definitions, and confirmed paths pin their reward/deed. Editing a JSON target is not a migration of a player's accepted objective. Use a new disposable player/world to test revised defaults.
Translations stored as markers can resolve through updated client resources; saved literal prose does not become translated automatically. See Assets and Localization. Keep released IDs and branch meanings stable, including the effective primary story for a god.
Before distributing, verify both branches with independent players, interrupted observations, insufficient donation items, a failed/retried trial, save/reload, and repeated ending clicks. For functional bonuses also test unequip, abandon/recontact, death, cooldown, and canceled healing. Metadata-only markers must not be advertised as installed gameplay upgrades.
This page's JSON and helper signatures are checked against local source; no Forge build, runtime loader test, multiplayer session, or in-game playthrough was performed while authoring it.
- Narrative decoder and reload listener: exact JSON envelopes and reload behavior.
- StoryChronicle, StoryObjectiveType, and deferred registration: chapter, condition, and Java registration contracts.
- Narrative manager, specialization eligibility, and relationship proofs: earned rewards versus active abilities.
- Blessings screen and conversation screen: current player menu path.