Custom Quests

From DragonMineZ Wiki

Custom quests are sagas, saga quests and side quests you write yourself with JSON alone, with no Java code or add-on jar. Story content is per-world data (unlike races and forms, which live in config/dragonminez), so these files live inside the world save.

Where files go

<world save>/dragonminez/
  sagas/                     # saga manifests
  quests/<questFolder>/      # quest files for one saga, loaded alphabetically
  sidequests/                # standalone side quests (any subfolder layout, e.g. sidequests/<category>/)

Source: QuestRegistry.loadAll(server) (src/main/java/com/dragonminez/common/quest/QuestRegistry.java). See Locating Specific Folders for the full folder map.

Story toggles

Configured in: General Server Configgameplay

These toggles live in general-server.json → gameplay, and all of them default to true:

Field Type What it does
storyModeEnabled Boolean Master switch for the saga and quest system.
createDefaultSagas Boolean Generates DragonMineZ's default saga and saga quest files (needs storyModeEnabled too).
sideQuestsEnabled Boolean Master switch for the side quest system.
createDefaultSideQuests Boolean Generates DragonMineZ's default side quest files.
autoUpdateQuests Boolean Upgrades DragonMineZ's default files in place when a new DragonMineZ version changes them (see Default file upgrades).

Default files are generated one by one, by filename, whenever they are missing, so a default file you delete comes back on the next load while its createDefault... toggle is on. Your own files (any other filename) are never touched.

Step 1: Create a saga manifest

Create one file per saga in dragonminez/sagas/, for example my_saga.json:

{
  "id": "my_saga",
  "name": "dmz.saga.my_saga",
  "requirements": {
    "previousSaga": "saiyan_saga"
  },
  "questFolder": "saga_my_saga"
}
Field Type What it does
id String Unique saga id.
name String Translation key for the saga's display name.
requirements.previousSaga String Id of the saga that must be completed first, or "" for none.
questFolder String Name of the subfolder under dragonminez/quests/ that holds this saga's quest files.

The default saga ids are classic_saga, saiyan_saga, frieza_saga, android_saga, future_saga, buu_saga, movies_saga, gt_saga and daima_saga (see Sagas).

Source: SagaDefaults.java, Saga.java.

Step 2: Add quest files

Inside dragonminez/quests/saga_my_saga/, add one file per quest, named so the files sort in play order (01_...json, 02_...json, ...). Filename order is the quest order within the saga.

{
  "id": 1,
  "title": "dmz.quest.my_saga1.name",
  "description": "dmz.quest.my_saga1.desc",
  "type": "SAGA",
  "category": "saga_my_saga",
  "parallel_objectives": false,
  "party_scaling": true,
  "secret": false,
  "claim_mode": "TREE_OR_NPC",
  "requirements": {
    "operator": "AND",
    "conditions": [
      { "type": "LEVEL", "minLevel": 1 },
      { "type": "DIMENSION", "dimension": "minecraft:overworld" }
    ]
  },
  "objectives": [
    { "type": "KILL", "entity": "dragonminez:saga_raditz", "count": 1, "health": 450.0, "meleeDamage": 22.0, "kiDamage": 39.0 }
  ],
  "rewards": [
    { "type": "ATTRIBUTES", "amount": 10 },
    { "type": "ITEM", "item": "dragonminez:senzu_bean", "count": 1 }
  ]
}

A saga quest is identified as <sagaId>:<id> (for example my_saga:1), using the numeric id, not the filename. Two files with the same id in one folder overwrite each other.

To insert a quest between two existing ones, give it a new, unused id and a filename that sorts between them. DragonMineZ's own sagas do this: Frieza Saga quest 17 is 01b_scout_the_ruined_village.json, between 01_... and 02_.... Renumbering the existing quests instead would break saved progress and every SAGA_QUEST condition or form requisite that points at them.

Quest fields

Saga quests and side quests share exactly the same schema, so these fields apply to both.

