Skip to content
Merged
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
23 changes: 22 additions & 1 deletion .github/workflows/spellcheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,30 @@ jobs:
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}

- name: Get current date for cache key
id: date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT

- name: Cache pob-dict files
id: dict-cache
uses: actions/cache@v4
with:
path: |
cspell.json
pob-dict.txt
poe-dict.txt
ignore-dict.txt
extra-en-dict.txt
contribs-dict.txt
# Cache key rotates daily so the dict stays reasonably fresh
# without fetching on every run.
key: pob-dict-${{ runner.os }}-${{ steps.date.outputs.date }}

- name: Fetch config file and dictionaries
if: steps.dict-cache.outputs.cache-hit != 'true'
run: |
curl --silent --show-error --fail --parallel --remote-name-all \
curl --silent --show-error --fail --retry 3 --retry-delay 5 --parallel \
--remote-name-all \
"${{ env.CONFIG_URL }}/cspell.json" \
"${{ env.CONFIG_URL }}/pob-dict.txt" \
"${{ env.CONFIG_URL }}/poe-dict.txt" \
Expand Down
72 changes: 62 additions & 10 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,70 @@ jobs:
- name: Run tests
run: busted --lua=luajit
check_modcache:
name: check ModCache is up-to-date
runs-on: ubuntu-latest
container: ghcr.io/pathofbuildingcommunity/pathofbuilding-tests:latest
permissions:
contents: write
steps:
- name: Install git dependency
run: apk add git
- name: Checkout
uses: actions/checkout@v4
with:
# Check out the PR head branch so we can push back to it.
# On push events github.head_ref is empty; fall back to the pushed ref.
ref: ${{ github.head_ref || github.ref }}
token: ${{ secrets.GITHUB_TOKEN }}

- name: Cache test image
id: image-cache
uses: actions/cache@v4
with:
path: /tmp/pob-test-image.tar
key: pob-test-image-${{ hashFiles('docker-compose.yml') }}
restore-keys: pob-test-image-

- name: Pull or restore image
run: |
if [[ "${{ steps.image-cache.outputs.cache-hit }}" == "true" ]]; then
echo "Restoring image from cache"
docker load < /tmp/pob-test-image.tar
else
echo "Cache miss — pulling from GHCR"
docker pull ghcr.io/pathofbuildingcommunity/pathofbuilding-tests:latest
docker save ghcr.io/pathofbuildingcommunity/pathofbuilding-tests:latest \
-o /tmp/pob-test-image.tar
fi

- name: Regenerate ModCache
env:
LUA_PATH: ../runtime/lua/?.lua;../runtime/lua/?/init.lua
REGENERATE_MOD_CACHE: 1
run: cd src; luajit HeadlessWrapper.lua
- run: git config --global --add safe.directory $(pwd)
- name: Check if the generated ModCache is different
run: git diff --exit-code src/Data/ModCache.lua
# Run on the host (not via container:) so git credentials are available
# for the auto-commit step. Mount the workspace read-write so the
# regenerated ModCache.lua lands in the workspace.
run: |
docker run --rm \
-e HOME=/tmp \
-e "LUA_PATH=../runtime/lua/?.lua;../runtime/lua/?/init.lua" \
-e REGENERATE_MOD_CACHE=1 \
-v "${{ github.workspace }}:/workdir" \
-w /workdir/src \
ghcr.io/pathofbuildingcommunity/pathofbuilding-tests:latest \
luajit HeadlessWrapper.lua

- name: Auto-commit ModCache if stale
run: |
if git diff --quiet src/Data/ModCache.lua; then
echo "ModCache.lua is up to date ✓"
exit 0
fi

echo "ModCache.lua was out of date — committing regenerated version"
git diff --stat src/Data/ModCache.lua

git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add src/Data/ModCache.lua
git commit -m "chore: auto-regenerate ModCache.lua"

# Push back to the PR branch (head_ref) or the pushed branch (ref_name).
TARGET="${{ github.head_ref || github.ref_name }}"
git push origin "HEAD:${TARGET}"

echo "::notice::ModCache.lua was regenerated and committed automatically."
28 changes: 27 additions & 1 deletion spec/System/TestItemParse_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -664,4 +664,30 @@ describe("TestAdvancedItemParse #item", function()
Note: ~b/o 2 chaos
]])
end)
end)

it("Has +N to Evasion Rating per player level parses to BASE Evasion with Level multiplier", function()
local item = new("Item", [[
Rarity: Rare
Striker's Grip
Fists of Stone
--------
Implicits: 3
Has +2 to Evasion Rating per player level (implicit)
Has +1 to maximum Energy Shield per player level (implicit)
Has +1 to maximum Runic Ward per player level (implicit)
]])
-- All three implicit mod lines should be parsed (no extra/unsupported flag)
assert.are.equals(3, #item.implicitModLines)
assert.is_nil(item.implicitModLines[1].extra)
assert.is_nil(item.implicitModLines[2].extra)
assert.is_nil(item.implicitModLines[3].extra)
-- Check raw mod values
assert.are.equals(2, item.baseModList[1].value)
assert.are.equals(1, item.baseModList[2].value)
assert.are.equals(1, item.baseModList[3].value)
-- Check mod names map to correct stats
assert.are.equals("Evasion", item.baseModList[1].name)
assert.are.equals("EnergyShield", item.baseModList[2].name)
assert.are.equals("Ward", item.baseModList[3].name)
end)
end)
41 changes: 41 additions & 0 deletions spec/System/TestModParser_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
describe("TestModParser", function()
-- Low-level round-trip tests for specialModList entries added for Way of the Stonefist.
-- These verify that modLib.parseMod correctly maps modifier strings to their flag mods
-- without going through the full build pipeline.

it("parses 'ignore attribute requirements to equip gloves' to IgnoreAttributeRequirementsForGloves", function()
local mods, extra = modLib.parseMod("Ignore attribute requirements to equip gloves")
assert.is_nil(extra)
assert.is_not_nil(mods)
assert.are.equals(1, #mods)
assert.are.equals("IgnoreAttributeRequirementsForGloves", mods[1].name)
assert.are.equals("FLAG", mods[1].type)
end)

it("parses 'gloves you equip have their base type transformed to fists of stone while equipped' to GloveBaseTypeTransform", function()
local mods, extra = modLib.parseMod("Gloves you equip have their base type transformed to fists of stone while equipped")
assert.is_nil(extra)
assert.is_not_nil(mods)
assert.are.equals(1, #mods)
assert.are.equals("GloveBaseTypeTransform", mods[1].name)
assert.are.equals("FLAG", mods[1].type)
end)

it("parses 'their explicit modifiers are transformed into more powerful related modifiers' to GloveExplicitModTransform", function()
local mods, extra = modLib.parseMod("Their explicit modifiers are transformed into more powerful related modifiers")
assert.is_nil(extra)
assert.is_not_nil(mods)
assert.are.equals(1, #mods)
assert.are.equals("GloveExplicitModTransform", mods[1].name)
assert.are.equals("FLAG", mods[1].type)
end)

it("does not parse 'ignore attribute requirements' (global) as the gloves-scoped flag", function()
local mods, extra = modLib.parseMod("Ignore attribute requirements")
assert.is_nil(extra)
assert.is_not_nil(mods)
assert.are.equals(1, #mods)
assert.are.equals("IgnoreAttributeRequirements", mods[1].name)
assert.are_not.equals("IgnoreAttributeRequirementsForGloves", mods[1].name)
end)
end)
170 changes: 170 additions & 0 deletions spec/System/TestStonefist_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
describe("TestStonefist", function()
before_each(function()
newBuild()
end)

-- ModParser: flag parsing

it("GloveBaseTypeTransform flag is set from ascendancy mod string", function()
build.configTab.input.customMods = "\z
Gloves you equip have their base type transformed to fists of stone while equipped\n\z
"
build.configTab:BuildModList()
runCallback("OnFrame")

assert.is_true(build.calcsTab.mainEnv.modDB:Flag(nil, "GloveBaseTypeTransform"))
end)

it("IgnoreAttributeRequirementsForGloves flag is set from ascendancy mod string", function()
build.configTab.input.customMods = "\z
Ignore attribute requirements to equip gloves\n\z
"
build.configTab:BuildModList()
runCallback("OnFrame")

assert.is_true(build.calcsTab.mainEnv.modDB:Flag(nil, "IgnoreAttributeRequirementsForGloves"))
end)

it("GloveExplicitModTransform flag is set from ascendancy mod string", function()
build.configTab.input.customMods = "\z
their explicit modifiers are transformed into more powerful related modifiers\n\z
"
build.configTab:BuildModList()
runCallback("OnFrame")

assert.is_true(build.calcsTab.mainEnv.modDB:Flag(nil, "GloveExplicitModTransform"))
end)

-- CalcPerform: base type transform overwrites glove armour values

it("GloveBaseTypeTransform: equipping pure-evasion gloves gains Armour from Fists of Stone base", function()
-- Suede Bracers: Evasion only, no Armour stat
build.itemsTab:CreateDisplayItemFromRaw([[
New Item
Suede Bracers
Evasion: 10
]])
build.itemsTab:AddDisplayItem()
runCallback("OnFrame")

local baseArmour = build.calcsTab.mainOutput.Armour or 0

-- Apply transform flag
build.configTab.input.customMods = "\z
Gloves you equip have their base type transformed to fists of stone while equipped\n\z
"
build.configTab:BuildModList()
runCallback("OnFrame")

local transformedArmour = build.calcsTab.mainOutput.Armour or 0
-- Fists of Stone base armour is 44; should exceed the evasion-only baseline
assert.is_true(transformedArmour > baseArmour,
("expected transformed armour %d > base armour %d"):format(transformedArmour, baseArmour))
assert.is_near(44, transformedArmour, 10)
end)

it("GloveBaseTypeTransform: armour-only gloves take on Fists of Stone armour value (~44)", function()
-- Stocky Mitts base armour = 15; Fists of Stone base armour = 44
build.itemsTab:CreateDisplayItemFromRaw([[
New Item
Stocky Mitts
]])
build.itemsTab:AddDisplayItem()
runCallback("OnFrame")

local baseArmour = build.calcsTab.mainOutput.Armour or 0

build.configTab.input.customMods = "\z
Gloves you equip have their base type transformed to fists of stone while equipped\n\z
"
build.configTab:BuildModList()
runCallback("OnFrame")

local transformedArmour = build.calcsTab.mainOutput.Armour or 0
-- Armour should change from Stocky Mitts base (~15) to Fists of Stone base (~44)
assert.are_not.equals(baseArmour, transformedArmour)
assert.is_near(44, transformedArmour, 10)
end)

it("GloveBaseTypeTransform: Fists of Stone implicit injects Evasion per level into modDB", function()
build.itemsTab:CreateDisplayItemFromRaw([[
New Item
Stocky Mitts
Armour: 10
]])
build.itemsTab:AddDisplayItem()
runCallback("OnFrame")

local baseEvasion = build.calcsTab.mainOutput.Evasion or 0

build.configTab.input.customMods = "\z
Gloves you equip have their base type transformed to fists of stone while equipped\n\z
"
build.configTab:BuildModList()
runCallback("OnFrame")

-- Implicit: +2 Evasion per level; at level 1 that is +2, plus Fists of Stone base evasion (40)
local transformedEvasion = build.calcsTab.mainOutput.Evasion or 0
assert.is_true(transformedEvasion > baseEvasion,
("expected evasion %d > base evasion %d after Fists of Stone transform"):format(transformedEvasion, baseEvasion))
end)

it("GloveBaseTypeTransform: Ward implicit is injected (Ward > 0 after transform)", function()
build.itemsTab:CreateDisplayItemFromRaw([[
New Item
Stocky Mitts
Armour: 10
]])
build.itemsTab:AddDisplayItem()
build.configTab.input.customMods = "\z
Gloves you equip have their base type transformed to fists of stone while equipped\n\z
"
build.configTab:BuildModList()
runCallback("OnFrame")

-- Implicit: +1 Ward per level; should be > 0 at any character level
local ward = build.calcsTab.calcsOutput.Ward or 0
assert.is_true(ward > 0,
("expected Ward > 0 from Fists of Stone implicit, got %d"):format(ward))
end)

it("GloveBaseTypeTransform: actual Fists of Stone base still receives per-level Evasion implicit", function()
-- Equip an actual Fists of Stone item (not a transformed base).
-- The guard must not skip implicit injection when baseName is already "Fists of Stone".
build.itemsTab:CreateDisplayItemFromRaw([[
New Item
Fists of Stone
]])
build.itemsTab:AddDisplayItem()
runCallback("OnFrame")

local baseEvasion = build.calcsTab.mainOutput.Evasion or 0

build.configTab.input.customMods = "\z
Gloves you equip have their base type transformed to fists of stone while equipped\n\z
"
build.configTab:BuildModList()
runCallback("OnFrame")

-- +2 Evasion per level implicit must fire; Evasion should increase above the bare base
local transformedEvasion = build.calcsTab.mainOutput.Evasion or 0
assert.is_true(transformedEvasion > baseEvasion,
("expected FoS implicit to increase Evasion from %d to >%d"):format(baseEvasion, baseEvasion))
end)

-- CalcPerform: scoped attribute requirement ignore

it("IgnoreAttributeRequirementsForGloves does not zero global attribute requirements", function()
-- The scoped flag should NOT zero requirements from non-glove sources
build.configTab.input.customMods = "\z
Ignore attribute requirements to equip gloves\n\z
"
build.configTab:BuildModList()
runCallback("OnFrame")

-- Global flag should be nil; only the scoped flag should be set
local globalFlag = build.calcsTab.mainEnv.modDB:Flag(nil, "IgnoreAttributeRequirements")
assert.is_falsy(globalFlag)
assert.is_true(build.calcsTab.mainEnv.modDB:Flag(nil, "IgnoreAttributeRequirementsForGloves"))
end)
end)
12 changes: 10 additions & 2 deletions src/Data/Bases/gloves.lua
Original file line number Diff line number Diff line change
Expand Up @@ -889,5 +889,13 @@ itemBases["Grand Manchettes"] = {
armour = { Armour = 44, Evasion = 40, EnergyShield = 15, },
req = { level = 65, str = 32, dex = 32, int = 32, },
}


itemBases["Fists of Stone"] = {
type = "Gloves",
subType = "Armour/Evasion/Energy Shield",
quality = 20,
socketLimit = 3,
tags = { armour = true, default = true, gloves = true, str_dex_int_armour = true, },
implicitModTypes = { },

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invented tag — martial_artist_transform is not referenced anywhere.

A search of the entire src/ tree finds zero uses of martial_artist_transform. It is not a tag present in PoE2 game data and nothing in PoB reads it. This adds noise to the base definition without any effect. The unstaged working-tree version of this file already removes it — that removal is correct and should be included in the next commit.

armour = { Armour = 44, Evasion = 40, EnergyShield = 15, },
req = { },
}
Loading
Loading