Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified assets/database/db.bin
Binary file not shown.
18,401 changes: 9,226 additions & 9,175 deletions assets/database/db.json

Large diffs are not rendered by default.

Binary file modified assets/database/leftover_db.bin
Binary file not shown.
23,009 changes: 11,529 additions & 11,480 deletions assets/database/leftover_db.json

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions proto/spell.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,48 @@ package proto;

option go_package = "./proto";

import "google/protobuf/duration.proto";

import "common.proto";
enum ItemEffectType {
NONE = 0;
PROC = 1;
ON_USE = 2;
}
message ScalingItemEffectProperties {
// keys are the numeric values of proto.common.Stat
map<int32, double> stats = 1;
double rppm_ilvl_modifier = 2;
}
message ItemEffect {
int32 spell_id = 1;
string label = 9;
ItemEffectType type = 2;
int32 effect_duration = 3; // seconds

map<int32, ScalingItemEffectProperties> scaling_options = 4;

oneof effect {
ProcEffect proc = 7;
OnUseEffect on_use = 8;
}
}

message ProcEffect {
double proc_chance = 1; // e.g. 0.20 = 20%
double rppm = 3;
int32 rppm_scale = 4;
map<int32, double> spec_modifiers = 5;
int32 icd = 2; // internal cooldown in seconds
}

message OnUseEffect {
int32 cooldown = 1; // seconds between uses

int32 category_id = 11;

int32 category_cooldown = 8;
}

