fix(monsters): apply 1.13c MonUMod resistance bonuses in applyEliteModifiers/enrichMonsterStats and isolate hover from 36px click-assist
This commit is contained in:
parent
0be447fab3
commit
44ec4124b9
|
|
@ -1014,16 +1014,69 @@ export function applyEliteModifiers(
|
|||
damage = Math.round(damage * 1.2)
|
||||
speed = Math.round(speed * 1.1 * 100) / 100
|
||||
}
|
||||
let physicalRes = base.resistances?.physical ?? 0
|
||||
let magicRes = base.resistances?.magic ?? 0
|
||||
let fireRes = base.resistances?.fire ?? 0
|
||||
let lightningRes = base.lightningResist ?? base.resistances?.lightning ?? 0
|
||||
let coldRes = base.resistances?.cold ?? 0
|
||||
let poisonRes = base.resistances?.poison ?? 0
|
||||
|
||||
if (modifiers.includes('stoneskin')) {
|
||||
physicalRes += 50
|
||||
}
|
||||
if (modifiers.includes('manahit')) {
|
||||
magicRes += 20
|
||||
}
|
||||
if (modifiers.includes('fireenchant')) {
|
||||
fireRes += 75
|
||||
}
|
||||
if (modifiers.includes('coldenchant')) {
|
||||
coldRes += 75
|
||||
}
|
||||
if (modifiers.includes('lightenchant')) {
|
||||
lightningRes += 75
|
||||
}
|
||||
if (modifiers.includes('poisonenchant')) {
|
||||
poisonRes += 75
|
||||
}
|
||||
if (modifiers.includes('magicresistant')) {
|
||||
fireRes += 40
|
||||
coldRes += 40
|
||||
lightningRes += 20
|
||||
}
|
||||
if (modifiers.includes('spectralhit')) {
|
||||
fireRes += 20
|
||||
coldRes += 20
|
||||
lightningRes += 20
|
||||
}
|
||||
|
||||
const hasAnyResMod =
|
||||
physicalRes !== 0 ||
|
||||
magicRes !== 0 ||
|
||||
fireRes !== 0 ||
|
||||
lightningRes !== 0 ||
|
||||
coldRes !== 0 ||
|
||||
poisonRes !== 0
|
||||
const updatedResistances = base.resistances
|
||||
? { ...base.resistances, lightning: lightningRes }
|
||||
: (lightningRes !== 0 ? { lightning: lightningRes } : undefined)
|
||||
? {
|
||||
...base.resistances,
|
||||
physical: physicalRes,
|
||||
magic: magicRes,
|
||||
fire: fireRes,
|
||||
lightning: lightningRes,
|
||||
cold: coldRes,
|
||||
poison: poisonRes,
|
||||
}
|
||||
: hasAnyResMod
|
||||
? {
|
||||
...(physicalRes !== 0 ? { physical: physicalRes } : {}),
|
||||
...(magicRes !== 0 ? { magic: magicRes } : {}),
|
||||
...(fireRes !== 0 ? { fire: fireRes } : {}),
|
||||
...(lightningRes !== 0 ? { lightning: lightningRes } : {}),
|
||||
...(coldRes !== 0 ? { cold: coldRes } : {}),
|
||||
...(poisonRes !== 0 ? { poison: poisonRes } : {}),
|
||||
}
|
||||
: undefined
|
||||
|
||||
return {
|
||||
...base,
|
||||
|
|
@ -1743,9 +1796,28 @@ export function enrichMonsterStats(
|
|||
const nameKey = stats.nameKey ?? (isSuperUniqueLeader ? (su?.nameKey || stats.name || stats.superUniqueId) : (kind.nameKey || stats.name))
|
||||
const rawDesc = stats.descKey ?? (kind.descKey !== '' ? kind.descKey : undefined)
|
||||
const descKey = rawDesc !== undefined && rawDesc !== '' && rawDesc.toLowerCase() !== 'dummy' ? rawDesc : undefined
|
||||
const mergedRes = stats.resistances
|
||||
? { ...kind.resistances, ...stats.resistances }
|
||||
const kindWithMods = stats.modifiers && stats.modifiers.length > 0
|
||||
? applyEliteModifiers(
|
||||
{
|
||||
...stats,
|
||||
resistances: kind.resistances,
|
||||
lightningResist: kind.resistances.lightning,
|
||||
},
|
||||
stats.rank ?? 'normal',
|
||||
stats.modifiers,
|
||||
stats.superUniqueId,
|
||||
).resistances ?? kind.resistances
|
||||
: kind.resistances
|
||||
const mergedRes: MonsterResistances = stats.modifiers && stats.modifiers.length > 0
|
||||
? {
|
||||
physical: Math.max(kindWithMods.physical ?? 0, stats.resistances?.physical ?? kindWithMods.physical ?? 0),
|
||||
magic: Math.max(kindWithMods.magic ?? 0, stats.resistances?.magic ?? kindWithMods.magic ?? 0),
|
||||
fire: Math.max(kindWithMods.fire ?? 0, stats.resistances?.fire ?? kindWithMods.fire ?? 0),
|
||||
lightning: Math.max(kindWithMods.lightning ?? 0, stats.resistances?.lightning ?? kindWithMods.lightning ?? 0),
|
||||
cold: Math.max(kindWithMods.cold ?? 0, stats.resistances?.cold ?? kindWithMods.cold ?? 0),
|
||||
poison: Math.max(kindWithMods.poison ?? 0, stats.resistances?.poison ?? kindWithMods.poison ?? 0),
|
||||
}
|
||||
: (stats.resistances ? { ...kind.resistances, ...stats.resistances } : kind.resistances)
|
||||
return {
|
||||
...stats,
|
||||
nameKey,
|
||||
|
|
@ -1756,7 +1828,10 @@ export function enrichMonsterStats(
|
|||
killable: stats.killable ?? kind.killable,
|
||||
inTown: stats.inTown ?? kind.inTown,
|
||||
resistances: mergedRes,
|
||||
resists: stats.resists ? { ...mergedRes, ...stats.resists } : mergedRes,
|
||||
resists: stats.modifiers && stats.modifiers.length > 0
|
||||
? mergedRes
|
||||
: (stats.resists ? { ...mergedRes, ...stats.resists } : mergedRes),
|
||||
...(mergedRes.lightning !== undefined ? { lightningResist: mergedRes.lightning } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4787,6 +4787,9 @@ export class SceneMouseController {
|
|||
this.lockedMonster = null
|
||||
} else if (e.button === 2) {
|
||||
this.isRightMouseDown = false
|
||||
if (!this.isLeftMouseDown) {
|
||||
this.activeClickType = 'none'
|
||||
}
|
||||
} else {
|
||||
this.isLeftMouseDown = false
|
||||
this.isRightMouseDown = false
|
||||
|
|
@ -4815,7 +4818,8 @@ export class SceneMouseController {
|
|||
const range = getSkillRange(rightSkillId, this.hudManager)
|
||||
|
||||
// 1. Check Monster: sprite bounds first, fallback to living monster within ~36 px
|
||||
let clickedMonster: Monster | null = this.findMonsterAtWorld(worldX, worldY)
|
||||
const hoveredAtClick = this.findMonsterAtWorld(worldX, worldY)
|
||||
let clickedMonster: Monster | null = hoveredAtClick
|
||||
if (clickedMonster === null) {
|
||||
let bestMonDist = 36
|
||||
for (const m of this.engine.world.monsters) {
|
||||
|
|
@ -4833,7 +4837,7 @@ export class SceneMouseController {
|
|||
|
||||
if (clickedMonster !== null) {
|
||||
this.activeClickType = 'monster'
|
||||
this.hoveredMonster = clickedMonster
|
||||
this.hoveredMonster = hoveredAtClick
|
||||
const distToPlayer = Math.hypot(clickedMonster.x - player.x, clickedMonster.y - player.y)
|
||||
if (distToPlayer <= range) {
|
||||
this.navTarget = null
|
||||
|
|
@ -5060,7 +5064,8 @@ export class SceneMouseController {
|
|||
}
|
||||
|
||||
// 4. Check Monster: sprite bounds first, fallback to living monster within ~36 px
|
||||
let clickedMonster: Monster | null = this.findMonsterAtWorld(worldX, worldY)
|
||||
const hoveredAtClick = this.findMonsterAtWorld(worldX, worldY)
|
||||
let clickedMonster: Monster | null = hoveredAtClick
|
||||
if (clickedMonster === null) {
|
||||
let bestMonDist = 36
|
||||
for (const m of this.engine.world.monsters) {
|
||||
|
|
@ -5079,7 +5084,7 @@ export class SceneMouseController {
|
|||
if (clickedMonster !== null) {
|
||||
this.activeClickType = 'monster'
|
||||
this.lockedMonster = clickedMonster
|
||||
this.hoveredMonster = clickedMonster
|
||||
this.hoveredMonster = hoveredAtClick
|
||||
const leftSkillId = this.hudManager ? this.hudManager.hotkeys.leftSkillId : 47
|
||||
const range = getSkillRange(leftSkillId, this.hudManager)
|
||||
const distToPlayer = Math.hypot(clickedMonster.x - player.x, clickedMonster.y - player.y)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import {
|
|||
readMonsterKinds,
|
||||
readSuperUniques,
|
||||
monsterStatsOf,
|
||||
applyEliteModifiers,
|
||||
enrichMonsterPacks,
|
||||
} from '../src/game/monsters.ts'
|
||||
import { Rng } from '../src/game/rng.ts'
|
||||
|
|
@ -1153,6 +1154,132 @@ describe('Diablo II 1.13c Monster Lifebar & 2-Line Subtext Tooltip Parity', () =
|
|||
// Leader level must be base 1 + 2 (3) or base 1 + 3 (4), never +4 or +5!
|
||||
expect(leader.level === 3 || leader.level === 4).toBe(true)
|
||||
})
|
||||
|
||||
it('applies 1.13c MonUMod elemental/physical/magic resistance bonuses in applyEliteModifiers and enrichMonsterPacks so Bishibosh and Frozenstein display immunities on Line 2 (D2Game.dll 0x6fc41be0)', () => {
|
||||
const kinds = readMonsterKinds(parseTable(new TextEncoder().encode(RAW_MONSTATS)), 'normal')
|
||||
const superUniquesList = readSuperUniques(parseTable(new TextEncoder().encode(RAW_SUPERUNIQUES)), 'normal')
|
||||
const superUniques = new Map(superUniquesList.map(s => [s.id.toLowerCase(), s]))
|
||||
|
||||
// 1. Pre-baked Cold Plains Bishibosh (fallenshaman1: base fire=25 + magicresistant(+40) + fireenchant(+75) = 140 >= 100)
|
||||
const bishiboshPack = enrichMonsterPacks(
|
||||
[
|
||||
{
|
||||
superUniqueId: 'Bishibosh',
|
||||
members: [
|
||||
{
|
||||
id: 'fallenshaman1',
|
||||
name: 'Bishibosh',
|
||||
hp: 43,
|
||||
damage: 2,
|
||||
cooldownTicks: 25,
|
||||
reach: 40,
|
||||
aggroRadius: 560,
|
||||
speed: 113,
|
||||
xp: 32,
|
||||
level: 5,
|
||||
rank: 'unique',
|
||||
resistances: { physical: 0, magic: 0, fire: 25, lightning: 20, cold: 0, poison: 0 },
|
||||
resists: { physical: 0, magic: 0, fire: 25, lightning: 20, cold: 0, poison: 0 },
|
||||
modifiers: ['magicresistant', 'fireenchant'],
|
||||
superUniqueId: 'Bishibosh',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
kinds,
|
||||
superUniques,
|
||||
)
|
||||
const bishiboshStats = bishiboshPack[0]!.members[0]!
|
||||
expect(bishiboshStats.resistances?.fire).toBe(140)
|
||||
expect(bishiboshStats.resistances?.cold).toBe(40)
|
||||
expect(bishiboshStats.resistances?.lightning).toBe(20)
|
||||
|
||||
const bishiboshHud = monsterToTargetHudInfo({
|
||||
kind: 'fallenshaman1',
|
||||
x: 100,
|
||||
y: 100,
|
||||
hp: 43,
|
||||
maxHp: 43,
|
||||
state: 'idle',
|
||||
stats: bishiboshStats,
|
||||
} as any)
|
||||
const bishiboshSubEn = buildMonsterHoverSubtext(bishiboshHud, 'en')
|
||||
expect(bishiboshSubEn.line1).toBe('Demon Magic Resistant Fire Enchanted Raises Fallen')
|
||||
expect(bishiboshSubEn.line2).toBe('Immune to Fire')
|
||||
expect(bishiboshSubEn.line2Colored).toBe('ÿc1Immune to Fire')
|
||||
|
||||
const bishiboshSubZh = buildMonsterHoverSubtext(bishiboshHud, 'zh')
|
||||
expect(bishiboshSubZh.line2).toBe(lookupMonsterTblString('strScaryMonDesc1', 'zh'))
|
||||
expect(bishiboshSubZh.line2Colored).toBe(`ÿc1${lookupMonsterTblString('strScaryMonDesc1', 'zh')}`)
|
||||
|
||||
// 2. Pre-baked Frozenstein (snowyeti4: base cold=75 + coldenchant(+75) = 150 >= 100, manahit(+20 magic))
|
||||
const frozensteinPack = enrichMonsterPacks(
|
||||
[
|
||||
{
|
||||
superUniqueId: 'Frozenstein',
|
||||
members: [
|
||||
{
|
||||
id: 'snowyeti4',
|
||||
name: 'Frozenstein',
|
||||
hp: 300,
|
||||
damage: 25,
|
||||
cooldownTicks: 25,
|
||||
reach: 40,
|
||||
aggroRadius: 560,
|
||||
speed: 120,
|
||||
xp: 200,
|
||||
level: 40,
|
||||
rank: 'unique',
|
||||
resistances: { physical: 0, magic: 0, fire: 0, lightning: 0, cold: 75, poison: 0 },
|
||||
modifiers: ['coldenchant', 'manahit'],
|
||||
superUniqueId: 'Frozenstein',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
kinds,
|
||||
superUniques,
|
||||
)
|
||||
const frozensteinStats = frozensteinPack[0]!.members[0]!
|
||||
expect(frozensteinStats.resistances?.cold).toBe(150)
|
||||
expect(frozensteinStats.resistances?.magic).toBe(20)
|
||||
|
||||
// 3. applyEliteModifiers directly for stoneskin (+50 physical), spectralhit (+20 fire/cold/ltng), poisonenchant (+75 poison)
|
||||
const baseStats = monsterStatsOf(kinds.get('fallen1')!, new Rng(1), 4)
|
||||
const modded = applyEliteModifiers(baseStats, 'unique', ['stoneskin', 'spectralhit', 'poisonenchant'])
|
||||
expect(modded.resistances?.physical).toBe(50)
|
||||
expect(modded.resistances?.fire).toBe(20)
|
||||
expect(modded.resistances?.cold).toBe(20)
|
||||
expect(modded.resistances?.lightning).toBe(20)
|
||||
expect(modded.resistances?.poison).toBe(75)
|
||||
})
|
||||
|
||||
it('isolates hoveredMonster from 36px click-assist fallback in handleRightClickWorld and handleLeftClickWorld', () => {
|
||||
const { canvas, engine, hudManager, controller } = createHoverTestScene()
|
||||
const monster0 = engine.world.monsters[0]!
|
||||
// Monster 0 is at (560, 500) with drawBounds [544..576, 450..504]
|
||||
// World point (535, 500) is OUTSIDE drawBounds (x=535 < 544) but WITHIN 36px fallback (dist=25 <= 36)
|
||||
expect(controller.findMonsterAtWorld(535, 500)).toBeNull()
|
||||
|
||||
// 1. Right-click at (535, 500) (outside drawBounds, inside 36px fallback) must NOT stick hoveredMonster or targetMonster
|
||||
controller.handleRightClickWorld(535, 500)
|
||||
expect(controller.hoveredMonster).toBeNull()
|
||||
expect(hudManager.targetMonster).toBeNull()
|
||||
canvas.dispatchEvent(new SyntheticPointerEvent('pointerup', { clientX: 435, clientY: 316, button: 2, buttons: 0 }))
|
||||
expect(controller.activeClickType).toBe('none')
|
||||
|
||||
// 2. Left-click at (535, 500) (outside drawBounds, inside 36px fallback) locks monster while held, then clears on release
|
||||
controller.isLeftMouseDown = true
|
||||
controller.handleLeftClickWorld(535, 500)
|
||||
expect(controller.lockedMonster).toBe(monster0)
|
||||
expect(controller.hoveredMonster).toBeNull()
|
||||
expect(hudManager.targetMonster?.nameKey).toBe('Fallen')
|
||||
|
||||
canvas.dispatchEvent(new SyntheticPointerEvent('pointerup', { clientX: 435, clientY: 316, button: 0, buttons: 0 }))
|
||||
expect(controller.lockedMonster).toBeNull()
|
||||
expect(controller.hoveredMonster).toBeNull()
|
||||
expect(hudManager.targetMonster).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue