Feedback & Colored Combat Text
Coordinated sound, camera shake, and colored floating combat text for every moment
Overview
Adds a single feedback layer so the big moments of play - a critical hit, a level-up, a bounty completion, a shop purchase - feel like more than a line of text. One service fans each moment out to up to five channels at once, in lockstep: floating combat text (colored where it fires), a 3D sound at the player, an optional camera shake, the corner notification feed, and an in-menu toast for actions taken from inside a UI page (for example accepting a quest from the Quests menu shows a toast over that page in addition to the shared sound cue). Everything is cosmetic, server-owner configurable, and can be turned off.
The two pieces are colored combat text (the red "CRIT!" and gold level-up popups, tuned in combat-text.json) and the broader feedback layer (the per-moment sound and camera, tuned in feedback.json). Each channel is independently guarded, so a misconfigured asset can never break combat.
Feedback moments (authored files)
New: the toast and the jingle a player gets for a progression moment are files, not code. One file per moment lives at Server/ZiggfreedCommon/FeedbackMoments/<moment id>.json, and the file name is the moment. Reword one, recolour it, give it a different sound, announce it to the whole server, or run a console command on it, all without a jar involved.
Eight moments fire today, and the mod ships a file for each of them. Seven are the quest and achievement notices players already know; the library underneath ships a plainer, neutral file for those same seven under the same names (so a bare server gets a notice and a jingle out of the box), and the MMO's files win because a pack that depends on the library loads after it. The eighth, Rare_Find, is the MMO's own, with no neutral default beneath it.
| Moment (file name) | Fires when | What the shipped file does |
|---|---|---|
Quest_Objective_Progressed | One step of a quest a player is carrying moves | The white {step} ({current}/{required}) tick, shown only at the quarter marks (EveryPercent: 25); the tick that finishes the step says "Objective complete" in green instead. No sound. |
Quest_Completed | The last objective is met and the quest pays out on the spot | The yellow "Quest complete" line and its jingle. |
Quest_Parked | The last objective is met but the reward waits to be collected | Three wordings from one file: the yellow "open the Quests page" notice with a soft sting; a full bag reads red ("free up inventory space") and stays silent; a board contract points at the Bounty Board. |
Quest_Claimed | A quest's rewards are paid to the player | Silent for a quest that settled on the spot (it already had its completion jingle); the legendary-chest chime when the player came back to collect a parked reward. |
Achievement_Unlocked | An achievement is earned | The golden title, the points line, the achievement's own picture, the unlock jingle, and the server-wide banner for the achievements that ask for one. |
Achievement_Claimed | An achievement's rewards are paid to the player | As Quest_Claimed: the chime only when the player came back to collect. |
Achievement_Server_First_Lost | A player finishes a one-winner achievement somebody else already won | A quiet grey note, no sound. |
Rare_Find | A rolled loot table lands a roll that named this moment as its Cue, and that roll actually paid out | A gold Reward-toned toast plus the legendary-chest chime. |
Rare_Find also shows the no-Java celebration rule: the cue id IS the moment id. Any loot roll or ladder floor in a Lootables file can carry a Cue, and a pack that authors a different cue value plus a moment file of that same name gives that roll a celebration of its own, with no code anywhere. A cue fires only when the grants beside it genuinely paid out, so it never celebrates over an empty hand.
Three of the shipped files, to show the shape:
{
"Toast": {
"Title": { "Key": "notify.quest_complete", "Args": ["title"] },
"Tone": "Reward"
},
"Sound": { "Id": "SFX_Discovery_Z2_Short" }
}{
"Toast": {
"Title": { "Key": "notify.quest_ready_to_claim", "Args": ["title"] },
"Tone": "Reward"
},
"Sound": { "Id": "SFX_Discovery_Z1_Short" },
"Variants": [
{
"When": { "reason": "no_space" },
"Toast": {
"Title": { "Key": "notify.quest_inventory_full", "Args": ["title"] },
"Tone": "Error"
},
"Sound": {}
},
{
"When": { "turnIn": "accept_site" },
"Toast": {
"Title": { "Key": "notify.contract_ready_to_claim", "Args": ["title"] },
"Tone": "Reward"
}
}
]
}{
"Toast": {
"Title": { "Key": "notify.achievement_unlocked", "Args": ["title"] },
"Secondary": { "Key": "notify.achievement_points_awarded", "Args": ["points"] },
"Tone": "Reward"
},
"Broadcast": {
"Title": { "KeyArg": "announceTitleKey", "Args": ["announceSkill", "announceAmount"] },
"Secondary": { "KeyArg": "announceBodyKey", "Args": ["player", "title"] },
"Major": true
},
"Sound": { "Id": "SFX_Capture_Crate_Capture_Succeed_Local" }
}The unlock toast draws the achievement's own picture beside the words, so there is nothing to name per achievement. The banner is drawn only for an achievement whose own file authored an Announce block: that block carries just two keys, TitleKey and BodyKey, and the moment reads them through KeyArg, so the words are the achievement's own and one that authored none shows no banner at all. The server-first note is deliberately grey and silent: losing a race should never be louder than winning one.
The four groups
A moment file carries up to four independent groups. Author the ones you want and delete the rest; none of them needs any of the others.
| Group | What it does |
|---|---|
Toast | The corner card the player who earned it sees, as a Title line and an optional Secondary line. |
Broadcast | A centered banner for everybody online (Major: true for the big style). Add it to announce every unlock the same way; the shipped unlock file draws it only for an achievement that authored an Announce block. |
Sound | Any sound id, played at the player's own position, so anyone standing nearby hears it too. |
Command | A console command line run on the moment - a title card, a webhook relay, a fireworks spawn - with {player} and any of the moment's values in braces. |
A line of text is written the same way everywhere: Key, Args, and optionally Color; the toast as a whole carries a Tone.
Keyis a line from a language file, written without that file's own name prefix, exactly as you wrote it there. Point it at a key of your own to reword the moment entirely; a key no language file ships is shown as written, so a one-server line can be the sentence itself.KeyArgreads the key from one of the moment's values instead of naming it. That is howAchievement_Unlocked.jsondraws each announcing achievement's own words.Argsnames which of the moment's values fill that line's blanks, in order. A line naming a value the moment does not carry is skipped rather than printed half-empty, so an experiment can never leave a broken line on screen.playeralways answers with the player's name.Toast.Tonecolours the whole notification, frame and words together, from the closed setInfo/Success/Reward/Warning/Error, and picks the matching corner-feed style. Unauthored reads asInfo, the neutral blue.Coloron a single line, as hex, still overrides the tone's colour for that line alone.
Two more knobs branch on the moment itself:
Variantsis an ordered list of{When, <groups>}entries. The first entry whoseWhenvalues all match the moment's own values (compared as text, sotrueand25may be written bare) overlays only the groups it restates; an empty group there means "nothing for this case". This is how oneQuest_Parked.jsonsays three things and how the twoclaimedfiles chime only for a collect ("When": {"collected": true}).Toast.EveryPercentshows an ordinary progress tick only when it crosses a multiple of that many percent, which is what keeps a long step from chattering. Without it there are no marks at all, and a player on the "milestones" setting sees only the finish.
What each moment carries, for Args, KeyArg and When:
| Moment | Values |
|---|---|
| every quest moment | quest (its id), title (its name, already in the reader's language), player |
Quest_Completed, Quest_Parked | parked; a parked one adds reason (collect, no_space, away) and, when the quest is collected somewhere in particular, turnIn (character or accept_site) |
Quest_Claimed, Achievement_Claimed | collected (true when the player came back for a parked reward) plus granted / queued / failed reward counts |
Quest_Objective_Progressed | objective (the step's id), step (its own sentence), current, required, finished |
| every achievement moment | achievement, title, icon, points, player; the unlock adds awaiting_claim, and an announcing achievement adds announceTitleKey, announceBodyKey, announceSkill, announceAmount |
Rare_Find | player only - it fires for whatever rare roll authored it, so the words are about the find, not any one item |
A Command line may use {player} and any of those values in braces. Each shipped file's own comment repeats the list for its moment.
Overriding one
A moment is content, so it layers like any other: ship a file with the same name from your own pack, or drop one into a pack you already load, and yours wins. Deleting the toast group and keeping the sound is a valid file, and so is a file with nothing but a Broadcast.
Prefer a content pack: author one file per moment under Server/ZiggfreedCommon/FeedbackMoments/ inside a pack folder, zip it, and drop the .zip into your server Mods/ folder. Building the zip.
A player's own quest-notification setting still has the last word on the personal toast, graded per moment: all sees every progress tick, milestones the authored marks and the finish, objectives the finish only, quests no step toasts, and none nothing at all (a player on none no longer sees the red inventory-full notice either; the reward still waits, and the Quests page still shows it). Only the personal toast is gated that way; a banner, a sound and a console command belong to the moment.
The server-wide banner an achievement shows is authored on the achievement itself, as an Announce block carrying TitleKey and BodyKey; how it is drawn is authored once for every achievement, in Achievement_Unlocked.json. The older written-out Title / Body leaves and the owner-file announcementCommand are retired: an achievement file still carrying announcementCommand gets one warning at start naming the replacement (a Command group on Achievement_Unlocked.json, or a Variants entry scoped by "When": {"achievement": "<id>"}) and the leaf is otherwise ignored.
Because the moment is announced by the engine for everything in the shared runtime, these files also dress a quest or achievement the MMO does not own (a board contract, or another mod's content on the same server). A line whose named value the moment did not carry is skipped rather than drawn with a gap, so content with no title stays silent; a server running two mods that both want their own words gives one of them its own moment ids rather than sharing these.
Colored Combat Text
Two combat moments float colored text above an entity. The text is fully localized into every supported language and ASCII-only, so it renders cleanly everywhere.
| Moment | What you see | Color | Anchored on |
|---|---|---|---|
| Critical hit | A red CRIT! on the struck target | Red (MMO_CombatText_Red) | The target you hit |
| Skill level-up | A gold <Skill> LV <n> | Gold (MMO_CombatText_Gold) | The leveling player |
Only crits and level-ups float. Kill XP and per-action XP show in the corner notification feed instead of floating above mobs, the combo finisher keeps its bonus and notification but does not float, and lifesteal keeps its notification. One color renders per entity per instant, so a held crit color also tints that hit's damage number red - that is intended.
Critical-hit corner notifications ship off by default: the red floating "CRIT!" already marks every crit, so the separate corner toast is redundant. Anyone who wants both can re-enable the crit toggle in Settings and the choice sticks.

combat-text.json
Colored combat text lives in mods/mmoskilltree/combat-text.json. It is override-based - only the fields you change are written; everything else tracks the shipped defaults. A master enabled flag gates the whole feature, and crits and level-ups each have their own toggle.
{
"schemaVersion": 1,
"enabled": true,
"crits": true,
"levelUps": true,
"critStyle": "MMO_CombatText_Red",
"levelUpStyle": "MMO_CombatText_Gold"
}| Field | Type | Default | Description |
|---|---|---|---|
enabled | - | true | Master switch for all colored combat text. Nothing floats when off. |
crits | - | true | Toggles the red crit popup on its own. |
levelUps | - | true | Toggles the gold level-up popup on its own. |
critStyle / levelUpStyle | - | Red / Gold | The CombatText asset id each popup renders in. Point either at any authored style; an unknown id degrades to plain white. |
The Feedback Layer
Behind the floating text, one service emits a moment and fans it out to a sound, an optional camera shake, the notification feed, and (in menus) a toast - all in lockstep, all from one call. Each moment maps to a row in feedback.json that carries the sound and camera ids for that event.
Moments that fire a sound or a shake
| Moment | When it fires | Default sound |
|---|---|---|
KILL_XP | Per-kill skill XP, anchored on the slain mob | Off (high frequency) |
CRIT | A critical hit lands | Off (high frequency) |
COMBO_FINISHER | A combo finisher triggers | Off (high frequency) |
LEVEL_UP | A skill levels up | Off (level-up plays its own reward sound) |
QUEST_ACCEPT | A quest or bounty is accepted, from any surface | SFX_Drop_Items_Paper |
QUEST_ABANDON | A quest is abandoned | SFX_Generic_Crafting_Failed |
BOUNTY_COMPLETE | All objectives on a bounty are met | SFX_Chest_Legendary_Open |
BOUNTY_REROLL | A bounty board is rerolled | SFX_Coins_Land |
SHOP_PURCHASE | An offer is bought at any storefront | SFX_Coins_Land |
Combat moments (KILL_XP, CRIT, COMBO_FINISHER, LEVEL_UP) ship with their sound off by default because they fire often and a default chime would be spammy. Quest, bounty, and shop moments ship with a sensible stock sound. Set a sound id on any row to opt a moment in.
Camera shake
Every moment can also drive a brief camera shake, written straight to the player the same way the engine drives its own damage shake. Camera shake is off by default for every moment (no shake assets ship); the channel exists for servers that want it. Set a cameraEffect id and an intensity on a row to turn it on.
feedback.json
The sound and camera channels live in mods/mmoskilltree/feedback.json. Each moment is one row; an empty sound or cameraEffect string means that channel is off for that moment.
{
"enabled": true,
"events": {
"QUEST_ACCEPT": { "sound": "SFX_Drop_Items_Paper", "cameraEffect": "", "cameraIntensity": 1.0 },
"QUEST_ABANDON": { "sound": "SFX_Generic_Crafting_Failed", "cameraEffect": "", "cameraIntensity": 1.0 },
"BOUNTY_COMPLETE": { "sound": "SFX_Chest_Legendary_Open", "cameraEffect": "", "cameraIntensity": 1.0 },
"SHOP_PURCHASE": { "sound": "SFX_Coins_Land", "cameraEffect": "", "cameraIntensity": 1.0 },
"CRIT": { "sound": "", "cameraEffect": "", "cameraIntensity": 1.0 }
}
}The top-level enabled flag gates the entire sound and camera layer; turning it off leaves colored combat text and the notification feed untouched, since those are governed separately.
Turning It Off
There are independent switches at every level, so you can keep exactly what you want.
- All colored combat text - set
enabled: falseincombat-text.json. - Just crits or just level-ups - flip
critsorlevelUpsincombat-text.json. - All sounds and camera shake - set
enabled: falseinfeedback.json, or clear a single moment'ssound/cameraEffectstring. - Corner notifications - players toggle individual notification categories (including the crit toast) in their in-game Settings, and the choice persists.
Quest accept and abandon cues are shared across every surface, so the bounty board, the NPC quest page, dialogue, and the quest log all play the identical cue. See Quests and Combo Finishers for the gameplay those moments come from.