fix(skill): implement Glacial Spike explosion and 64px AoE freeze (Fixes #430)
This commit is contained in:
parent
0aa6c3680d
commit
71466df5e7
|
|
@ -70,6 +70,7 @@ export interface MonsterSimInput {
|
||||||
y: number
|
y: number
|
||||||
stats: { id: string }
|
stats: { id: string }
|
||||||
chillTicks?: number
|
chillTicks?: number
|
||||||
|
frozenTicks?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnimatorSimInput {
|
export interface AnimatorSimInput {
|
||||||
|
|
@ -125,13 +126,18 @@ export function executeMonsterRenderLoop(
|
||||||
const drawY = frame.anchorY !== undefined
|
const drawY = frame.anchorY !== undefined
|
||||||
? monster.y + frame.anchorY
|
? monster.y + frame.anchorY
|
||||||
: monster.y - frame.height + FEET_HEIGHT / 2
|
: monster.y - frame.height + FEET_HEIGHT / 2
|
||||||
|
const isFrozen = monster.frozenTicks !== undefined && monster.frozenTicks > 0
|
||||||
const isChilled = monster.chillTicks !== undefined && monster.chillTicks > 0
|
const isChilled = monster.chillTicks !== undefined && monster.chillTicks > 0
|
||||||
const isAdditiveFallback = ADDITIVE_MONSTER_TOKENS.has(art.token.toUpperCase())
|
const isAdditiveFallback = ADDITIVE_MONSTER_TOKENS.has(art.token.toUpperCase())
|
||||||
const baseBlendMode = art.blendMode ?? (isAdditiveFallback ? 'additive' : 'normal')
|
const baseBlendMode = art.blendMode ?? (isAdditiveFallback ? 'additive' : 'normal')
|
||||||
renderer.draw(frame, drawX, drawY, {
|
renderer.draw(frame, drawX, drawY, {
|
||||||
atlas: art.handle,
|
atlas: art.handle,
|
||||||
blendMode: baseBlendMode,
|
blendMode: baseBlendMode,
|
||||||
...(isChilled ? { tint: [0.4, 0.65, 1.0, 1.0] as const, paletteRow: 1 } : {}),
|
...(isFrozen
|
||||||
|
? { tint: [0.25, 0.6, 1.0, 1.0] as const, paletteRow: 1 }
|
||||||
|
: isChilled
|
||||||
|
? { tint: [0.4, 0.65, 1.0, 1.0] as const, paletteRow: 1 }
|
||||||
|
: {}),
|
||||||
})
|
})
|
||||||
if (art.glowGroups !== undefined) {
|
if (art.glowGroups !== undefined) {
|
||||||
const glowGroup = art.glowGroups[groupIndex]
|
const glowGroup = art.glowGroups[groupIndex]
|
||||||
|
|
@ -151,8 +157,13 @@ export function executeMonsterRenderLoop(
|
||||||
if (!missingArtList.includes(missingId)) {
|
if (!missingArtList.includes(missingId)) {
|
||||||
missingArtList.push(missingId)
|
missingArtList.push(missingId)
|
||||||
}
|
}
|
||||||
|
const isFrozen = monster.frozenTicks !== undefined && monster.frozenTicks > 0
|
||||||
const isChilled = monster.chillTicks !== undefined && monster.chillTicks > 0
|
const isChilled = monster.chillTicks !== undefined && monster.chillTicks > 0
|
||||||
const tint = isChilled ? ([0.4, 0.65, 1.0, 1.0] as const) : ([0.9, 0.2, 0.2, 1.0] as const)
|
const tint = isFrozen
|
||||||
|
? ([0.25, 0.6, 1.0, 1.0] as const)
|
||||||
|
: isChilled
|
||||||
|
? ([0.4, 0.65, 1.0, 1.0] as const)
|
||||||
|
: ([0.9, 0.2, 0.2, 1.0] as const)
|
||||||
renderer.drawSolid(monster.x - MARKER_WIDTH / 2, monster.y - MARKER_HEIGHT, MARKER_WIDTH, MARKER_HEIGHT, tint)
|
renderer.drawSolid(monster.x - MARKER_WIDTH / 2, monster.y - MARKER_HEIGHT, MARKER_WIDTH, MARKER_HEIGHT, tint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -550,6 +550,17 @@ export class GameEngine {
|
||||||
maxFrames,
|
maxFrames,
|
||||||
radius: hit.aoeRadius ?? 24,
|
radius: hit.aoeRadius ?? 24,
|
||||||
})
|
})
|
||||||
|
if (missileType === 'glacialspike' || hit.skillId === '55' || hit.skillId === 'glacialspike') {
|
||||||
|
const ejectaData = CANONICAL_113C_MISSILES['glacialspikeejecta']
|
||||||
|
this.explosions.push({
|
||||||
|
missileType: 'glacialspikeejecta',
|
||||||
|
x: hit.x,
|
||||||
|
y: hit.y,
|
||||||
|
frame: 0,
|
||||||
|
maxFrames: ejectaData?.animLen || 15,
|
||||||
|
radius: hit.aoeRadius ?? 64,
|
||||||
|
})
|
||||||
|
}
|
||||||
this.world.events.push({
|
this.world.events.push({
|
||||||
kind: 'missileExplode',
|
kind: 'missileExplode',
|
||||||
missileType: explType,
|
missileType: explType,
|
||||||
|
|
@ -567,7 +578,15 @@ export class GameEngine {
|
||||||
for (const m of this.world.monsters) {
|
for (const m of this.world.monsters) {
|
||||||
if (m.state === 'dead' || m.hp <= 0) continue
|
if (m.state === 'dead' || m.hp <= 0) continue
|
||||||
if (Math.hypot(m.x - wallHit.x, m.y - wallHit.y) <= wallHit.aoeRadius) {
|
if (Math.hypot(m.x - wallHit.x, m.y - wallHit.y) <= wallHit.aoeRadius) {
|
||||||
damageMonster(this.world, m.index, 10)
|
const dmg = wallHit.damage ?? 10
|
||||||
|
damageMonster(this.world, m.index, dmg)
|
||||||
|
if (wallHit.statusEffect === 'freeze' || wallHit.missileType === 'glacialspike' || wallHit.skillId === '55' || wallHit.skillId === 'glacialspike') {
|
||||||
|
m.frozenTicks = wallHit.statusDuration ?? 75
|
||||||
|
} else if (wallHit.statusEffect === 'chill') {
|
||||||
|
m.chillTicks = wallHit.statusDuration ?? 100
|
||||||
|
} else if (wallHit.statusEffect === 'poison') {
|
||||||
|
m.poisonTicks = wallHit.statusDuration ?? 100
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -585,6 +604,17 @@ export class GameEngine {
|
||||||
maxFrames,
|
maxFrames,
|
||||||
radius: wallHit.aoeRadius ?? 24,
|
radius: wallHit.aoeRadius ?? 24,
|
||||||
})
|
})
|
||||||
|
if (missileType === 'glacialspike' || wallHit.skillId === '55' || wallHit.skillId === 'glacialspike') {
|
||||||
|
const ejectaData = CANONICAL_113C_MISSILES['glacialspikeejecta']
|
||||||
|
this.explosions.push({
|
||||||
|
missileType: 'glacialspikeejecta',
|
||||||
|
x: wallHit.x,
|
||||||
|
y: wallHit.y,
|
||||||
|
frame: 0,
|
||||||
|
maxFrames: ejectaData?.animLen || 15,
|
||||||
|
radius: wallHit.aoeRadius ?? 64,
|
||||||
|
})
|
||||||
|
}
|
||||||
this.world.events.push({
|
this.world.events.push({
|
||||||
kind: 'missileExplode',
|
kind: 'missileExplode',
|
||||||
missileType: explType,
|
missileType: explType,
|
||||||
|
|
|
||||||
|
|
@ -1145,6 +1145,17 @@ export class MissileEngine {
|
||||||
targetY: hitWallY,
|
targetY: hitWallY,
|
||||||
dmgPacket: m.dmgPacket,
|
dmgPacket: m.dmgPacket,
|
||||||
})
|
})
|
||||||
|
this.spawnMissile({
|
||||||
|
missileNameOrId: 'glacialspikeejecta',
|
||||||
|
sourceSkillId: m.sourceSkillId,
|
||||||
|
slvl: m.slvl,
|
||||||
|
owner: m.owner,
|
||||||
|
startX: hitWallX,
|
||||||
|
startY: hitWallY,
|
||||||
|
targetX: hitWallX,
|
||||||
|
targetY: hitWallY,
|
||||||
|
dmgPacket: m.dmgPacket,
|
||||||
|
})
|
||||||
subMissilesSpawned.push(expName)
|
subMissilesSpawned.push(expName)
|
||||||
subMissilesSpawned.push('glacialspikeejecta')
|
subMissilesSpawned.push('glacialspikeejecta')
|
||||||
} else if (m.name === 'iceblast' || m.sourceSkillId === 45) {
|
} else if (m.name === 'iceblast' || m.sourceSkillId === 45) {
|
||||||
|
|
@ -1463,7 +1474,19 @@ export class MissileEngine {
|
||||||
targetY: m.y,
|
targetY: m.y,
|
||||||
dmgPacket: m.dmgPacket,
|
dmgPacket: m.dmgPacket,
|
||||||
})
|
})
|
||||||
|
this.spawnMissile({
|
||||||
|
missileNameOrId: 'glacialspikeejecta',
|
||||||
|
sourceSkillId: m.sourceSkillId,
|
||||||
|
slvl: m.slvl,
|
||||||
|
owner: m.owner,
|
||||||
|
startX: m.x,
|
||||||
|
startY: m.y,
|
||||||
|
targetX: m.x,
|
||||||
|
targetY: m.y,
|
||||||
|
dmgPacket: m.dmgPacket,
|
||||||
|
})
|
||||||
subMissilesSpawned.push(expName)
|
subMissilesSpawned.push(expName)
|
||||||
|
subMissilesSpawned.push('glacialspikeejecta')
|
||||||
} else if (m.name === 'teeth') {
|
} else if (m.name === 'teeth') {
|
||||||
const expName = m.record.explosionMissile || 'teethexplode'
|
const expName = m.record.explosionMissile || 'teethexplode'
|
||||||
this.spawnMissile({
|
this.spawnMissile({
|
||||||
|
|
|
||||||
|
|
@ -815,6 +815,27 @@ export const CANONICAL_113C_MISSILES: Readonly<Record<string, MissileTxtData>> =
|
||||||
trans: 1,
|
trans: 1,
|
||||||
distancePx: 0,
|
distancePx: 0,
|
||||||
},
|
},
|
||||||
|
glacialspikeejecta: {
|
||||||
|
name: 'glacialspikeejecta',
|
||||||
|
id: 89,
|
||||||
|
vel: 0,
|
||||||
|
maxVel: 0,
|
||||||
|
range: 15,
|
||||||
|
levRange: 0,
|
||||||
|
celFile: 'FreezeExplodeEjecta',
|
||||||
|
animLen: 15,
|
||||||
|
animSpeed: 16,
|
||||||
|
loopAnim: 0,
|
||||||
|
numDirections: 8,
|
||||||
|
explosionMissile: '',
|
||||||
|
light: 11,
|
||||||
|
red: 81,
|
||||||
|
green: 81,
|
||||||
|
blue: 255,
|
||||||
|
speedPxPerSec: 0,
|
||||||
|
trans: 1,
|
||||||
|
distancePx: 0,
|
||||||
|
},
|
||||||
frozenorb: {
|
frozenorb: {
|
||||||
name: 'frozenorb',
|
name: 'frozenorb',
|
||||||
id: 260,
|
id: 260,
|
||||||
|
|
@ -4384,6 +4405,9 @@ export interface ProjectileOutcome {
|
||||||
readonly skillId?: string | undefined
|
readonly skillId?: string | undefined
|
||||||
readonly missileType?: string | undefined
|
readonly missileType?: string | undefined
|
||||||
readonly aoeRadius?: number | undefined
|
readonly aoeRadius?: number | undefined
|
||||||
|
readonly damage?: number | undefined
|
||||||
|
readonly statusEffect?: 'chill' | 'freeze' | 'poison' | undefined
|
||||||
|
readonly statusDuration?: number | undefined
|
||||||
}[] | undefined
|
}[] | undefined
|
||||||
/** Newly spawned child projectiles during this tick (e.g. Frozen Orb bolts / nova). */
|
/** Newly spawned child projectiles during this tick (e.g. Frozen Orb bolts / nova). */
|
||||||
readonly spawnedProjectiles?: readonly Projectile[] | undefined
|
readonly spawnedProjectiles?: readonly Projectile[] | undefined
|
||||||
|
|
@ -4454,6 +4478,9 @@ export function tickProjectiles(
|
||||||
skillId?: string | undefined
|
skillId?: string | undefined
|
||||||
missileType?: string | undefined
|
missileType?: string | undefined
|
||||||
aoeRadius?: number | undefined
|
aoeRadius?: number | undefined
|
||||||
|
damage?: number | undefined
|
||||||
|
statusEffect?: 'chill' | 'freeze' | 'poison' | undefined
|
||||||
|
statusDuration?: number | undefined
|
||||||
}[] = []
|
}[] = []
|
||||||
let expired = 0
|
let expired = 0
|
||||||
let wallHits = 0
|
let wallHits = 0
|
||||||
|
|
@ -4848,6 +4875,9 @@ export function tickProjectiles(
|
||||||
skillId: projectile.skillId,
|
skillId: projectile.skillId,
|
||||||
missileType: projectile.missileType,
|
missileType: projectile.missileType,
|
||||||
aoeRadius: projectile.aoeRadius,
|
aoeRadius: projectile.aoeRadius,
|
||||||
|
damage: projectile.damage,
|
||||||
|
statusEffect: projectile.statusEffect,
|
||||||
|
statusDuration: projectile.statusDuration,
|
||||||
})
|
})
|
||||||
if (projectile.missileType === 'frozenorb') {
|
if (projectile.missileType === 'frozenorb') {
|
||||||
for (let i = 0; i < 64; i += 4) {
|
for (let i = 0; i < 64; i += 4) {
|
||||||
|
|
@ -4871,6 +4901,33 @@ export function tickProjectiles(
|
||||||
pierce: false,
|
pierce: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} else if (projectile.missileType === 'glacialspike') {
|
||||||
|
spawned.push({
|
||||||
|
skillId: projectile.skillId,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
vx: 0,
|
||||||
|
vy: 0,
|
||||||
|
damage: 0,
|
||||||
|
ttl: 15,
|
||||||
|
fromPlayer: projectile.fromPlayer,
|
||||||
|
missileType: 'freezeexplode',
|
||||||
|
aoeRadius: projectile.aoeRadius ?? 64,
|
||||||
|
pierce: true,
|
||||||
|
})
|
||||||
|
spawned.push({
|
||||||
|
skillId: projectile.skillId,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
vx: 0,
|
||||||
|
vy: 0,
|
||||||
|
damage: 0,
|
||||||
|
ttl: 15,
|
||||||
|
fromPlayer: projectile.fromPlayer,
|
||||||
|
missileType: 'glacialspikeejecta',
|
||||||
|
aoeRadius: projectile.aoeRadius ?? 64,
|
||||||
|
pierce: true,
|
||||||
|
})
|
||||||
} else if (isBlizzardShard(projectile.missileType)) {
|
} else if (isBlizzardShard(projectile.missileType)) {
|
||||||
const expType = getBlizzardExplosionMissile(projectile.missileType!)
|
const expType = getBlizzardExplosionMissile(projectile.missileType!)
|
||||||
spawned.push({
|
spawned.push({
|
||||||
|
|
@ -4900,6 +4957,8 @@ export function tickProjectiles(
|
||||||
projectile.missileType === 'meteor' ||
|
projectile.missileType === 'meteor' ||
|
||||||
projectile.missileType === 'meteortail' ||
|
projectile.missileType === 'meteortail' ||
|
||||||
projectile.missileType === 'firewallmaker' ||
|
projectile.missileType === 'firewallmaker' ||
|
||||||
|
projectile.missileType === 'freezeexplode' ||
|
||||||
|
projectile.missileType === 'glacialspikeejecta' ||
|
||||||
isTurretEmitter
|
isTurretEmitter
|
||||||
if (!isHarmlessEmitter) {
|
if (!isHarmlessEmitter) {
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
|
|
@ -5015,7 +5074,34 @@ export function tickProjectiles(
|
||||||
|
|
||||||
if (!projectile.pierce) {
|
if (!projectile.pierce) {
|
||||||
struck = true
|
struck = true
|
||||||
if (isBlizzardShard(projectile.missileType)) {
|
if (projectile.missileType === 'glacialspike') {
|
||||||
|
spawned.push({
|
||||||
|
skillId: projectile.skillId,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
vx: 0,
|
||||||
|
vy: 0,
|
||||||
|
damage: 0,
|
||||||
|
ttl: 15,
|
||||||
|
fromPlayer: projectile.fromPlayer,
|
||||||
|
missileType: 'freezeexplode',
|
||||||
|
aoeRadius: projectile.aoeRadius ?? 64,
|
||||||
|
pierce: true,
|
||||||
|
})
|
||||||
|
spawned.push({
|
||||||
|
skillId: projectile.skillId,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
vx: 0,
|
||||||
|
vy: 0,
|
||||||
|
damage: 0,
|
||||||
|
ttl: 15,
|
||||||
|
fromPlayer: projectile.fromPlayer,
|
||||||
|
missileType: 'glacialspikeejecta',
|
||||||
|
aoeRadius: projectile.aoeRadius ?? 64,
|
||||||
|
pierce: true,
|
||||||
|
})
|
||||||
|
} else if (isBlizzardShard(projectile.missileType)) {
|
||||||
const expType = getBlizzardExplosionMissile(projectile.missileType!)
|
const expType = getBlizzardExplosionMissile(projectile.missileType!)
|
||||||
spawned.push({
|
spawned.push({
|
||||||
skillId: projectile.skillId,
|
skillId: projectile.skillId,
|
||||||
|
|
|
||||||
|
|
@ -5090,6 +5090,21 @@ export function drawExplosion(
|
||||||
if (explosionArt !== undefined) {
|
if (explosionArt !== undefined) {
|
||||||
const frameCount = Math.max(1, explosionArt.meta.framesPerDirection)
|
const frameCount = Math.max(1, explosionArt.meta.framesPerDirection)
|
||||||
const frameIndex = Math.min(Math.max(0, exp.frame), frameCount - 1)
|
const frameIndex = Math.min(Math.max(0, exp.frame), frameCount - 1)
|
||||||
|
if (explosionArt.meta.directions > 1) {
|
||||||
|
for (let dir = 0; dir < explosionArt.meta.directions; dir++) {
|
||||||
|
const group = explosionArt.frames[dir]
|
||||||
|
const frame = group?.[frameIndex] ?? group?.[0]
|
||||||
|
if (frame !== undefined) {
|
||||||
|
const drawX = frame.anchorX !== undefined ? exp.x + frame.anchorX : exp.x - frame.width / 2
|
||||||
|
const drawY = frame.anchorY !== undefined ? exp.y + frame.anchorY : exp.y - frame.height / 2
|
||||||
|
renderer.draw(frame, drawX, drawY, {
|
||||||
|
atlas: explosionArt.handle,
|
||||||
|
blendMode: 'additive',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
const group = explosionArt.frames[0]
|
const group = explosionArt.frames[0]
|
||||||
const frame = group?.[frameIndex] ?? group?.[0]
|
const frame = group?.[frameIndex] ?? group?.[0]
|
||||||
if (frame !== undefined) {
|
if (frame !== undefined) {
|
||||||
|
|
@ -6382,7 +6397,9 @@ if (typeof window !== 'undefined') {
|
||||||
const clip = art.clips['nu'] ?? art.clips['wl']
|
const clip = art.clips['nu'] ?? art.clips['wl']
|
||||||
if (clip !== undefined) animator.play('nu', clip)
|
if (clip !== undefined) animator.play('nu', clip)
|
||||||
}
|
}
|
||||||
animator.tick()
|
if (!monster.frozenTicks || monster.frozenTicks <= 0) {
|
||||||
|
animator.tick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const idx of monsterAnimators.keys()) {
|
for (const idx of monsterAnimators.keys()) {
|
||||||
|
|
@ -6770,6 +6787,7 @@ if (typeof window !== 'undefined') {
|
||||||
const drawY = frame.anchorY !== undefined
|
const drawY = frame.anchorY !== undefined
|
||||||
? monster.y + frame.anchorY
|
? monster.y + frame.anchorY
|
||||||
: monster.y - frame.height + FEET_HEIGHT / 2
|
: monster.y - frame.height + FEET_HEIGHT / 2
|
||||||
|
const isFrozen = monster.frozenTicks !== undefined && monster.frozenTicks > 0
|
||||||
const isChilled = monster.chillTicks !== undefined && monster.chillTicks > 0
|
const isChilled = monster.chillTicks !== undefined && monster.chillTicks > 0
|
||||||
const isAdditiveFallback = ADDITIVE_MONSTER_TOKENS.has(art.token.toUpperCase())
|
const isAdditiveFallback = ADDITIVE_MONSTER_TOKENS.has(art.token.toUpperCase())
|
||||||
const baseBlendMode = art.blendMode ?? (isAdditiveFallback ? 'additive' : 'normal')
|
const baseBlendMode = art.blendMode ?? (isAdditiveFallback ? 'additive' : 'normal')
|
||||||
|
|
@ -6777,7 +6795,11 @@ if (typeof window !== 'undefined') {
|
||||||
atlas: art.handle,
|
atlas: art.handle,
|
||||||
blendMode: baseBlendMode,
|
blendMode: baseBlendMode,
|
||||||
elevationY: Math.max(0, frame.height - 16),
|
elevationY: Math.max(0, frame.height - 16),
|
||||||
...(isChilled ? { tint: [0.4, 0.65, 1.0, 1.0] as const, paletteRow: 1 } : {}),
|
...(isFrozen
|
||||||
|
? { tint: [0.25, 0.6, 1.0, 1.0] as const, paletteRow: 1 }
|
||||||
|
: isChilled
|
||||||
|
? { tint: [0.4, 0.65, 1.0, 1.0] as const, paletteRow: 1 }
|
||||||
|
: {}),
|
||||||
})
|
})
|
||||||
if (art.glowGroups !== undefined) {
|
if (art.glowGroups !== undefined) {
|
||||||
const glowGroup = art.glowGroups[groupIndex]
|
const glowGroup = art.glowGroups[groupIndex]
|
||||||
|
|
@ -6798,8 +6820,13 @@ if (typeof window !== 'undefined') {
|
||||||
state.missingMonsterArt.push(missingId)
|
state.missingMonsterArt.push(missingId)
|
||||||
console.error(`monster art missing at draw time: ${missingId} (drawn as a red box)`)
|
console.error(`monster art missing at draw time: ${missingId} (drawn as a red box)`)
|
||||||
}
|
}
|
||||||
|
const isFrozen = monster.frozenTicks !== undefined && monster.frozenTicks > 0
|
||||||
const isChilled = monster.chillTicks !== undefined && monster.chillTicks > 0
|
const isChilled = monster.chillTicks !== undefined && monster.chillTicks > 0
|
||||||
const tint = isChilled ? ([0.4, 0.65, 1.0, 1.0] as const) : ([0.9, 0.2, 0.2, 1.0] as const)
|
const tint = isFrozen
|
||||||
|
? ([0.25, 0.6, 1.0, 1.0] as const)
|
||||||
|
: isChilled
|
||||||
|
? ([0.4, 0.65, 1.0, 1.0] as const)
|
||||||
|
: ([0.9, 0.2, 0.2, 1.0] as const)
|
||||||
renderer.drawSolid(monster.x - MARKER_WIDTH / 2, monster.y - MARKER_HEIGHT, MARKER_WIDTH, MARKER_HEIGHT, tint)
|
renderer.drawSolid(monster.x - MARKER_WIDTH / 2, monster.y - MARKER_HEIGHT, MARKER_WIDTH, MARKER_HEIGHT, tint)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -272,4 +272,33 @@ describe('Monster Rendering Dispatch in act-scene.ts (Milestone 2 / R2)', () =>
|
||||||
expect(renderer.drawCalls[0]!.options.blendMode).toBe('normal')
|
expect(renderer.drawCalls[0]!.options.blendMode).toBe('normal')
|
||||||
expect(renderer.drawCalls[1]!.options.blendMode).toBe('additive')
|
expect(renderer.drawCalls[1]!.options.blendMode).toBe('additive')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Case 8: Frozen monster applies ice blue tint [0.25, 0.6, 1.0, 1.0] and paletteRow: 1 to base body without polluting additive glow pass', () => {
|
||||||
|
const renderer = new MockRenderer()
|
||||||
|
const art = makeArt({ token: 'FR', blendMode: 'normal', hasGlow: true, glowFrameSize: [64, 64] })
|
||||||
|
const artMap = new Map<string, LoadedMonsterArt>([['frozen_fr', art]])
|
||||||
|
const monster: MonsterSimInput = {
|
||||||
|
index: 9,
|
||||||
|
state: 'chase',
|
||||||
|
corpseTicks: 0,
|
||||||
|
facing: 0,
|
||||||
|
x: 200,
|
||||||
|
y: 300,
|
||||||
|
stats: { id: 'frozen_fr' },
|
||||||
|
frozenTicks: 75,
|
||||||
|
}
|
||||||
|
|
||||||
|
executeMonsterRenderLoop([monster], artMap, new Map(), 0, renderer)
|
||||||
|
|
||||||
|
expect(renderer.drawCalls).toHaveLength(2)
|
||||||
|
const [baseCall, glowCall] = renderer.drawCalls
|
||||||
|
|
||||||
|
expect(baseCall!.options.paletteRow).toBe(1)
|
||||||
|
expect(baseCall!.options.tint).toEqual([0.25, 0.6, 1.0, 1.0])
|
||||||
|
expect(baseCall!.options.blendMode).toBe('normal')
|
||||||
|
|
||||||
|
expect(glowCall!.options.paletteRow).toBeUndefined()
|
||||||
|
expect(glowCall!.options.tint).toBeUndefined()
|
||||||
|
expect(glowCall!.options.blendMode).toBe('additive')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1170,8 +1170,12 @@ describe('Diablo II v1.13c Sorceress Cold & Fire Cross-Tree E2E Integration Stre
|
||||||
// Fire Wall struck 5 times (once per tick): 5 * 50 = 250 fire damage!
|
// Fire Wall struck 5 times (once per tick): 5 * 50 = 250 fire damage!
|
||||||
expect(totalFireDamage).toBe(250)
|
expect(totalFireDamage).toBe(250)
|
||||||
// Fire Wall patch is still burning (30 - 5 = 25 ttl)
|
// Fire Wall patch is still burning (30 - 5 = 25 ttl)
|
||||||
expect(projs).toHaveLength(1)
|
const fwPatches = projs.filter(p => p.missileType === 'firewall')
|
||||||
expect(projs[0]!.ttl).toBe(25)
|
expect(fwPatches).toHaveLength(1)
|
||||||
|
expect(fwPatches[0]!.ttl).toBe(25)
|
||||||
|
// Glacial Spike explosion & ejecta submissiles are active
|
||||||
|
expect(projs.some(p => p.missileType === 'freezeexplode')).toBe(true)
|
||||||
|
expect(projs.some(p => p.missileType === 'glacialspikeejecta')).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('verifies player running with Blaze active generates footstep patches while casting Glacial Spike with zero self-damage', () => {
|
it('verifies player running with Blaze active generates footstep patches while casting Glacial Spike with zero self-damage', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue