Compare commits

...

10 commits

Author SHA1 Message Date
8b9f29fc62 chore: fix Interface version 30301 -> 30300 2026-06-10 02:16:14 +02:00
4cd6d6a612 ci(release): sync release.yml from coa-template
hide_archive_links is only honored by Gitea on release edit, not create —
add the PATCH step after create/lookup so auto-generated source archive
links actually stay hidden (coa-template 90874c5).
2026-06-10 02:11:46 +02:00
694f6262ee feat(class): add CoA custom-class stat-summary defaults via CoAClassPrimaryStats
All checks were successful
release / release (push) Successful in 2s
Copy CoAClassSpecData.lua into the addon folder and load it before
RatingBuster.lua in the .toc.  After the vanilla if/elseif ladder in
applyClassProfileDefaults(), a new CoA branch checks
CoAClassPrimaryStats[cls]: if the player's token is a CoA custom class,
enable the stat-summary keys that match the class's union of primary
stats (Agility→physical melee block, Strength→physical melee block,
Intellect→spell block, Spirit→healing/mp5 block, Stamina→health/sta).
Vanilla classes and unrelated settings are not touched.
2026-05-30 01:27:49 +02:00
e7b3d4fb93 ci(release): hide auto-generated source archives (hide_archive_links) 2026-05-29 20:51:15 +02:00
a969cf5fbd fix(libs): pcall AceGUI OnGamePadButtonDown (3.3.5 has no gamepad script type)
All checks were successful
release / release (push) Successful in 2s
2026-05-29 20:23:33 +02:00
1887013749 fix(class): resolve player class at login and defer class-specific defaults/options (was hardcoded DRUID) 2026-05-29 10:43:54 +02:00
3b8d9b36b7 ci: respect GITHUB_REPOSITORY + tolerate per-asset upload failures
All checks were successful
release / release (push) Successful in 2s
2026-05-25 12:16:51 +02:00
3f478be582 ci: add Gitea Actions release workflow (per-addon git-archive zip) 2026-05-25 12:01:38 +02:00
47c02e09c7 chore: remove .github/ (upstream templates, not relevant on Gitea) 2026-05-25 11:02:51 +02:00
7a2d38d6e0 fix(libs): pick up coa-ace3 9583952 (AceDB falsy-defaults PR #10 backport)
Re-sync after coa-ace3 9583952 backported WoWUIDev/Ace3 PR #10 which fixes
the AceDB-3.0 simple-value defaults metatable: previously falsy defaults
like ["*"] = false read back as nil because of `k2~=nil and v or nil`
short-circuiting. Now they round-trip correctly.
2026-05-24 19:31:45 +02:00
12 changed files with 414 additions and 150 deletions

View file

@ -0,0 +1,75 @@
name: release
on:
push:
tags:
- '*-coa.*' # Asc-1.1.6-coa.2, 9.1.40-coa.3, etc.
- 'v*' # v0.3.0 for repos without an upstream version
jobs:
release:
runs-on: linux-amd64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # build_zip uses git archive HEAD; full history is fine
- name: Build per-addon zip(s)
run: bash tools/build_zip.sh
- name: Publish release (Gitea API direct; no action dependency)
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
API: ${{ github.server_url }}/api/v1
# Gitea attachment ceiling is 200 MiB (see roles/gitea config).
# Skip anything larger so one oversized asset doesn't fail the job.
MAX_BYTES: 209715200
run: |
set -euo pipefail
# Create the release (or reuse if it already exists for this tag).
RID=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
"$API/repos/$REPO/releases/tags/$TAG" 2>/dev/null \
| jq -r '.id // empty')
if [ -z "$RID" ]; then
RID=$(curl -sf -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
"$API/repos/$REPO/releases" \
-d "$(jq -nc --arg t "$TAG" '{tag_name:$t,name:$t,draft:false,prerelease:false,hide_archive_links:true}')" \
| jq -r '.id')
fi
echo "release id: $RID"
# Gitea honors hide_archive_links only on edit, not create — PATCH it
# so the auto-generated Source Code (zip/tar.gz) links stay hidden.
curl -sf -X PATCH -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
"$API/repos/$REPO/releases/$RID" -d '{"hide_archive_links":true}' >/dev/null || true
# Upload every dist/*.zip. Per-asset failures don't fail the job —
# we want partial releases to still publish rather than block the
# whole pipeline on one big file.
failed=0
uploaded=0
for f in dist/*.zip; do
name=$(basename "$f")
size=$(stat -c '%s' "$f")
if [ "$size" -gt "$MAX_BYTES" ]; then
echo "::warning::skip $name (${size} B > ${MAX_BYTES} B Gitea limit; host on CDN instead)"
failed=$((failed+1))
continue
fi
echo "uploading $name ($(numfmt --to=iec "$size"))"
if curl -sf -X POST -H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@$f" \
"$API/repos/$REPO/releases/$RID/assets?name=$name" \
| jq -r '" -> " + .browser_download_url'; then
uploaded=$((uploaded+1))
else
echo "::warning::upload failed for $name"
failed=$((failed+1))
fi
done
echo "release published: $uploaded uploaded, $failed skipped/failed"
# Only fail the job if NO assets uploaded — a release with zero
# attachments isn't useful to anyone.
[ "$uploaded" -gt 0 ]

View file

@ -1,77 +0,0 @@
name: "Bug Report"
description: Create a report to help us improve this addon
labels: '🐛 Bug'
body:
- type: markdown
attributes:
value: |
Please search for existing issues before creating a new one.
- type: textarea
attributes:
label: Description
description: What did you expect to happen and what happened instead?
validations:
required: true
- type: dropdown
id: flavor
attributes:
label: Realm
description: What realm did this occur on?
options:
- Area 52 (Default)
- Seasonal
- Grizzly Hills
- Rexxar
- Other
validations:
required: true
- type: checkboxes
id: testing
attributes:
label: Tested with only this addon
description: Did you try having just this addon as the only enabled addon and everything else disabled?
options:
- label: "Yes"
- label: "No"
validations:
required: true
- type: textarea
attributes:
label: Lua Error
description: |
Do you have an error log of what happened? If you don't see any errors, make sure that error reporting is enabled (`/console scriptErrors 1`)
validations:
required: false
- type: textarea
attributes:
label: Reproduction Steps
description: Please list out the steps to reproduce your bug.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: input
attributes:
label: Last Good Version
description: |
Was it working in a previous version? If yes, which update did it stop working? If you don't know, when was the last date you were aware it was working
placeholder: "MM/DD/YYYY"
validations:
required: false
- type: textarea
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
placeholder: Click here to attach your screenshots via the editor button in the top right.
validations:
required: false

View file

@ -1 +0,0 @@
blank_issues_enabled: false

View file

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -1,28 +0,0 @@
# Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context.
<!-- A #issueNumber will be sufficient. -->
Fixes #(issue)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
## How Has This Been Tested
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
- [ ] Test A
- [ ] Test B
## Checklist
<!-- These can be checked off after the pull request is submitted, in case you want discussion before they are completely ready -->
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
<!-- Is there any additional work that needs to be done? If so, add it to the above list -->

3
.gitignore vendored
View file

@ -4,4 +4,5 @@
.install
.lua/*
.vscode
.idea
.idea
dist/

View file

@ -0,0 +1,143 @@
-- CoAClassSpecData.lua — GENERATED by coa-db/tools/gen_coa_class_spec_lua.py
-- Source of truth: coa-db/data/class_spec_meta.json (class.file_string tokens + wiki specs).
-- Do not hand-edit; regenerate from coa-db. Neutral stat keys; each addon maps them.
-- Keyed by the in-game class token (2nd return of UnitClass), e.g. Templar=MONK.
CoAClassSpec = {
["BARBARIAN"] = { name="Barbarian", classId=12, specs={
{ name="Headhunting", roles={"RANGED"}, primaryStat="Agility", weights={ Agility=1, RangedAttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5 } },
{ name="Brutality", roles={"MELEE"}, primaryStat="Agility", weights={ Agility=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
{ name="Ancestry", roles={"MELEE","SUPPORT"}, primaryStat="Agility", weights={ Agility=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
}},
["WITCHDOCTOR"] = { name="Witch Doctor", classId=13, specs={
{ name="Shadowhunting", roles={"RANGED"}, primaryStat="Agility", weights={ Agility=1, RangedAttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5 } },
{ name="Voodoo", roles={"CASTER"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
{ name="Brewing", roles={"HEALER"}, primaryStat="Spirit", weights={ Spirit=1, SpellPower=0.8, CritRating=0.5, HasteRating=0.5, Mp5=0.5, Intellect=0.55 } },
}},
["DEMONHUNTER"] = { name="Felsworn", classId=14, specs={
{ name="Slayer", roles={"MELEE"}, primaryStat="Agility", weights={ Agility=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
{ name="Infernal", roles={"CASTER"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
{ name="Tyrant", roles={"TANK"}, primaryStat="Agility", weights={ Stamina=1, Armor=0.6, Dodge=0.55, Parry=0.55, Defense=0.6, Agility=0.4 } },
}},
["WITCHHUNTER"] = { name="Witch Hunter", classId=15, specs={
{ name="Boltslinger", roles={"RANGED"}, primaryStat="Agility", weights={ Agility=1, RangedAttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5 } },
{ name="Darkness", roles={"RANGED"}, primaryStat="Agility", weights={ Agility=1, RangedAttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5 } },
{ name="Inquisition", roles={"MELEE"}, primaryStat="Agility", weights={ Agility=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
{ name="Black Knight", roles={"TANK"}, primaryStat="Agility", weights={ Stamina=1, Armor=0.6, Dodge=0.55, Parry=0.55, Defense=0.6, Agility=0.4 } },
}},
["STORMBRINGER"] = { name="Stormbringer", classId=16, specs={
{ name="Maelstrom", roles={"CASTER"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
{ name="Lightning", roles={"CASTER"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
{ name="Wind", roles={"CASTER","SUPPORT"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
}},
["FLESHWARDEN"] = { name="Knight of Xoroth", classId=17, specs={
{ name="Hellfire", roles={"MELEE"}, primaryStat="Strength", weights={ Strength=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
{ name="Defiance", roles={"TANK"}, primaryStat="Strength", weights={ Stamina=1, Armor=0.6, Dodge=0.55, Parry=0.55, Defense=0.6, Strength=0.4 } },
{ name="War", roles={"MELEE"}, primaryStat="Strength", weights={ Strength=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
}},
["GUARDIAN"] = { name="Guardian", classId=18, specs={
{ name="Gladiator", roles={"MELEE"}, primaryStat="Strength", weights={ Strength=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
{ name="Inspiration", roles={"MELEE","SUPPORT"}, primaryStat="Strength", weights={ Strength=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
{ name="Vanguard", roles={"TANK"}, primaryStat="Strength", weights={ Stamina=1, Armor=0.6, Dodge=0.55, Parry=0.55, Defense=0.6, Strength=0.4 } },
}},
["MONK"] = { name="Templar", classId=19, specs={
{ name="Oathkeeper", roles={"TANK"}, primaryStat="Agility", weights={ Stamina=1, Armor=0.6, Dodge=0.55, Parry=0.55, Defense=0.6, Agility=0.4 } },
{ name="Zealot", roles={"MELEE"}, primaryStat="Agility", weights={ Agility=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
{ name="Crusader", roles={"MELEE"}, primaryStat="Agility", weights={ Agility=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
}},
["SONOFARUGAL"] = { name="Bloodmage", classId=20, specs={
{ name="Fleshweaver", roles={"SUPPORT"}, primaryStat="Spirit", weights={ Spirit=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
{ name="Sanguine", roles={"CASTER"}, primaryStat="Spirit", weights={ Spirit=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
{ name="Accursed", roles={"MELEE","CASTER"}, primaryStat="Agility", weights={ Agility=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
{ name="Eternal", roles={"TANK"}, primaryStat="Agility", weights={ Stamina=1, Armor=0.6, Dodge=0.55, Parry=0.55, Defense=0.6, Agility=0.4 } },
}},
["RANGER"] = { name="Ranger", classId=21, specs={
{ name="Archery", roles={"RANGED"}, primaryStat="Agility", weights={ Agility=1, RangedAttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5 } },
{ name="Farstrider", roles={"RANGED","SUPPORT"}, primaryStat="Agility", weights={ Agility=1, RangedAttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5 } },
{ name="Brigand", roles={"MELEE"}, primaryStat="Agility", weights={ Agility=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
}},
["CHRONOMANCER"] = { name="Chronomancer", classId=22, specs={
{ name="Infinite", roles={"CASTER"}, primaryStat="Spirit", weights={ Spirit=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
{ name="Time", roles={"HEALER"}, primaryStat="Spirit", weights={ Spirit=1, SpellPower=0.8, CritRating=0.5, HasteRating=0.5, Mp5=0.5, Intellect=0.55 } },
{ name="Artificer", roles={"RANGED"}, primaryStat="Spirit", weights={ Spirit=1, RangedAttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5 } },
}},
["NECROMANCER"] = { name="Necromancer", classId=23, specs={
{ name="Death", roles={"CASTER"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
{ name="Animation", roles={"CASTER"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
{ name="Rime", roles={"CASTER"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
}},
["PYROMANCER"] = { name="Pyromancer", classId=24, specs={
{ name="Incineration", roles={"CASTER"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
{ name="Flameweaving", roles={"HEALER"}, primaryStat="Spirit", weights={ Spirit=1, SpellPower=0.8, CritRating=0.5, HasteRating=0.5, Mp5=0.5, Intellect=0.55 } },
{ name="Draconic", roles={"CASTER"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
}},
["CULTIST"] = { name="Cultist", classId=25, specs={
{ name="Heretic", roles={"HEALER"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, CritRating=0.5, HasteRating=0.5, Mp5=0.5, Spirit=0.55 } },
{ name="Corruption", roles={"CASTER"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
{ name="Godblade", roles={"MELEE"}, primaryStat="Strength", weights={ Strength=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
{ name="Dreadnought", roles={"TANK"}, primaryStat="Strength", weights={ Stamina=1, Armor=0.6, Dodge=0.55, Parry=0.55, Defense=0.6, Strength=0.4 } },
}},
["STARCALLER"] = { name="Starcaller", classId=26, specs={
{ name="Sentinel", roles={"RANGED"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
{ name="Warden", roles={"MELEE"}, primaryStat="Intellect", weights={ Intellect=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
{ name="Moon Priest", roles={"HEALER"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, CritRating=0.5, HasteRating=0.5, Mp5=0.5, Spirit=0.55 } },
{ name="Moon Guard", roles={"TANK"}, primaryStat="Intellect", weights={ Stamina=1, Armor=0.6, Dodge=0.55, Parry=0.55, Defense=0.6, Intellect=0.4 } },
}},
["SUNCLERIC"] = { name="Sun Cleric", classId=27, specs={
{ name="Piety", roles={"CASTER"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
{ name="Valkyrie", roles={"MELEE"}, primaryStat="Strength", weights={ Strength=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
{ name="Seraphim", roles={"TANK"}, primaryStat="Strength", weights={ Stamina=1, Armor=0.6, Dodge=0.55, Parry=0.55, Defense=0.6, Strength=0.4 } },
{ name="Blessings", roles={"HEALER"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, CritRating=0.5, HasteRating=0.5, Mp5=0.5, Spirit=0.55 } },
}},
["TINKER"] = { name="Tinker", classId=28, specs={
{ name="Demolition", roles={"RANGED"}, primaryStat="Agility", weights={ Agility=1, RangedAttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5 } },
{ name="Mechanics", roles={"MELEE"}, primaryStat="Agility", weights={ Agility=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
{ name="Invention", roles={"HEALER"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, CritRating=0.5, HasteRating=0.5, Mp5=0.5, Spirit=0.55 } },
}},
["PROPHET"] = { name="Venomancer", classId=29, specs={
{ name="Fortitude", roles={"TANK"}, primaryStat="Intellect", weights={ Stamina=1, Armor=0.6, Dodge=0.55, Parry=0.55, Defense=0.6, Intellect=0.4 } },
{ name="Stalking", roles={"MELEE"}, primaryStat="Intellect", weights={ Intellect=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
{ name="Rot", roles={"CASTER"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
{ name="Vizier", roles={"HEALER"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, CritRating=0.5, HasteRating=0.5, Mp5=0.5, Spirit=0.55 } },
}},
["REAPER"] = { name="Reaper", classId=30, specs={
{ name="Soul", roles={"MELEE"}, primaryStat="Strength", weights={ Strength=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
{ name="Harvest", roles={"MELEE"}, primaryStat="Strength", weights={ Strength=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
{ name="Domination", roles={"TANK"}, primaryStat="Strength", weights={ Stamina=1, Armor=0.6, Dodge=0.55, Parry=0.55, Defense=0.6, Strength=0.4 } },
}},
["WILDWALKER"] = { name="Primalist", classId=31, specs={
{ name="Grovekeeper", roles={"SUPPORT"}, primaryStat="Strength", weights={ Strength=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
{ name="Wildwalker", roles={"MELEE"}, primaryStat="Strength", weights={ Strength=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
{ name="Mountain King", roles={"TANK"}, primaryStat="Strength", weights={ Stamina=1, Armor=0.6, Dodge=0.55, Parry=0.55, Defense=0.6, Strength=0.4 } },
{ name="Geomancy", roles={"CASTER"}, primaryStat="Intellect", weights={ Intellect=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
}},
["SPIRITMAGE"] = { name="Runemaster", classId=32, specs={
{ name="Engravement", roles={"MELEE"}, primaryStat="Agility", weights={ Agility=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
{ name="Glyphic", roles={"CASTER"}, primaryStat="Spirit", weights={ Spirit=1, SpellPower=0.8, HitRating=0.7, CritRating=0.55, HasteRating=0.55 } },
{ name="Riftblade", roles={"MELEE"}, primaryStat="Agility", weights={ Agility=1, AttackPower=0.5, CritRating=0.55, HitRating=0.6, HasteRating=0.5, ArmorPenetration=0.5 } },
}},
}
CoAClassPrimaryStats = {
["BARBARIAN"] = { "Agility" },
["WITCHDOCTOR"] = { "Agility", "Intellect", "Spirit" },
["DEMONHUNTER"] = { "Agility", "Intellect", "Stamina" },
["WITCHHUNTER"] = { "Agility", "Intellect", "Stamina" },
["STORMBRINGER"] = { "Intellect" },
["FLESHWARDEN"] = { "Strength", "Intellect", "Stamina" },
["GUARDIAN"] = { "Strength", "Stamina" },
["MONK"] = { "Agility", "Stamina" },
["SONOFARUGAL"] = { "Spirit", "Stamina", "Agility" },
["RANGER"] = { "Agility" },
["CHRONOMANCER"] = { "Spirit" },
["NECROMANCER"] = { "Intellect" },
["PYROMANCER"] = { "Intellect", "Spirit" },
["CULTIST"] = { "Intellect", "Strength", "Stamina" },
["STARCALLER"] = { "Intellect", "Stamina" },
["SUNCLERIC"] = { "Intellect", "Strength", "Stamina" },
["TINKER"] = { "Agility", "Intellect" },
["PROPHET"] = { "Intellect", "Stamina" },
["REAPER"] = { "Strength", "Stamina" },
["WILDWALKER"] = { "Strength", "Intellect", "Stamina" },
["SPIRITMAGE"] = { "Agility", "Spirit" },
}

View file

@ -352,7 +352,11 @@ Spi -> MP5, MP5NC, HP5, SpellDmg, Healing
}
-- Class specific defaults
if class == "DRUID" then
-- Applied at OnInitialize after UnitClass("player") is available.
-- CoA custom classes (not in the vanilla if/elseif ladder) fall through
-- to no block, which is the correct generic fallback for classless chars.
local function applyClassProfileDefaults(cls)
if cls == "DRUID" then
profileDefault.ratingSpell = true
profileDefault.ratingPhysical = true
profileDefault.sumHP = true
@ -381,7 +385,7 @@ if class == "DRUID" then
profileDefault.showHealingFromSpi = true
profileDefault.sumIgnoreCloth = false
profileDefault.sumIgnoreLeather = false
elseif class == "HUNTER" then
elseif cls == "HUNTER" then
profileDefault.ratingPhysical = true
profileDefault.sumHP = true
profileDefault.sumMP = true
@ -399,7 +403,7 @@ elseif class == "HUNTER" then
profileDefault.showAPFromSta = true
profileDefault.sumIgnoreLeather = false
profileDefault.sumIgnoreMail = false
elseif class == "MAGE" then
elseif cls == "MAGE" then
profileDefault.ratingSpell = true
profileDefault.sumHP = true
profileDefault.sumMP = true
@ -417,7 +421,7 @@ elseif class == "MAGE" then
profileDefault.showMP5FromSpi = true
profileDefault.showSpellCritFromSpi = true -- Molten Armor
profileDefault.sumIgnoreCloth = false
elseif class == "PALADIN" then
elseif cls == "PALADIN" then
profileDefault.ratingSpell = true
profileDefault.ratingPhysical = true
profileDefault.sumHP = true
@ -448,7 +452,7 @@ elseif class == "PALADIN" then
profileDefault.sumIgnoreLeather = false
profileDefault.sumIgnoreMail = false
profileDefault.sumIgnorePlate = false
elseif class == "PRIEST" then
elseif cls == "PRIEST" then
profileDefault.ratingSpell = true
profileDefault.sumHP = true
profileDefault.sumMP = true
@ -466,7 +470,7 @@ elseif class == "PRIEST" then
profileDefault.showSpellDmgFromSpi = true
profileDefault.showHealingFromSpi = true
profileDefault.sumIgnoreCloth = false
elseif class == "ROGUE" then
elseif cls == "ROGUE" then
profileDefault.ratingPhysical = true
profileDefault.sumHP = true
profileDefault.sumResilience = true
@ -478,7 +482,7 @@ elseif class == "ROGUE" then
profileDefault.sumArmorPenetration = true
profileDefault.showSpellCritFromInt = false
profileDefault.sumIgnoreLeather = false
elseif class == "SHAMAN" then
elseif cls == "SHAMAN" then
profileDefault.ratingSpell = true
profileDefault.ratingPhysical = true
profileDefault.sumHP = true
@ -499,7 +503,7 @@ elseif class == "SHAMAN" then
profileDefault.sumIgnoreCloth = false
profileDefault.sumIgnoreLeather = false
profileDefault.sumIgnoreMail = false
elseif class == "WARLOCK" then
elseif cls == "WARLOCK" then
profileDefault.ratingSpell = true
profileDefault.sumHP = true
profileDefault.sumMP = true
@ -515,7 +519,7 @@ elseif class == "WARLOCK" then
profileDefault.showMP5FromSpi = true
profileDefault.showSpellDmgFromSpi = true
profileDefault.sumIgnoreCloth = false
elseif class == "WARRIOR" then
elseif cls == "WARRIOR" then
profileDefault.ratingPhysical = true
profileDefault.sumHP = true
profileDefault.sumResilience = true
@ -536,7 +540,7 @@ elseif class == "WARRIOR" then
profileDefault.sumIgnoreMail = false
end
profileDefault.sumIgnorePlate = false
elseif class == "DEATHKNIGHT" then
elseif cls == "DEATHKNIGHT" then
profileDefault.ratingPhysical = true
profileDefault.sumHP = true
profileDefault.sumResilience = true
@ -555,6 +559,69 @@ elseif class == "DEATHKNIGHT" then
profileDefault.sumIgnorePlate = false
end
-- CoA custom classes: enable stat-summary defaults based on primary stats.
-- CoAClassPrimaryStats is defined in CoAClassSpecData.lua (loaded before this file).
if CoAClassPrimaryStats and CoAClassPrimaryStats[cls] then
local stats = CoAClassPrimaryStats[cls]
local hasAgi, hasStr, hasInt, hasSpi, hasSta = false, false, false, false, false
for _, stat in ipairs(stats) do
if stat == "Agility" then hasAgi = true end
if stat == "Strength" then hasStr = true end
if stat == "Intellect" then hasInt = true end
if stat == "Spirit" then hasSpi = true end
if stat == "Stamina" then hasSta = true end
end
-- Always: HP and resilience
profileDefault.sumHP = true
profileDefault.sumResilience = true
if hasAgi then
profileDefault.ratingPhysical = true
profileDefault.sumAgi = true
profileDefault.sumAP = true
profileDefault.sumHit = true
profileDefault.sumCrit = true
profileDefault.sumHaste = true
profileDefault.sumExpertise = true
profileDefault.sumArmorPenetration = true
end
if hasStr then
profileDefault.ratingPhysical = true
profileDefault.sumStr = true
profileDefault.sumAP = true
profileDefault.sumHit = true
profileDefault.sumCrit = true
profileDefault.sumHaste = true
profileDefault.sumExpertise = true
profileDefault.sumArmorPenetration = true
end
if hasInt then
profileDefault.ratingSpell = true
profileDefault.sumMP = true
profileDefault.sumInt = true
profileDefault.sumSpellDmg = true
profileDefault.sumSpellHit = true
profileDefault.sumSpellCrit = true
profileDefault.sumSpellHaste = true
profileDefault.sumHealing = true
profileDefault.sumMP5 = true
profileDefault.showSpellDmgFromInt = true
profileDefault.showMP5FromInt = true
end
if hasSpi then
profileDefault.ratingSpell = true
profileDefault.sumMP = true
profileDefault.sumSpi = true
profileDefault.sumHealing = true
profileDefault.sumMP5 = true
profileDefault.showMP5FromSpi = true
profileDefault.showHealingFromSpi = true
end
if hasSta then
profileDefault.sumSta = true
end
end
end -- applyClassProfileDefaults
local defaults = {}
defaults.profile = profileDefault
@ -2252,7 +2319,10 @@ end
-- Class specific options
if class == "DRUID" then
-- Applied at OnInitialize (before SetupOptions) after UnitClass("player") is available.
-- CoA custom classes fall through to no block: no class-specific UI entries shown.
local function applyClassOptions(cls)
if cls == "DRUID" then
options.args.stat.args.agi.args.heal = { -- Nurturing Instinct (Rank 2) - 2,14
type = 'toggle',
width = "full",
@ -2307,7 +2377,7 @@ if class == "DRUID" then
get = getProfileOption,
set = setProfileOptionAndClearCache,
}
elseif class == "HUNTER" then
elseif cls == "HUNTER" then
options.args.stat.args.int.args.rap = { -- Careful Aim
type = 'toggle',
width = "full",
@ -2326,7 +2396,7 @@ elseif class == "HUNTER" then
get = getProfileOption,
set = setProfileOptionAndClearCache,
}
elseif class == "MAGE" then
elseif cls == "MAGE" then
options.args.stat.args.int.args.dmg = { -- Mind Mastery (Rank 5) - 1,22
type = 'toggle',
width = "full",
@ -2363,7 +2433,7 @@ elseif class == "MAGE" then
get = getProfileOption,
set = setProfileOptionAndClearCache,
}
elseif class == "PALADIN" then
elseif cls == "PALADIN" then
options.args.stat.args.int.args.dmg = { -- Paladin: Holy Guidance (Rank 5) - 1,19
type = 'toggle',
width = "full",
@ -2400,7 +2470,7 @@ elseif class == "PALADIN" then
get = getProfileOption,
set = setProfileOptionAndClearCache,
}
elseif class == "PRIEST" then
elseif cls == "PRIEST" then
options.args.stat.args.spi.args.mp5 = { -- Meditation (Rank 3) - 1,9
type = 'toggle',
width = "full",
@ -2428,8 +2498,8 @@ elseif class == "PRIEST" then
get = getProfileOption,
set = setProfileOptionAndClearCache,
}
elseif class == "ROGUE" then
elseif class == "SHAMAN" then
elseif cls == "ROGUE" then
elseif cls == "SHAMAN" then
options.args.stat.args.str.args.dmg = { -- Mental Quickness (Rank 3) - 2,15
type = 'toggle',
width = "full",
@ -2475,7 +2545,7 @@ elseif class == "SHAMAN" then
get = getProfileOption,
set = setProfileOptionAndClearCache,
}
elseif class == "WARLOCK" then
elseif cls == "WARLOCK" then
options.args.stat.args.sta.args.dmg = { -- Demonic Knowledge (Rank 3) - 2,20 - UnitExists("pet")
type = 'toggle',
width = "full",
@ -2512,7 +2582,7 @@ elseif class == "WARLOCK" then
get = getProfileOption,
set = setProfileOptionAndClearCache,
}
elseif class == "WARRIOR" then
elseif cls == "WARRIOR" then
options.args.stat.args.armor = { -- Armored to the Teeth (Rank 3) - 2,1
type = 'group',
order = 7,
@ -2530,7 +2600,7 @@ elseif class == "WARRIOR" then
},
},
}
elseif class == "DEATHKNIGHT" then
elseif cls == "DEATHKNIGHT" then
options.args.stat.args.str.args.parry = { -- Death Knight: Forceful Deflection - Passive
type = 'toggle',
width = "full",
@ -2558,6 +2628,7 @@ elseif class == "DEATHKNIGHT" then
},
}
end
end -- applyClassOptions
function RatingBuster:SetupOptions()
-- Inject profile options
@ -2660,6 +2731,16 @@ end
-- OnInitialize(name) called at ADDON_LOADED
function RatingBuster:OnInitialize()
-- Resolve player class as early as possible (ADDON_LOADED).
-- UnitClass("player") is nil at file-load time on this client (CoA 3.3.5a)
-- but is populated by ADDON_LOADED. Apply class-specific profileDefault
-- mutations BEFORE AceDB:New() so that first-install defaults are correct,
-- and apply class-specific options mutations BEFORE SetupOptions/RegisterOptionsTable.
local _, resolvedClass = UnitClass("player")
if resolvedClass then
class = resolvedClass
end
applyClassProfileDefaults(class)
-- Create DB
self.db = AceDB:New("RatingBusterDB", defaults)
self.db.RegisterCallback(self, "OnProfileChanged", "OnProfileChanged")
@ -2668,6 +2749,7 @@ function RatingBuster:OnInitialize()
profileDB = self.db.profile
applyClassOptions(class)
self:SetupOptions()
-- Hook ShoppingTooltips to enable options to Hide Blizzard Item Comparisons
@ -2680,6 +2762,13 @@ end
function RatingBuster:OnEnable()
-- Hook item tooltips
TipHooker:Hook(self.ProcessTooltip, "item")
-- Ensure class is resolved at PLAYER_LOGIN in case ADDON_LOADED fired too early.
-- This guards the runtime stat-conversion paths (class is used throughout
-- ProcessTooltip and StatLogic calls).
local _, resolvedClass = UnitClass("player")
if resolvedClass then
class = resolvedClass
end
-- Initialize playerLevel
playerLevel = UnitLevel("player")
-- for setting a new level

View file

@ -1,4 +1,4 @@
## Interface: 30301
## Interface: 30300
## Title: RatingBuster
## Notes: Item stat breakdown, analysis and comparison
## Notes-zhTW: 裝備數值解析與比較
@ -36,5 +36,8 @@ RatingBuster-Locale-frFR.lua
RatingBuster-Locale-koKR.lua
RatingBuster-Locale-esES.lua
# CoA class/spec data (must load before RatingBuster.lua) #
CoAClassSpecData.lua
# Core #
RatingBuster.lua

View file

@ -111,7 +111,15 @@ local function copyDefaults(dest, src)
end
else
-- Values are not tables, so this is just a simple return
local mt = {__index = function(t,k2) return k2~=nil and v or nil end}
-- (PR #10 backport: the old `k2~=nil and v or nil` short-circuits to
-- nil whenever the default `v` itself is falsy — so `["*"] = false`
-- defaults silently became nil. Make the read explicit instead.)
local mt = {
__index = function(t,k2)
if k2 == nil then return nil end
return v
end,
}
setmetatable(dest, mt)
end
elseif type(v) == "table" then

View file

@ -199,7 +199,7 @@ local function Constructor()
button:SetScript("OnKeyDown", Keybinding_OnKeyDown)
button:SetScript("OnMouseDown", Keybinding_OnMouseDown)
button:SetScript("OnMouseWheel", Keybinding_OnMouseWheel)
button:SetScript("OnGamePadButtonDown", Keybinding_OnKeyDown)
pcall(button.SetScript, button, "OnGamePadButtonDown", Keybinding_OnKeyDown)
button:SetPoint("BOTTOMLEFT")
button:SetPoint("BOTTOMRIGHT")
button:SetHeight(24)

71
tools/build_zip.sh Executable file
View file

@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Build per-addon zip artefacts from HEAD via git-archive.
#
# - Discovers top-level addon folders (Foo/Foo.toc).
# - Re-creates dist/ each run.
# - Always archives HEAD, so the working tree state is irrelevant.
# - If more than one addon folder is present, also emits <repo>-all.zip
# with every addon folder side-by-side at the zip root.
# - When run inside Gitea Actions the working tree lives under a
# per-job dir like /var/lib/act_runner/work/.../hostexecutor, so the
# repo name comes from $GITHUB_REPOSITORY (set by the runner) and
# only falls back to the toplevel basename for local invocations.
set -euo pipefail
root=$(git rev-parse --show-toplevel)
cd "$root"
# Gitea Actions sets GITHUB_REPOSITORY=owner/repo. The basename of
# `git rev-parse --show-toplevel` inside the runner is the worker dir
# (e.g. `hostexecutor`), which would name the bundle wrong.
if [ -n "${GITHUB_REPOSITORY:-}" ]; then
repo_name="${GITHUB_REPOSITORY##*/}"
else
repo_name=$(basename "$root")
fi
dist="$root/dist"
# Find Foo/Foo.toc pairs at depth 1; ignore libs nested deeper.
addons=()
while IFS= read -r toc; do
dir=$(dirname "$toc")
folder=$(basename "$dir")
base=$(basename "$toc" .toc)
# Accept Foo.toc and Foo_Wrath.toc style flavour variants; folder must match
# at least one toc basename prefix (Foo).
case "$base" in
"$folder"|"$folder"_*) addons+=("$folder") ;;
esac
done < <(command find . -mindepth 2 -maxdepth 2 -type f -name '*.toc' | sed 's|^\./||' | sort)
# Dedupe (a folder with Foo.toc + Foo_Wrath.toc shows up twice).
if [ ${#addons[@]} -gt 0 ]; then
mapfile -t addons < <(printf '%s\n' "${addons[@]}" | awk '!seen[$0]++')
fi
if [ ${#addons[@]} -eq 0 ]; then
echo "no addon folders found (looking for */Foo.toc with matching folder name)" >&2
exit 1
fi
rm -rf "$dist"
mkdir -p "$dist"
for folder in "${addons[@]}"; do
out="$dist/$folder.zip"
# No --prefix: the folder already sits at the repo root, so git-archive
# emits entries as <folder>/... which is exactly what
# Interface/AddOns/ expects after extraction.
git archive HEAD --format=zip -o "$out" -- "$folder"
echo "built dist/$folder.zip"
done
# Combined bundle only makes sense when there are multiple addons.
if [ ${#addons[@]} -gt 1 ]; then
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
git archive HEAD --format=tar -- "${addons[@]}" | tar -x -C "$tmp"
out="$dist/$repo_name-all.zip"
( cd "$tmp" && zip -qr "$out" "${addons[@]}" )
echo "built dist/$repo_name-all.zip"
fi