Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f24128a
Gamepad: buttons Ding does not use go on to the game
moust4ki Oct 15, 2026
bea5c8b
Take this guide up where I am: keep the player's place
moust4ki Oct 15, 2026
01f796a
The guide window stays in combat by default
moust4ki Oct 15, 2026
4958cfe
Smoke test: the panel stays in combat by default, minimizes with the …
moust4ki Oct 15, 2026
a9de9e8
Dungeon quests pack: registry, category, entrance step passed from in…
moust4ki Oct 15, 2026
3bd54dd
tools/draft_dungeon.lua: draft a dungeon's quest guide for a side
moust4ki Oct 15, 2026
2d437ac
Ding! - Dungeon quests: 41 guides, one per dungeon and side
moust4ki Oct 15, 2026
2b8106f
Docs and changelog for Ding! - Dungeon quests (v1.7.0)
moust4ki Oct 15, 2026
e9e6fc7
Translate Ding! - Dungeon quests into the 7 languages
moust4ki Oct 15, 2026
cceeb82
Arrival at 6 yards, areas at 25; the aids on for a new install
moust4ki Oct 15, 2026
070cf1c
Arrow test: areas at 25 / 40 yards, a place reached at 6 yards
moust4ki Oct 15, 2026
fd4359f
Merge claude/dungeon-quests into release v1.7.0
moust4ki Oct 15, 2026
7e3ad98
Leveling guides: the floor key, read from the route's notes
moust4ki Oct 15, 2026
be9a44a
Class quests and example pack: floor where the note names one
moust4ki Oct 15, 2026
105dd2e
Arrow: say the target's floor near it and on arrival, and stay
moust4ki Oct 15, 2026
909a761
The floor step key: import, check_guides and the guide format docs
moust4ki Oct 15, 2026
7060627
Floor tests, smoke scenario and changelog
moust4ki Oct 15, 2026
1fdbe2e
Merge remote-tracking branch 'origin/main' into claude/dungeon-quests…
moust4ki Oct 15, 2026
6554dd5
Merge main (v1.6.1) into release v1.7.0
moust4ki Oct 15, 2026
d37b7a2
Merge claude/floor-hints into release v1.7.0
moust4ki Oct 15, 2026
70d299b
Merge claude/dungeon-quests (main merged) into release v1.7.0: conten…
moust4ki Oct 15, 2026
6628fd5
README: version 1.7
moust4ki Oct 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
# Changelog

## v1.7.0

Nothing else to install: QuestieDB is no longer needed, and TomTom stays optional.