Field Type What it does
id Number or string Numeric for saga quests. Side quests use a string id instead.
title / description String Translation keys (plain text also works).
type String SAGA, SIDEQUEST, DAILY or EVENT. A DAILY quest can be repeated once per real-world day (server time zone).
category String Free-form grouping label. Defaults to "general".
parallel_objectives Boolean If true, all objectives progress at the same time instead of one after another.
party_scaling Boolean If true, item and kill counts scale up for each extra party member (defaultQuestPartyMultiplier in general-server.json, default 1.45), and quest enemies get more health and damage for each extra member (enemyHealthPerPartyPlayer 1.25, enemyDamagePerPartyPlayer 1.1).
secret Boolean Hides the quest from the quest log until it is started or available.
claim_mode String TREE_OR_NPC (claim from the quest screen or from an NPC) or NPC_ONLY (the player must talk to the turn-in NPC).
quest_giver String or null Id of the NPC that offers the quest (see Quest giver and turn-in NPCs).
turn_in String or null Id of the NPC that accepts the completed quest.
prerequisites Object Decides whether the quest can be unlocked at all. Same schema as requirements (see Prerequisites and requirements).
requirements Object Decides whether the quest can be started right now (for example the current dimension, biome or level).
repeatable Boolean If true, the quest can be started again after it is completed. Default false.
repeat_cooldown_seconds Integer Real-time seconds before a repeatable quest can be started again. 0 (the default) means right away.
time_limit_seconds Integer Time limit after accepting the quest, in in-game seconds. When it runs out, the quest fails. 0 (the default) means no limit.
objectives Array See Objective types.
rewards Array See Reward types.
defaultsVersion String Only used on DragonMineZ's own default files (see Default file upgrades).

When a repeatable or daily quest is started again, its objective progress and reward claims are reset, so the rewards can be claimed again.

Source: QuestParser.parseQuest, Quest.java.

Objective types

Each entry in objectives needs a type plus that type's fields. Fields marked with * are optional, and sensible defaults apply.