message SpellEffect {
int32 id = 1;
Expand Down
1 change: 1 addition & 0 deletions proto/ui.proto
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ message UIItem {

FactionRestriction faction_restriction = 25;
map<int32, ScalingItemProperties> scaling_options = 29; // keys are the other ilvl variants that this item could potentially have
repeated ItemEffect item_effects = 30;
}

enum Expansion {
Expand Down
5 changes: 5 additions & 0 deletions sim/core/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,11 @@ func SpellSchoolFromProto(p proto.SpellSchool) SpellSchool {
}
}

const (
RPPM_HASTE = 1 << (iota)
RPPM_CRIT
)

/*
outcome roll hit/miss/crit/glance (assigns Outcome mask) -> If Hit, Crit Roll -> damage (applies metrics) -> trigger proc

Expand Down
1 change: 0 additions & 1 deletion sim/core/stats/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,6 @@ func (stats Stats) ToProtoArray() []float64 {
// shared indices between the two.
return stats[:ProtoStatsLen]
}

func (stats Stats) ToProtoMap() map[int32]float64 {
m := make(map[int32]float64, ProtoStatsLen)
for i := 0; i < int(ProtoStatsLen); i++ {
Expand Down
2 changes: 1 addition & 1 deletion tools/database/dbc/consumable.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func (consumable *Consumable) GetStatModifiers() *stats.Stats {
if effect.ID != 0 {
if spellEffects, ok := dbcInstance.SpellEffects[effect.SpellID]; ok {
for _, spellEffect := range spellEffects {
stat := spellEffect.ParseStatEffect()
stat := spellEffect.ParseStatEffect(false, 0)
stats.AddInplace(stat)
}
}
Expand Down
26 changes: 22 additions & 4 deletions tools/database/dbc/dbc.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ type DBC struct {
ItemArmorTotal map[int]ItemArmorTotal
ArmorLocation map[int]ArmorLocation
SpellScalings map[int]SpellScaling
Consumables map[int]Consumable // Item ID
ItemEffects map[int]ItemEffect // Parent Item ID
Consumables map[int]Consumable // Item ID
ItemEffects map[int]ItemEffect // Parent Item ID
ItemEffectsByParentID map[int][]ItemEffect // new
}

func NewDBC() *DBC {
Expand All @@ -46,6 +47,7 @@ func NewDBC() *DBC {
Consumables: make(map[int]Consumable),
ItemEffects: make(map[int]ItemEffect),
SpellScalings: make(map[int]SpellScaling),
ItemEffectsByParentID: make(map[int][]ItemEffect),
}
}

Expand Down Expand Up @@ -173,12 +175,28 @@ func (d *DBC) loadItemEffects(filename string) error {
}
}

for i := range effects {
effect := effects[i]
// Initialize maps if needed
if d.ItemEffects == nil {
d.ItemEffects = make(map[int]ItemEffect)
}
if d.ItemEffectsByParentID == nil {
d.ItemEffectsByParentID = make(map[int][]ItemEffect)
}

// Populate both maps
for _, effect := range effects {
// Single lookup by effect ID
d.ItemEffects[effect.ID] = effect
// Grouping by parent item ID
d.ItemEffectsByParentID[effect.ParentItemID] = append(
d.ItemEffectsByParentID[effect.ParentItemID],
effect,
)
}

return nil
}

func (d *DBC) loadRandomPropertiesByIlvl(filename string) error {
data, err := ReadGzipFile(filename)
if err != nil {
Expand Down
12 changes: 12 additions & 0 deletions tools/database/dbc/enums.go
Original file line number Diff line number Diff line change
Expand Up @@ -1060,3 +1060,15 @@ const (
BANDAGE
OTHER
)

type RPPMModifierType int

const (
RPPMModifierHaste RPPMModifierType = iota + 1 // 1
RPPMModifierCrit // 2
RPPMModifierClass // 3
RPPMModifierSpec // 4
RPPMModifierRace // 5
RPPMModifierIlevel // 6
RPPMModifierUnkAdjust // 7
)
2 changes: 1 addition & 1 deletion tools/database/dbc/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (item *Item) ToScaledUIItem(itemLevel int) *proto.UIItem {
WeaponType: weaponType,
WeaponSpeed: float64(item.ItemDelay) / 1000,
GemSockets: item.GetGemSlots(),
SocketBonus: item.GetGemBonus().ToProtoArray(),
SocketBonus: NullFloat(item.GetGemBonus().ToProtoArray()),
}

item.ParseItemFlags(uiItem)
Expand Down
198 changes: 197 additions & 1 deletion tools/database/dbc/item_effect.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package dbc

import (
"maps"

"github.com/wowsims/mop/sim/core"
"github.com/wowsims/mop/sim/core/proto"
"github.com/wowsims/mop/sim/core/stats"
)

// ItemEffect represents an item effect in the game.
type ItemEffect struct {
ID int // Effect ID
Expand All @@ -14,7 +22,6 @@ type ItemEffect struct {
ParentItemID int // Parent item ID
}

// ToMap converts the ItemEffect to a map representation.
func (e *ItemEffect) ToMap() map[string]interface{} {
return map[string]interface{}{
"ID": e.ID,
Expand All @@ -33,3 +40,192 @@ func (e *ItemEffect) ToMap() map[string]interface{} {
func GetItemEffect(effectId int) ItemEffect {
return dbcInstance.ItemEffects[effectId]
}

var emptyStats = stats.Stats{}

func (e *ItemEffect) ToProto(itemLevel int, levelState proto.ItemLevelState) *proto.ItemEffect {
pe := newProtoShell(e)

statsSpellID, ilvlRppmMod := applyTrigger(e, pe, itemLevel)

pe.ScalingOptions[int32(levelState)] = buildScalingProps(statsSpellID, itemLevel)
if ilvlRppmMod != 0.0 && ilvlRppmMod != 1.0 {
pe.ScalingOptions[int32(levelState)].RppmIlvlModifier = ilvlRppmMod
}
return pe
}

func newProtoShell(e *ItemEffect) *proto.ItemEffect {
sp := dbcInstance.Spells[e.SpellID]
return &proto.ItemEffect{
SpellId: int32(e.SpellID),
Label: sp.NameLang,
Type: proto.ItemEffectType_NONE,
EffectDuration: int32(sp.Duration) / 1000,
ScalingOptions: make(map[int32]*proto.ScalingItemEffectProperties),
}
}

func applyTrigger(e *ItemEffect, pe *proto.ItemEffect, itemLevel int) (int, float64) {
trig, statsSpellID := resolveTrigger(e.TriggerType, e.SpellID)
sp := dbcInstance.Spells[statsSpellID]
if sp.Duration > 0 {
pe.EffectDuration = sp.Duration / 1000
}
switch trig {
case ITEM_SPELLTRIGGER_ON_USE:
pe.Type = proto.ItemEffectType_ON_USE
pe.Effect = &proto.ItemEffect_OnUse{
OnUse: &proto.OnUseEffect{
Cooldown: int32(e.CoolDownMSec / 1000),
CategoryId: int32(e.SpellCategoryID),
CategoryCooldown: int32(e.CategoryCoolDownMSec / 1000),
},
}

case ITEM_SPELLTRIGGER_CHANCE_ON_HIT:
// For procchance and ICD we always use the original spell id
spTop := dbcInstance.Spells[e.SpellID]
effect := &proto.ProcEffect{
ProcChance: float64(spTop.ProcChance) / 100,
Icd: int32(spTop.ProcCategoryRecovery / 1000),
Rppm: spTop.Rppm,
RppmScale: int32(realPpmScale(spTop)),
}
// On procs we want the lower name though
pe.Label = sp.NameLang
ilvlMod, specMods := realPpmModifier(spTop, itemLevel)
effect.SpecModifiers = specMods

pe.Type = proto.ItemEffectType_PROC
pe.Effect = &proto.ItemEffect_Proc{
Proc: effect,
}
return statsSpellID, ilvlMod
default:
// leave as NONE
}

return statsSpellID, 0
}

func realPpmScale(spell Spell) int {
scale := 0
for _, mod := range spell.RppmModifiers {
switch mod.ModifierType {
case RPPMModifierHaste:
scale |= core.RPPM_HASTE
case RPPMModifierCrit:
scale |= core.RPPM_CRIT
}
}
return scale
}

func realPpmModifier(spell Spell, itemLevel int) (float64, map[int32]float64) {
specModifier := make(map[int32]float64)
ilvlModifier := 1.0
for _, mod := range spell.RppmModifiers {
switch mod.ModifierType {
case RPPMModifierSpec:
spec := SpecFromID(mod.Param)
specModifier[int32(spec)] = 1.0 * (1.0 + mod.Coeff)

case RPPMModifierIlevel:
basePoints := dbcInstance.RandomPropertiesByIlvl[int(mod.Param)][proto.ItemQuality_ItemQualityRare][0]
ilvlPoints := dbcInstance.RandomPropertiesByIlvl[itemLevel][proto.ItemQuality_ItemQualityRare][0]
if basePoints != ilvlPoints {
ilvlModifier *= 1.0 + ((float64(ilvlPoints)/float64(basePoints))-1.0)*mod.Coeff
}
}
}
return ilvlModifier, specModifier
}

func resolveTrigger(topType, spellID int) (triggerType, statsSpellID int) {
if topType == ITEM_SPELLTRIGGER_ON_USE || topType == ITEM_SPELLTRIGGER_CHANCE_ON_HIT {
return topType, spellID
}
for _, se := range dbcInstance.SpellEffects[spellID] {
if se.EffectAura == A_PROC_TRIGGER_SPELL {
// stats come from the triggered spell
return resolveTrigger(ITEM_SPELLTRIGGER_CHANCE_ON_HIT, se.EffectTriggerSpell)
}
}
return topType, spellID
}

func buildScalingProps(spellID, itemLevel int) *proto.ScalingItemEffectProperties {
total := collectStats(spellID, itemLevel)
src := total.ToProtoMap()

m := make(map[int32]float64, len(src))
maps.Copy(m, src)

return &proto.ScalingItemEffectProperties{Stats: m}
}

func collectStats(spellID, itemLevel int) stats.Stats {
var total stats.Stats
visited := make(map[int]bool)

var recurse func(int)
recurse = func(id int) {
if visited[id] {
return
}
visited[id] = true

sp := dbcInstance.Spells[id]
for _, se := range dbcInstance.SpellEffects[id] {
if s := se.ParseStatEffect(sp.HasAttributeAt(11, 0x4), itemLevel); s != &emptyStats {
total.AddInplace(s)
} else if se.EffectAura == A_PROC_TRIGGER_SPELL {
recurse(se.EffectTriggerSpell)
}
}
}

recurse(spellID)
return total
}

func ParseItemEffects(itemID, itemLevel int, levelState proto.ItemLevelState) []*proto.ItemEffect {
raw := dbcInstance.ItemEffectsByParentID[itemID]
out := make([]*proto.ItemEffect, 0, len(raw))
for _, ie := range raw {
out = append(out, ie.ToProto(itemLevel, levelState))
}
return out
}

func MergeItemEffectsForAllStates(parsed *proto.UIItem) []*proto.ItemEffect {
itemID := int(parsed.Id)
raws := dbcInstance.ItemEffectsByParentID[itemID]
var merged []*proto.ItemEffect

for idx := range raws {
var base *proto.ItemEffect

for key, props := range parsed.ScalingOptions {
state := proto.ItemLevelState(key)
ilvl := int(props.Ilvl)
slice := ParseItemEffects(itemID, ilvl, state)
eff := slice[idx]

if base == nil {
base = eff
} else {
base.ScalingOptions[key] = eff.ScalingOptions[key]
}
}
if base == nil {
continue
}

if len(base.ScalingOptions) > 0 {
merged = append(merged, base)
}
}
return merged
}
Loading
Loading