- **Dungeon quests**: a new pack, **Ding! - Dungeon quests** (Settings > Guide, under
"Dungeons"), with one guide per dungeon for your side, listed by level (the ones above your
level are greyed, you can still pick them). Each guide takes you to every quest giver of that
dungeon and through the chains that lead to them, in an order that keeps the trips short, then
to the dungeon's entrance, then through the dungeon with a note for each quest (which boss or
monster drops what, where it waits), and back to the turn-ins and the quests they open. Every
dungeon from Ragefire Chasm, the Deadmines and Wailing Caverns to Blackrock Depths, Lower
Blackrock Spire, Dire Maul, Scholomance and Stratholme, and Forever's own: the Hall of Thanes,
the Ruins of Lordaeron, the Excavation Site of the Wetlands and the City of Dalaran.
- The first step of each chain says what it brings (a weapon of your choice, armor of your
kind, a ring, a big amount of XP) and when it is long or far away; very long or far chains are
optional and say why. A quest that starts from a drop shows only once the drop is in your bags.
Raids, the Upper Spire and the level 60 armor-set chains are not in the pack.
- A step that takes you to a dungeon's entrance now passes once you are inside: the game gives
no position in a dungeon, and the step stayed on screen for the whole run.
- "Take this guide up where I am" keeps your place: it stops only on a quest of your log still
to finish or a quest of the route you never took. With "Elite quests" turned on, it used to
send you back to the first elite quest of the section.
- The guide window stays as it is in combat: minimizing it there is now an option, off by
default (Settings > Appearance, "Minimize the guide in combat"). It was on for everyone: turn it
on again there if you liked it.
- The arrow says "You're here" 6 yards from the place or the NPC (it was 10, and the arrow faded
while you still had a few steps to walk), and "You're in the area" 25 yards from the monsters
or the objects to gather (it was 40, more than an attack's reach).
- The aids are on for a new install: the quest marks and counters above the monsters, the line
in their tooltip, and enemy nameplates shown farther. The game hides the enemy nameplates
until you press V: Ding shows them once so that its counters appear, and says so in the chat.
- Upstairs and downstairs: when the NPC or the object waits on another floor, the arrow no
longer says "You're here" in the middle of the building. It says "The NPC is one floor up",
"at the top" or "one floor down", and stays on screen until you find the stairs.

## v1.6.1

Nothing else to install: QuestieDB is no longer needed, and TomTom stays optional.
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ Ding/UI/ Ding's own frames: Themes, Widgets (tokens, shapes, fonts), Game
Ding/Locales/ interface texts (enUS.lua + one file per language); Guides/<locale>/: guide
translations
Ding/DB/ DingDB, the game database, generated by tools/build_data.lua (never by hand)
Ding/Guides/ the packs: Leveling (generated by tools/write_leveling.lua), Zones, Items, Class
Ding/Guides/ the packs: Leveling (generated by tools/write_leveling.lua), Zones, Items, Class,
Dungeons (drafted by tools/draft_dungeon.lua, finished by hand)
Ding/Media/ kit textures (white, tinted) and the icon; Noto Sans fonts (OFL)
examples/ DingExamplePack: example guide pack (separate addon, not in the zip), tested
tools/ offline Lua 5.1 scripts: checks, tests (tests/*_test.lua), wow_mock.lua (mocked
Expand Down
6 changes: 6 additions & 0 deletions Ding/Core/Compat.lua
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,12 @@ function C.TargetName()
return type(name) == "string" and name or nil
end

-- Inside an instance (a dungeon, a raid, a battleground): the game gives no position there.
function C.InInstance()
local ok, inside = safe(IsInInstance)
return ok and plain(inside) and true or false
end

function C.InGroup()
local ok, group = safe(IsInGroup)
if ok then return plain(group) and true or false end
Expand Down
20 changes: 20 additions & 0 deletions Ding/Core/Data.lua
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,26 @@ function D:DungeonEntrances(areaID)
return out
end

-- The dungeon (AreaID) whose entrance lies at a point (a `go` step's { uiMapId, x, y }, within
-- 1.5 % of the map), or nil.
function D:DungeonAtEntrance(map, x, y)
local areas = {}
if self.backend == "ding" then
for area in pairs(DB.zones.dungeons) do areas[#areas + 1] = area end
else
local private = questieZones()
local dungeons = private and private.dungeons
for area in pairs(type(dungeons) == "table" and dungeons or {}) do areas[#areas + 1] = area end
end
table.sort(areas)
for _, area in ipairs(areas) do
for _, p in ipairs(self:DungeonEntrances(area)) do
if p.map == map and math.abs(p.x - x) <= 1.5 and math.abs(p.y - y) <= 1.5 then return area end
end
end
return nil
end

-- Items a quest's objectives ask for.
function D:ObjectiveItems(questID)
local out = {}
Expand Down
47 changes: 25 additions & 22 deletions Ding/Core/Ding.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Ding.version = (type(version) == "string" and version ~= "" and not version:find
local DEFAULTS = {
panel = {
shown = true, point = "TOPRIGHT", x = -260, y = -220, scale = 1, width = 300,
locked = false, bgAlpha = 0.60, compactInCombat = true,
locked = false, bgAlpha = 0.60, compactInCombat = false,
hideInInstance = false, keyHints = true, itemButton = true, preview = 0, weaponReminder = true,
why = true, -- why the guide makes its choices: lines under surprising steps, tooltips (Core/Why.lua)
},
Expand All @@ -45,8 +45,9 @@ local DEFAULTS = {
trainerSteps = true, flightSteps = true, autoEquip = false, autoFly = false,
suggestGear = true, -- a better piece of gear in the bags: row, banner, item button (Core/Gear.lua)
-- Quest monsters (UI/QuestMobs.lua): marks on enemy nameplates, the tooltip line, and
-- the enemy nameplates' distance raised to the client's maximum.
mobIcons = true, mobTooltip = true, farPlates = false,
-- the enemy nameplates' distance raised to the client's maximum: every aid on for a new
-- install (Yendi's choice, v1.7.0).
mobIcons = true, mobTooltip = true, farPlates = true,
dialogScale = 0.85, -- the settings, the NPC directory, the setup assistant, import (UI.Dialog)
-- The look (UI/Themes.lua); opacityTouched: the player moved the opacity slider, which
-- then no longer follows the theme's default.
Expand Down Expand Up @@ -103,9 +104,9 @@ function Ding:ResetSettings()
self.db.ui.welcomed, self.db.ui.setupDone = true, true
end

-- Settings saved before the design (layout 1), then before themes (layout 2), in the current
-- shape, once.
local LAYOUT = 3
-- Settings saved before the design (layout 1), before themes (layout 2), before v1.7.0
-- (layout 3), in the current shape, once.
local LAYOUT = 4
local OLD_OPACITY = 0.85 -- the background's default before themes
local function migrate(db)
-- The arrow's style before the skins: the game's minimap arrow stays; Ding's arrow is now
Expand All @@ -116,7 +117,7 @@ local function migrate(db)
end
local layout = db.layout or 1
if layout >= LAYOUT then return end
if layout >= 2 then
if layout == 2 then
-- v1.5.0: the Halo theme for everyone (the changelog says so). A background left at
-- the old default follows the theme's; one the player chose is kept.
local alpha = tonumber(db.panel.bgAlpha)
Expand All @@ -126,22 +127,24 @@ local function migrate(db)
db.panel.bgAlpha = DEFAULTS.panel.bgAlpha
end
db.ui.theme = DEFAULTS.ui.theme
db.layout = LAYOUT
return
end
local arrow = db.arrow
if arrow.point == "TOP" and arrow.x == 0 and arrow.y == -120 then
arrow.point, arrow.x, arrow.y = "CENTER", 0, 120
end
if tonumber(arrow.scale) then
arrow.size = math.max(40, math.min(80, math.floor(56 * arrow.scale / 4 + 0.5) * 4))
elseif layout < 2 then
local arrow = db.arrow
if arrow.point == "TOP" and arrow.x == 0 and arrow.y == -120 then
arrow.point, arrow.x, arrow.y = "CENTER", 0, 120
end
if tonumber(arrow.scale) then
arrow.size = math.max(40, math.min(80, math.floor(56 * arrow.scale / 4 + 0.5) * 4))
end
arrow.scale = nil
-- Ding's arrow first: TomTom's was the default before the design.
arrow.tomtom = false
db.panel.scale = math.max(0.5, math.min(1.5, tonumber(db.panel.scale) or 1))
db.panel.textSize = nil
db.ui.opacity = nil
end
arrow.scale = nil
-- Ding's arrow first: TomTom's was the default before the design.
arrow.tomtom = false
db.panel.scale = math.max(0.5, math.min(1.5, tonumber(db.panel.scale) or 1))
db.panel.textSize = nil
db.ui.opacity = nil
-- v1.7.0: the guide window stays as it is in combat (Yendi's choice, the changelog says
-- so); minimizing it there is an option, off. The old default, on, is dropped once.
db.panel.compactInCombat = false
db.layout = LAYOUT
end
Ding.Migrate = migrate
Expand Down
81 changes: 61 additions & 20 deletions Ding/Core/Engine.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ E.hold = false -- true after /ding prev: stay on the shown step until "next"
E.finished = false
E.logReady = false -- the client has sent the quest log (it comes a moment after login)

local DEFAULT_RADIUS = 10 -- yards
-- Yards from a step's place that reach it: about the distance to speak to an NPC (a tester:
-- "you're here" at 10 yards, a few more steps to walk without the arrow).
local DEFAULT_RADIUS = 6 -- yards
local LOOKAHEAD = 5 -- steps checked to detect progress past manual steps
local RESUME_WINDOW = 10 -- seconds after loading a guide where it resyncs on turn-ins
local START_TIMEOUT = 5 -- seconds the engine waits for the quest log at login
Expand Down Expand Up @@ -845,9 +847,11 @@ local function saveStep(index)
end

-- Resuming up to a step (E:Resume): saved per character, so a reload keeps it.
local function setResumeTo(step)
-- strict: a resume of "take this guide up where I am" (E:Reset, E:LeftBehind).
local function setResumeTo(step, strict)
E.resumeTo = step
Ding.char.resumeTo = step and E.guide and { guide = E.guide.id, step = step } or nil
E.resumeStrict = step and strict and true or nil
Ding.char.resumeTo = step and E.guide and { guide = E.guide.id, step = step, strict = E.resumeStrict } or nil
end

---------------------------------------------------------------------------
Expand All @@ -861,11 +865,11 @@ end
-- the quests alone: quests done out of the guide's order (a zone done earlier, in another
-- guide) sent characters far ahead, past the quests, trainers and trips still to do
-- (Redridge before Duskwood, then the Wetlands at level 24 before the route's Wetlands).
function E:Resume(id, silent)
function E:Resume(id, silent, strict)
local guide = Ding:GetGuide(id)
if guide then followSteps(guide) end
local last = Ding.char.lastStep and Ding.char.lastStep[id]
return self:SetGuide(id, 1, silent, last)
return self:SetGuide(id, 1, silent, last, strict)
end

-- Switches to a guide pack: the section the character last followed in it (Ding.char.packGuide)
Expand Down Expand Up @@ -898,8 +902,8 @@ local function endTrip()
return true
end

-- resumeTo: optional, see E:Resume.
function E:SetGuide(id, index, silent, resumeTo)
-- resumeTo, strict: optional, see E:Resume and E:Reset.
function E:SetGuide(id, index, silent, resumeTo, strict)
local guide = Ding:GetGuide(id)
if not guide then
Ding:Print(L.GUIDE_UNKNOWN:format(tostring(id)))
Expand All @@ -909,7 +913,7 @@ function E:SetGuide(id, index, silent, resumeTo)
followSteps(guide)
self.guide = guide
self.index = index or 1
setResumeTo(resumeTo and resumeTo > self.index and resumeTo or nil)
setResumeTo(resumeTo and resumeTo > self.index and resumeTo or nil, strict)
self.skipNotes, self.passNotes = nil, nil
self.hold = false
self.finished = false
Expand Down Expand Up @@ -973,13 +977,30 @@ local function turnedInHere(step)
return false
end

-- "Take this guide up where I am" (E:Reset) stops on a quest of the route the character can
-- still take: not behind an option (an elite, a dungeon's, turned on since), not optional, open
-- to it now (level, prerequisites). What the player left behind on purpose passes.
local function stillToTake(step)
if step.optional or step.dungeon then return false end
for _, o in ipairs(STEP_OPTIONS) do
if step[o.key] then return false end
end
for _, q in ipairs(step.accept or {}) do
if not isAccepted(q) and not tooLow(q) and not isGrey(q) and not missingPrereq(q) then return true end
end
return false
end

-- Resuming: a step the player left behind (no work left on a quest in the log, before the
-- player's place) is passed. Not one where the player just turned a quest in: its next quest
-- comes a moment later, from the same NPC (in game, "Ormer's Revenge" turned in, its sequel
-- not taken yet: the resume passed the step that takes it and the one that completes it).
function E:LeftBehind(step, index)
return self.resumeTo ~= nil and index < self.resumeTo and logKnown() and not backlog(step)
and not turnedInHere(step)
if not (self.resumeTo ~= nil and index < self.resumeTo and logKnown() and not backlog(step)
and not turnedInHere(step)) then
return false
end
return not (self.resumeStrict and stillToTake(step))
end

-- Why the guide passed a step for the character by itself, in a word the window says
Expand Down Expand Up @@ -1221,17 +1242,24 @@ function E:Prev()
self:Refresh()
end

-- "Take this guide up where I am": forgets the steps validated by hand or skipped (what
-- is not really done is to do again), then finds the player's place again from their
-- quests, instead of starting over at step 1.
-- "Take this guide up where I am": forgets the steps validated by hand or skipped, then takes
-- the section up from its start to the character's place (a strict resume, E:LeftBehind): it
-- stops on what is really left there, a quest of the log with work left or a quest of the
-- route still to take; what the player left behind on purpose passes, the quests behind an
-- option turned on since included. In game (15/10/2026): "Elite quests" turned on, the button
-- sent the player from step 38 back to step 5, an elite quest never taken.
function E:Reset()
if not self.guide then return end
Ding.char.manual[self.guide.id] = nil
if Ding.char.skipped then Ding.char.skipped[self.guide.id] = nil end
if Ding.char.markKeys then Ding.char.markKeys[self.guide.id] = nil end
if Ding.char.lastStep then Ding.char.lastStep[self.guide.id] = nil end
if Ding.char.lastKey then Ding.char.lastKey[self.guide.id] = nil end
self:Resume(self.guide.id, true)
local id = self.guide.id
local place = math.max(Ding.char.lastStep and Ding.char.lastStep[id] or 1,
self.browseFrom or 1, self.index or 1)
Ding.char.manual[id] = nil
if Ding.char.skipped then Ding.char.skipped[id] = nil end
if Ding.char.markKeys then Ding.char.markKeys[id] = nil end
Ding.char.lastStep = Ding.char.lastStep or {}
Ding.char.lastStep[id] = place
if Ding.char.lastKey then Ding.char.lastKey[id] = nil end
self:Resume(id, true, true)
end

-- At login the quest log arrives a moment later: the player is placed once it is there
Expand Down Expand Up @@ -1259,7 +1287,8 @@ function E:Start()
self.changedGuide = nil
elseif guide then
local resume = Ding.char.resumeTo
self:SetGuide(id, Ding.char.step or 1, true, resume and resume.guide == id and resume.step or nil)
local mine = resume and resume.guide == id
self:SetGuide(id, Ding.char.step or 1, true, mine and resume.step or nil, mine and resume.strict)
else
-- New character, or its guide is gone (a pack disabled or updated): a section of
-- the character's pack when it is still there and has one for them, else Ding's.
Expand All @@ -1273,9 +1302,20 @@ function E:Start()
end

-- Re-evaluate after any game state change.
-- A trip to a dungeon's entrance ends once the player is inside an instance: there the game
-- gives no position, and the step would stay on screen for the whole run (the "Enter ..."
-- steps of Ding! - Dungeon quests). Marked done like a point reached.
local function enteredDungeon()
if E.hold or not Ding.Data.available then return end
local step = E:CurrentStep()
if not (step and step.go and not isManuallyDone(E.index) and Compat.InInstance()) then return end
if Ding.Data:DungeonAtEntrance(step.go[1], step.go[2], step.go[3]) then setManuallyDone(E.index, true) end
end

function E:Update()
if not self.guide then return end
local oldGuide, oldIndex = self.guide.id, self.index
enteredDungeon()
local moved = self:Advance()
if moved then
Ding.Timing:Finish(oldGuide, oldIndex)
Expand Down Expand Up @@ -1979,6 +2019,7 @@ Ding:On("PLAYER_XP_UPDATE", function()
if step and step.level then scheduleUpdate() end
end)
Ding:On("ZONE_CHANGED_NEW_AREA", scheduleUpdate)
Ding:On("PLAYER_ENTERING_WORLD", scheduleUpdate)
Ding:On("HEARTHSTONE_BOUND", function()
local step = E:CurrentStep()
if step and step.sethearth then
Expand Down
7 changes: 5 additions & 2 deletions Ding/Core/Import.lua
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ local STEP_KEYS = {
faction = "faction", use = "items", useitem = "items", equip = "items", destroy = "items",
bank = "items", unbank = "items", keep = "items", kill = "npcPairs", collect = "itemPairs",
buy = "buys", talk = "npcs", gossip = "index", object = "objects", spots = "points",
patrol = "points", revive = "revive", when = "when", why = "why",
patrol = "points", revive = "revive", when = "when", why = "why", floor = "floor",
}
local WHEN_KEYS = {
done = "quests", notdone = "quests", log = "quests", notlog = "quests", ready = "quests",
Expand All @@ -69,6 +69,7 @@ local SAMPLES = {
next = '"guide-id"', when = "{ done = 456 }", held = "{ { 5469, 4 } }", steps = "{ { accept = 456 } }",
category = '"leveling" / "items"', guides = "{ { steps = { ... } } }", pack = '{ title = "text" }',
why = '"text" / { kind = "detour", quest = 67, xp = 9000 }', leftOut = '{ [456] = "text" }',
floor = '"up" / "top" / "down"',
}

-- The order of the keys in an exported guide.
Expand All @@ -81,7 +82,7 @@ local STEP_ORDER = {
"revive", "train", "fly", "vendor", "repair", "kill", "collect", "buy", "talk", "gossip", "useitem",
"object", "equip", "destroy", "bank", "unbank", "abandon", "keep", "spots", "patrol", "use",
"sticky", "noauto", "dungeon", "elite", "optional", "faction", "races", "classes", "when", "text",
"note", "why", "done", "notdone", "log", "notlog", "ready", "item", "noitem", "minlevel", "maxlevel",
"floor", "note", "why", "done", "notdone", "log", "notlog", "ready", "item", "noitem", "minlevel", "maxlevel",
}

---------------------------------------------------------------------------
Expand Down Expand Up @@ -191,6 +192,8 @@ local function checkValue(kind, key, v, at, ctx)
if not known then at("warning", L.IMPORT_CATEGORY:format(v)) end
elseif kind == "faction" then
if not FACTION_RACES[v] then expects() end
elseif kind == "floor" then
if v ~= "up" and v ~= "top" and v ~= "down" then expects() end
elseif kind == "races" or kind == "classes" then
local list = names(v)
if not list then return expects() end
Expand Down
Loading
Loading