| name | wow-encounter-detection |
| description | Documents Chronicle's WoW encounter/instance detection system for Turtle WoW and Vanilla WoW.
Covers registering raid instances, defining bosses and trash mobs via Identity maps, creating
boss-specific Character handlers with custom mechanics (multi-phase, add tracking), and zone
parsing from combat logs. Key patterns: CommonFactory for simple instances, characterFactory
for boss behaviors, Identity struct for hostile classification.
|
WoW Encounter Detection
When to Use This Skill
Use this skill when:
- Adding support for a new raid or dungeon instance
- Defining new bosses, adds, or trash mobs
- Implementing custom boss mechanics (multi-phase, add tracking, special death conditions)
- Debugging encounter start/end detection issues
- Understanding how fights are tracked and finalized
Architecture Overview
┌─────────────────────────────────────────────────────────────────────┐
│ Combat Log Parser │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Registry (registry/registry.go) │
│ - Maps zone names → Instance factories │
│ - DefaultRegistry() registers all known instances │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Instance (instances/common.go) │
│ - Owns Characters collection and Identifier │
│ - Processes messages, tracks fights, finalizes encounters │
└─────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌────────────────────────┐ ┌─────────────────────────────────┐
│ Identifier │ │ Characters (character/lookup) │
│ (instances/instance) │ │ - Creates Character per GUID │
│ - Maps Entry ID → │ │ - Factories for special bosses │
│ Identity │ │ - Tracks activity state │
│ - Boss/Trash/Hostile │ └─────────────────────────────────┘
└────────────────────────┘ │
▼
┌─────────────────────────────────┐
│ Character (character/*.go) │
│ - Processes messages │
│ - Tracks activity periods │
│ - Custom boss logic │
└─────────────────────────────────┘
Key Types
| Type | Location | Purpose |
|---|
Instance | instances/instance.go | Interface for dungeon/raid instances |
Common | instances/common.go | Default instance implementation |
CommonFactory | instances/common.go | Factory for creating instances |
Identity | instances/instance.go | Hostile classification (boss/trash) |
Identifier | instances/instance.go | Maps creature entry IDs to Identity |
Character | character/character.go | Interface for trackable units |
Characters | character/lookup.go | Registry + factory dispatch |
Data Flow
- Zone Change: Parser emits
ZONE_INFO → Registry finds matching Instance
- Message Processing: Instance receives messages → dispatches to Characters
- Activity Tracking: Characters track when hostiles become active/inactive
- Fight Detection: Instance monitors character activity changes → groups into fights
- Finalization: Instance produces
Encounter objects with kill/wipe status
Adding a New Raid Instance (Workflow)
Step 1: Define Hostiles in instances/hostiles.go
func MyRaidHostiles() map[uint32]Identity {
hostile := make(map[uint32]Identity)
LoadAdds(hostile, map[uint32]string{
12345: "Trash Mob A",
12346: "Trash Mob B",
})
LoadBosses(hostile, map[uint32]string{
99001: "First Boss",
99002: "Second Boss",
})
return hostile
}
Step 2: Create Instance Factory in instances/instances.go
var MyRaid = (&CommonFactory{
Name: "My Raid",
ZoneName: "my raid",
Hostiles: FromMap(MyRaidHostiles()),
}).New
Step 3: Register in registry/registry.go
func DefaultRegistry(logger *slog.Logger) *Registry {
r := NewRegistry(logger)
r.Register(wrap(instances.MyRaid))
r.RegisterWithComment(wrap(instances.MyRaid), "Only supports normal mode")
return r
}
Complete Example: Adding Onyxia's Lair
func OnyxiaHostiles() map[uint32]Identity {
hostile := make(map[uint32]Identity)
LoadAdds(hostile, map[uint32]string{
12129: "Onyxian Warder",
11262: "Onyxian Whelp",
})
LoadBosses(hostile, map[uint32]string{
10184: "Onyxia",
})
return hostile
}
var Onyxia = (&CommonFactory{
Name: "Onyxia's Lair",
ZoneName: "onyxia's lair",
Hostiles: FromMap(OnyxiaHostiles()),
}).New
r.Register(wrap(instances.Onyxia))
Defining Bosses and Hostiles
Identity Struct
type Identity struct {
Hostile bool
EncounterName string
Boss bool
}
Helper Functions
LoadAdds(hostile, map[uint32]string{
12345: "Trash Mob",
})
LoadBosses(hostile, map[uint32]string{
99001: "Boss Name",
})
Entry ID Sources
Entry IDs are the creature template IDs from the WoW database. For Turtle WoW:
- Check in-game with
/run print(UnitGUID("target")) - the entry is embedded in the GUID
- Use wowhead classic or turtle wow database sites
- Parse from existing combat logs
Multi-Unit Encounters
For encounters where multiple units share one encounter name (e.g., High Priest Thekal):
LoadBosses(hostile, map[uint32]string{
11348: "High Priest Thekal",
11347: "High Priest Thekal",
14599: "High Priest Thekal",
})
All three units map to the same encounter name.
Boss-Specific Behavior
For bosses with special mechanics, create a custom Character implementation.
When to Use Custom Characters
- Boss has phases with immunity/submerge (e.g., Ragnaros)
- Encounter ends when adds die, not the boss (e.g., Majordomo)
- Boss transforms or spawns a new unit (e.g., Sneed's Shredder)
- Special death timing (e.g., Core Hounds respawning)
Factory Pattern
Custom characters are registered in character/lookup.go:
var characterFactories = []characterFactory{
NewTotemCharacter,
NewCritterCharacter,
NewRagnarosCharacter,
NewMajordomoPartyCharacter,
}
Simple Example: Ragnaros (Extended Death Timer)
func NewRagnarosCharacter(id guid.GUID, all *Characters) (Character, bool) {
if !id.IsCreature() {
return nil, false
}
entry, ok := id.GetEntry()
if !ok {
return nil, false
}
if entry != 11502 {
return nil, false
}
c := NewCommonCharacter(id, all)
c.SetRecentlySlainDuration(time.Second * 15)
return c, true
}
Complex Example: Majordomo (Add-Based Encounter)
Majordomo doesn't die - encounter ends when all 8 adds die:
type MajordomoParty struct {
*Common
all *Characters
isMajordomo bool
party map[guid.GUID]Character
}
func NewMajordomoPartyCharacter(id guid.GUID, all *Characters) (Character, bool) {
entry, ok := id.GetEntry()
if !ok {
return nil, false
}
isMajordomo := false
switch entry {
case flamewakerElite, flamewakerHealer:
case majorDomoEntry:
isMajordomo = true
default:
return nil, false
}
return &MajordomoParty{
Common: NewCommonCharacter(id, all),
all: all,
isMajordomo: isMajordomo,
party: make(map[guid.GUID]Character),
}, true
}
func (c *MajordomoParty) Process(m messages.Message) error {
if allAddsDead {
c.Died("all_adds_dead", m)
}
}
Character Interface
type Character interface {
ID() guid.GUID
String() string
Died(reason string, m messages.Message)
Process(m messages.Message) error
Periods() []period.Period
RecentlySlain(m messages.Message) bool
IsActive() bool
CurrentPeriod() (period.Period, bool)
}
Zone Parsing
Zones are parsed from ZONE_INFO: lines in the combat log:
ZONE_INFO:<timestamp>&<zone_name>&<instance_id>
Example: ZONE_INFO:1706745600&molten core&12345
Zone Struct
type Zone struct {
Seen time.Time
Name string
InstanceID uint32
}
Zone Matching
Instances match zones by lowercase name comparison:
func (c *Common) MatchesZone(z zone.Zone) bool {
return strings.ToLower(z.Name) == c.zoneNameMatch
}
Important: Zone names in CommonFactory must be lowercase.
Fight Detection Logic
Fight Lifecycle
- Start: First hostile becomes active (takes/deals damage)
- Tracking: More hostiles join via activity
- End: All tracked hostiles become inactive (killed or timed out)
- Finalization: Determine kill type, name encounter
Kill Types
const (
KillTypeClean
KillTypePartial
KillTypeWipe
)
Activity Timeout
Characters become inactive after 60 seconds without activity:
const InactivityTimeout = time.Second * 60
This handles:
- Boss resets (no damage for 60s → encounter ends as wipe)
- Add despawns
- Combat drops
Anti-Patterns
Don't Hard-Code Outside Factories
❌ Bad: Checking entry IDs directly in Common.Process()
func (c *Common) Process(m messages.Message) error {
if entryID == 12018 {
}
}
✅ Good: Use characterFactory registration
var characterFactories = []characterFactory{
NewMajordomoPartyCharacter,
}
Don't Forget to Register
Every new instance needs both:
- Hostiles function in
hostiles.go
- Factory variable in
instances.go
- Registration in
registry.go
Don't Use Mixed Case Zone Names
❌ Bad:
ZoneName: "Molten Core",
✅ Good:
ZoneName: "molten core",
Don't Forget Boss Flag
Bosses need Boss: true to affect kill/wipe detection:
❌ Bad: Boss without flag
LoadAdds(hostile, map[uint32]string{
10184: "Onyxia",
})
✅ Good: Use LoadBosses
LoadBosses(hostile, map[uint32]string{
10184: "Onyxia",
})
Testing
Test Files
character/*_test.go - Unit tests for character handlers
registry/registry_test.go - Instance registration tests
Testing Custom Characters
func TestMajordomoParty(t *testing.T) {
db := unitdb.New()
chars := character.NewCharacters(db)
majordomoID := guid.NewCreature(majorDomoEntry, 1)
char, ok := character.NewMajordomoPartyCharacter(majordomoID, chars)
require.True(t, ok)
}
File Reference
| File | Purpose |
|---|
registry/registry.go | Instance factory registration |
instances/instances.go | Instance factory definitions |
instances/hostiles.go | Hostile identity maps per instance |
instances/common.go | Common instance implementation |
instances/instance.go | Instance interface + Identity/Identifier |
character/lookup.go | Character factory dispatch |
character/character.go | Base Character interface |
character/common.go | Default character implementation |
character/*.go | Boss-specific handlers |
types/zone/zoneinfo.go | Zone parsing |