type Fields What it does
ITEM item, count Have this many of an item in your inventory. The items are not taken; use DELIVER for that.
KILL entity, count, health*, meleeDamage*, kiDamage*, spawn*, count_mode*, AITier*, TextureVariant*, canTransform*, TransformHealth*, TransformMeleeDamage*, TransformKiDamage*, TransformHealthMultiplier*, TransformMeleeDamageMultiplier*, TransformKiMultiplier*, TransformTriggerPercent* entity can be a registry id or an entity tag (#minecraft:zombies; quest spawns pick a random member). health, meleeDamage and kiDamage default to 20, 1 and 1. spawn: QUEST (default, spawned when the quest starts) or NATURAL. count_mode: QUEST_SPAWNED_ONLY (default) or ANY_MATCHING. See Kill objective: AI tier and transformations.
SPAR Same as KILL, without the Transform* fields A non-lethal fight: when a quest-spawned partner would drop below 15% health, the fight ends, it counts as a win for the whole party and the partner leaves.
INTERACT entity*, entityName* Right-click interact with a matching entity.
TALK_TO npcId The player must talk to a specific NPC or master.
DELIVER item, count* (default 1), npcId (aliases npc_id and npc) Right-click the NPC to hand over the items. They are taken from the inventory, and partial deliveries count.
STRUCTURE structure The player must enter a generated structure.
BIOME biome The player must be standing in this biome (or biome tag).
DIMENSION dimension The player must be in this dimension.
COORDS x, y, z, radius* The player must be within radius blocks (default 10) of a point.
CHECKPOINT_RACE checkpoints (list of { "x", "y", "z" }), radius* (default 8) Reach every checkpoint in order.
SURVIVE_WAVES entity, waves* (default 3), mobs_per_wave* (default 3), wave_delay_seconds* (default 10), health*, meleeDamage*, kiDamage*, AITier*, TextureVariant*, CanTransform* Waves of enemies spawn around the player; the next one comes after a short pause once a wave is cleared. Each cleared wave counts as one step of progress.
ESCORT entity, x, y, z, radius* (default 6), health* An escort spawns next to the player and follows them; bring it within radius of the target. If it dies, the quest fails. health overrides its maximum health.
DRAGON_SUMMON dragon*, ball_set* The player must summon a specific dragon and/or ball set (aliases: dragon_id and dragonId; ballSet, ball_set_id, ballSetId and set).
SKILL skill, level* The player must reach a skill level (aliases: skillId and id; minLevel and required; default level 1).

Source: QuestParser.parseObjective and the classes under common/quest/objectives/.

While a COORDS, ESCORT or CHECKPOINT_RACE objective is tracked, the quest HUD shows the distance and direction to the target (or to the next checkpoint).

In a quest with parallel_objectives: false, kill objectives that follow each other directly are unlocked together, so a "kill A, kill B" pair can be fought in any order. Objectives of the other types still go one at a time.

Kill objective: AI tier and transformations

  • AITier is optional: 1 Novice, 2 Smart, 3 Elite, 4 Expert. If it is left out (or set to 0 or a negative value), the tier comes from the difficulty the quest was started on: EASY gives 1, NORMAL gives 2, HARD gives 3, and HARD against a saga's final boss gives 4. Set AITier explicitly to force a specific tier regardless of difficulty.
  • TextureVariant picks one of the textures of entities that have several.
  • canTransform (boolean, default true) controls whether the enemy may transform mid-fight at all. Set it to false to lock the enemy out of transforming entirely; this is enforced on the server, not just cosmetic. SURVIVE_WAVES reads it as CanTransform.
  • When canTransform is true, the seven Transform* fields tune the enemy's transformed stats without touching the global config defaults. All of them are optional (null falls back to the global transformation defaults in entities.json):
    • TransformHealth, TransformMeleeDamage, TransformKiDamage: absolute stat values for the transformed form.
    • TransformHealthMultiplier, TransformMeleeDamageMultiplier, TransformKiMultiplier: multipliers applied to the base (pre-transform) stats instead of absolute values.
    • Absolute values take precedence over multipliers when both are set for the same stat.
    • TransformTriggerPercent: fraction of maximum health (0.0–1.0) at which the enemy triggers its transformation.
{ "type": "KILL", "entity": "dragonminez:saga_vegeta", "count": 1, "health": 900.0, "meleeDamage": 40.0, "kiDamage": 60.0,
  "canTransform": true, "TransformTriggerPercent": 0.5, "TransformHealthMultiplier": 1.8, "TransformMeleeDamageMultiplier": 1.6 }

How a quest can fail

A failed quest loses its objective progress, and its quest-spawned enemies and escorts are removed. It can be started again from the quest log or from its NPC (the start requirements are checked again).

  • Party wiped out: when the player (or every member of their party) dies, accepted quests with a KILL, SPAR, SURVIVE_WAVES or ESCORT objective fail.
  • Escort died: the ESCORT target was killed.
  • Time ran out: time_limit_seconds passed since the quest was accepted.

Reward types

Each entry in rewards needs a type plus its fields. Every reward type also accepts an optional difficulty field that restricts the difficulties it is granted on (see below).

type Fields What it does
ITEM item, count* (default 1) Grants an item stack.
GENERIC_ITEM itemReward (an item object) Grants an item with enchantments, potion data or an armor trim. See Item objects.
ATTRIBUTES amount Grants Attributes, the reward every default quest uses: amount × attributeRewardPointRatio (by default half, rounded down) as Attribute Points, and the rest as the TP those stat points would cost the player when claiming. That TP is not boosted again and not shared with the party. The tooltip shows the split. See Stats and Attributes.
ATTRIBUTE_POINTS amount Grants Attribute Points only (placed by the player in the stats menu).
TPS amount Grants a fixed amount of training points (goes through the server's STORY TP boosts).
ALIGNMENT amount Adjusts the player's alignment (can be negative). See Alignment.
SKILL skill, level Raises a skill to at least this level.
TRANSFORMATION formGroup, formName, mastery* (default 100.0), stack* (default false) Unlocks a form of the player's race (or a stack form if stack is true): levels the group's form skill up to that form and grants at least that much mastery. Forms listed in the form's formRequisite are raised to the mastery they require too. Aliases: form_group and group; form_name and form.
KI_TECHNIQUE code Grants a ki technique from an exported technique code (aliases: techniqueCode and technique_code). See Techniques.
COMMAND command, translationKey* Runs a command on the server (operator level), with %player% replaced by the player's name. translationKey is the text shown in the reward list instead of the command.

TRANSFORMATION and SKILL rewards act on the player's own race groups and skills. To give race- or class-specific rewards, put them in quests gated with a RACE or CLASS condition (see Prerequisites and requirements).

Source: QuestParser.parseReward, the classes under common/quest/rewards/, Difficulty.java.

Restricting a reward by difficulty

The optional difficulty field of a reward is an allow-set of the difficulties this reward is granted on (aliases: difficulties, difficultyType and minDifficulty). It accepts a JSON array (["NORMAL","HARD"]), a string separated by commas or spaces ("NORMAL, HARD") or a single value ("HARD"). Valid values are EASY, NORMAL and HARD. When it is left out or empty, the reward is granted on all difficulties. This is a set membership check, not a minimum threshold: "EASY" alone means Easy only, and ["EASY","HARD"] (skipping Normal) is valid. Unknown values are dropped.

{ "type": "ITEM", "item": "dragonminez:senzu_bean", "count": 1, "difficulty": ["NORMAL", "HARD"] }
Old files Older quest files wrote the difficulty as a prefix on the type ("hard:ITEM", "normal:ITEM"). Those files still load: hard: means Hard only and normal: means Easy and Normal, unless the reward also has a difficulty field. New files should use the difficulty field.

Reward scaling

ITEM, GENERIC_ITEM, TPS, ATTRIBUTES, ATTRIBUTE_POINTS and ALIGNMENT amounts are scaled by the quest reward multiplier of the difficulty the quest was started on (easyModeQuestRewardMultiplier and hardModeQuestRewardMultiplier in general-server.json). TPS, ATTRIBUTES and ATTRIBUTE_POINTS rewards are also multiplied by storyResetTPMultiplier (default 0.5) once per story reset. SKILL, TRANSFORMATION, KI_TECHNIQUE and COMMAND are unlock-type rewards and are never scaled. Rewards are per player. Every party member claims their own rewards when the quest is done (from the quest log, or from the turn-in NPC with claim_mode: NPC_ONLY).

Item objects

GENERIC_ITEM rewards (and item_list_wish wishes, see Custom Wishes) describe items with a typed object. itemType picks the kind of item; itemId and count are always available.

itemType Extra fields What it does
generic_item (default) — Any item.
enchanted_item enchantments enchantments is a map of enchantment id to level, for example { "minecraft:sharpness": 3 }.
enchanted_book enchantments Stored enchantments on an enchanted book (itemId minecraft:enchanted_book).
potion, splash_potion, lingering_potion, tipped_arrow potion*, mobEffects* potion is a potion id (for example minecraft:strong_healing). mobEffects is a list of { "effect", "duration", "amplifier", "ambient", "visible", "showIcon" } (duration in ticks, at least 1). Set itemId to the matching vanilla item (minecraft:potion, minecraft:splash_potion, minecraft:lingering_potion or minecraft:tipped_arrow).
trimmed_armor material, pattern, enchantments* Armor with a trim, for example "material": "minecraft:gold" and "pattern": "minecraft:coast".

itemId is required and count must be at least 1. An invalid item object (a missing itemId, an unknown itemType, a bad level or duration) makes the whole quest file fail to load, and the file is named in the JSON load report. Unknown enchantment, potion or effect ids only skip that entry, with a warning in the log.

{ "type": "GENERIC_ITEM", "itemReward": {
    "itemType": "enchanted_item", "itemId": "minecraft:diamond_sword", "count": 1,
    "enchantments": { "minecraft:sharpness": 5, "minecraft:unbreaking": 3 } } }

Source: src/main/java/com/dragonminez/common/util/types/items/, GenericItemTypeAdapterFactory.java.

Prerequisites and requirements

Both prerequisites (can this quest ever be unlocked) and requirements (can it be started right now) use the same condition-tree schema:

{
  "operator": "AND",
  "conditions": [
    { "type": "LEVEL", "minLevel": 30 },
    { "type": "SAGA_QUEST", "sagaId": "saiyan_saga", "questId": 8 },
    { "operator": "OR", "conditions": [ ... ] }
  ]
}

operator is AND or OR. conditions can nest another { "operator": ..., "conditions": [...] } block.

type Fields
SAGA_QUEST sagaId, questId (int): a specific saga quest must be completed.
QUEST questId (string): a specific side quest must be completed.
LEVEL minLevel
STAT stat (STR, SKP, RES, VIT, PWR or ENE), minValue
RACE race (aliases: raceName and race_name): the player's race id, case-insensitive. Built-in ids: human, saiyan, namekian, frostdemon, bioandroid, majin, glind. Watch out: it is frostdemon (not frieza or colddemon) and bioandroid (not android or cell). Custom races use their folder name in config/dragonminez/races/.
CLASS class (aliases: className, class_name and characterClass): the player's class id, case-insensitive.
SKILL skill, minLevel* (aliases: skillId and id; level and required)
BIOME biome
DIMENSION dimension
STRUCTURE structure, hint* ({ "dimension", "x", "y", "z" }, all optional; it helps point the player towards the structure)
ALIGNMENT min*, max*
TIME mode (GAME_TIME with ticks, or REAL_TIME with milliseconds): time elapsed since the quest became eligible.

Source: QuestParser.parseCondition, QuestPrerequisites.java. The same condition blocks gate choices in NPC Dialogue.

Side quests

Side quests live anywhere under dragonminez/sidequests/ (subfolders are scanned too) and use the same quest schema as saga quests, with a few practical differences:

  • "id" is a string instead of a number (for example "bulma_gero_blueprints").
  • "type" is "SIDEQUEST" (or "DAILY" or "EVENT") instead of "SAGA".
  • There is no enclosing saga folder or manifest: each file stands alone.
  • quest_giver and turn_in are used more heavily, since side quests are usually driven by NPCs.
  • prerequisites commonly reference SAGA_QUEST or QUEST conditions to gate a side quest behind story progress.

Example (dragonminez/sidequests/collection/bulma_gero_blueprints.json):

{
  "id": "bulma_gero_blueprints",
  "title": "dmz.sidequest.bulma_gero.name",
  "description": "dmz.sidequest.bulma_gero.desc",
  "type": "SIDEQUEST",
  "category": "collection",
  "parallel_objectives": false,
  "party_scaling": true,
  "secret": false,
  "claim_mode": "TREE_OR_NPC",
  "quest_giver": "bulma",
  "turn_in": "bulma",
  "prerequisites": {
    "operator": "AND",
    "conditions": [
      { "type": "SAGA_QUEST", "sagaId": "android_saga", "questId": 6 }
    ]
  },
  "objectives": [
    { "type": "STRUCTURE", "structure": "dragonminez:gero_lab" },
    { "type": "ITEM", "item": "minecraft:redstone", "count": 32 },
    { "type": "ITEM", "item": "dragonminez:kikono_shard", "count": 8 },
    { "type": "TALK_TO", "npcId": "bulma" }
  ],
  "rewards": [
    { "type": "ATTRIBUTES", "amount": 25 }
  ]
}

A repeatable errand, delivered to an NPC:

{
  "id": "my_market_run",
  "title": "dmz.sidequest.my_market_run.name",
  "description": "dmz.sidequest.my_market_run.desc",
  "type": "SIDEQUEST",
  "category": "collection",
  "quest_giver": "bulma",
  "turn_in": "bulma",
  "repeatable": true,
  "repeat_cooldown_seconds": 3600,
  "objectives": [
    { "type": "DELIVER", "item": "minecraft:emerald", "count": 12, "npcId": "bulma" },
    { "type": "TALK_TO", "npcId": "bulma" }
  ],
  "rewards": [
    { "type": "ITEM", "item": "dragonminez:senzu_bean", "count": 2 }
  ]
}

Quest giver and turn-in NPCs

quest_giver and turn_in reference an NPC by id: the same id used by quest NPC entities and by master and trainer NPCs (TALK_TO and DELIVER objectives use the same ids). DragonMineZ indexes quests by both fields, so an NPC can show "available quest" markers and accept turn-ins without any extra wiring on your end. Both fields are optional: leave them out or set them to null if the quest isn't tied to an NPC (for example a saga quest that is only about exploring or killing).

Talking to the giver opens its dialogue, where the Quests option lists the quests it offers, the ones in progress and the ones ready to turn in, plus its locked quests: hovering one shows what is missing (for example the saga quest or side quest to complete first), and a completed repeatable quest shows a countdown until it can be taken again. Locked secret quests are not listed. The list is sorted by when each quest opens up in the story (saga order, then quest order, then level requirement), so an NPC's early quests come first.

If the giver is a master with an alignment requirement, its quests can only be started while the player's alignment fits, from any screen; quests already in progress can still be turned in. Goku, Gohan, King Kai, Old Kai, Roshi and Krillin need an alignment of 61 or more, and Cell and Frieza 40 or less. See Masters.

Unknown and misspelled keys

Every quest, saga, objective, reward, prerequisite or requirement condition and structure hint is checked against an explicit allow-list of fields (QuestParser.validate and validateSaga). Any key that isn't recognized (a typo, the wrong casing, a leftover field from an old schema) is not silently dropped: it is logged and shown to players in the in-game JSON load report on login (controlled by developer.reportJsonProblemsInChat, on by default), as well as in the server console. The entry still loads with the rest of its valid fields; only the unrecognized key is flagged. Use this to catch typos (for example CanTransfrom instead of canTransform) instead of having a field silently do nothing. A file with broken JSON syntax is skipped and reported in the same way.

Keys that start with _, $ or // are treated as comments and never flagged, so you can leave notes for yourself directly in the JSON.

Custom objective and reward types

Java add-ons can register their own objective and reward types through QuestObjectiveRegistry and QuestRewardRegistry. Once registered, they are written in quest JSON just like the built-in types. See Implementation and Events and Creating a DMZ Addon.

Default file upgrades

Configured in: General Server Configgameplay.autoUpdateQuests

Every generated quest, saga and side quest file is stamped with a defaultsVersion field (a semver string, currently "2.2.0"). QuestUpgrader manages it automatically and uses it to run a three-way merge of your edits against DragonMineZ's shipped defaults whenever the built-in content changes:

  • A field you never changed takes the new default.
  • A field you changed keeps your value. If DragonMineZ also changed it, that is recorded as a conflict, and your value still wins.
  • Keys you added are kept.
  • When the merge changes something, the previous file is copied to <world>/dragonminez/oldBackup/ first.

A summary is written to <world>/dragonminez/quest_update_report.txt and shown in the JSON load report. Turn the whole system off with autoUpdateQuests: false. You don't need to set defaultsVersion on your own custom content. See Reloading and Updating Changes for how upgrades are reported.

Translation keys

Follow the existing naming convention so your content matches the rest of the story:

  • Saga name: dmz.saga.<saga_id>
  • Saga quest title and description: dmz.quest.<key>.name / dmz.quest.<key>.desc
  • Side quest title and description: dmz.sidequest.<key>.name / dmz.sidequest.<key>.desc

Add these keys to your en_us.json or to your own language file. Custom translations merge with the Crowdin ones in the same way as for custom races.

Reload and validate

  1. Save your saga, quest or side quest JSON in the world's dragonminez/ folder.
  2. Validate the JSON syntax (see How to JSON).
  3. Run /dmzreload story (or /dmzreload all); see Reloading and Updating Changes.
  4. Open the quest log in game and check that your saga, quest or side quest appears with the right requirements, objectives and rewards.