diff --git a/client/webapp/index.html b/client/webapp/index.html --- a/client/webapp/index.html +++ b/client/webapp/index.html @@ -141,6 +141,7 @@ + diff --git a/client/webapp/js/engine/combat_skills.js b/client/webapp/js/engine/combat_skills.js --- a/client/webapp/js/engine/combat_skills.js +++ b/client/webapp/js/engine/combat_skills.js @@ -161,9 +161,16 @@ const dist = Math.hypot(player.wx - target.wx, player.wy - target.wy); if (dist < 7.5) { - target.hp = Math.max(0, target.hp - dmg); + if (target.isDummy) { + target.hp = target.maxHp; + if (typeof window.TargetDummyTelemetry !== 'undefined') { + window.TargetDummyTelemetry.recordHit(dmg, isCrit, Date.now()); + } + } else { + target.hp = Math.max(0, target.hp - dmg); + target.hasAggro = true; // Retaliate! + } target.hurtTimer = 0.38; - target.hasAggro = true; // Retaliate! // Trigger visceral flinch animation on target if (typeof AnimationEngine !== 'undefined' && target.anim) { @@ -194,8 +201,10 @@ // Apply element-specific status reactions if (elem === 'fire') { target.burnTimer = 2.0; - target.knockbackX = (target.wx >= player.wx ? 0.35 : -0.35); - target.wx += target.knockbackX; + if (!target.isDummy) { + target.knockbackX = (target.wx >= player.wx ? 0.35 : -0.35); + target.wx += target.knockbackX; + } } else if (elem === 'thunder') { target.shockTimer = 1.8; triggerScreenShake(5.5, 0.2); @@ -215,10 +224,8 @@ const col = elem === 'fire' ? '#f97316' : (elem === 'thunder' ? '#fbbf24' : '#38bdf8'); spawnDamageText(prefix + dmg.toLocaleString(), mPos.x, mPos.y - 45, col, isCrit); - if (target.hp <= 0) { - if (typeof window.recordMonsterKillForQuests === 'function') { - window.recordMonsterKillForQuests(target.name); - } + if (target.hp <= 0 && !target.isDummy) { + if (typeof window.recordMonsterKillForQuests === 'function') window.recordMonsterKillForQuests(target.name); if (typeof MonsterLootDropper !== 'undefined') { MonsterLootDropper.spawnMonsterLoot(target); } else { diff --git a/client/webapp/js/engine/monster_loot_dropper.js b/client/webapp/js/engine/monster_loot_dropper.js --- a/client/webapp/js/engine/monster_loot_dropper.js +++ b/client/webapp/js/engine/monster_loot_dropper.js @@ -26,7 +26,7 @@ ]; function spawnMonsterLoot(monster) { - if (!monster || typeof groundDrops === 'undefined') return []; + if (!monster || monster.isDummy || typeof groundDrops === 'undefined') return []; const isBoss = !!(monster.isBoss || monster.maxHp >= 5000 || (monster.id && monster.id.includes('boss'))); const mobLevel = monster.level || 80; diff --git a/client/webapp/js/engine/monster_system.js b/client/webapp/js/engine/monster_system.js --- a/client/webapp/js/engine/monster_system.js +++ b/client/webapp/js/engine/monster_system.js @@ -165,6 +165,11 @@ function updateMonstersTick(dt) { if (typeof player === 'undefined') return; + // Target Dummy Telemetry tick + if (typeof window.TargetDummyTelemetry !== 'undefined' && typeof window.TargetDummyTelemetry.update === 'function') { + window.TargetDummyTelemetry.update(dt); + } + // Player potion cooldown if (player.potionCooldown > 0) { player.potionCooldown -= dt; @@ -197,6 +202,17 @@ for (let i = 0; i < activeMonsters.length; i++) { const m = activeMonsters[i]; + // PoE2 Strict Standard: Target Dummy is immortal & anchored to spawn origin (zero drift) + if (m.isDummy) { + m.hp = m.maxHp; + m.wx = m.originWx; + m.wy = m.originWy; + m.burnTimer = 0; + m.shockTimer = 0; + m.freezeTimer = 0; + continue; + } + // Handle dead monster respawn cycle if (m.hp <= 0) { m.respawnTimer += dt; @@ -220,13 +236,11 @@ if (m.shockTimer > 0) m.shockTimer -= dt; if (m.freezeTimer > 0) m.freezeTimer -= dt; if (m.staggerTimer > 0) { m.staggerTimer -= dt; + if (m.staggerTimer <= 0) m.poise = m.maxPoise; continue; // Stunned during stagger window! } if (m.attackCooldown > 0) m.attackCooldown -= dt; - - // Training Dummy does not move or attack - if (m.isDummy) continue; // Disengage if player is dead @@ -410,6 +424,14 @@ // QA Spawner helpers for testing in browser function spawnQAEnemy(type) { + const z = typeof currentZoneId !== 'undefined' ? currentZoneId : 'zone_boundless_sanctuary'; + const isSafe = (z === 'zone_player_hideout' || z === 'zone_boundless_sanctuary'); + if (isSafe && type !== 'dummy') { + if (typeof showNoticeModal === 'function') { + showNoticeModal('🛡️ Khu Vực An Toàn (Safe Haven)\n\nKhông thể triệu hồi quái vật thù địch tại đây theo chuẩn PoE2!\nChỉ được phép triệu hồi Mộc Nhân Thử Chiêu.'); + } + return; + } const spawnWx = player.wx + (player.facing * 1.8); const spawnWy = player.wy; @@ -455,6 +477,11 @@ // Direct spawner by key for console & QA function spawnSpecificMonster(key, wx, wy) { + const z = typeof currentZoneId !== 'undefined' ? currentZoneId : 'zone_boundless_sanctuary'; + if ((z === 'zone_player_hideout' || z === 'zone_boundless_sanctuary') && !key.startsWith('dummy_')) { + console.warn(`[MonsterSystem] Cannot spawn hostile monster (${key}) in safe haven: ${z}`); + return null; + } const tmpl = MONSTER_TEMPLATES[key]; if (!tmpl) { console.warn(`[MonsterSystem] Unknown monster template key: ${key}`); diff --git a/client/webapp/js/ui/hideout_gates.js b/client/webapp/js/ui/hideout_gates.js --- a/client/webapp/js/ui/hideout_gates.js +++ b/client/webapp/js/ui/hideout_gates.js @@ -131,6 +131,10 @@ } else { safeShowNotice(`🚪 [BƯỚC VÀO TÀN ĐỒ HOANG VỰC]\n\nĐã bước qua cổng Tàn Đồ Bậc ${hideoutState.mapDevice.tier}!\nTiêu hao 1 cổng. Hiện còn: ${hideoutState.mapDevice.portalsRemaining}/6 Cổng khả dụng.`); } closeHideoutModal(); + const targetZone = hideoutState.mapDevice.targetZoneId || 'zone_tang_kiem_nhai'; + if (typeof window.teleportToZone === 'function') { + window.teleportToZone(targetZone); + } } export function enterWorldGate() { diff --git a/client/webapp/js/ui/worldmap_dungeons.js b/client/webapp/js/ui/worldmap_dungeons.js --- a/client/webapp/js/ui/worldmap_dungeons.js +++ b/client/webapp/js/ui/worldmap_dungeons.js @@ -116,6 +116,21 @@ if (typeof window.playAudioFx === 'function') window.playAudioFx('dodge'); if (typeof window.triggerHaptic === 'function') window.triggerHaptic([60, 40, 80]); + // PoE2 Standard: Flush active ground drops, flying projectiles, and monster entities + if (typeof groundDrops !== 'undefined') groundDrops.length = 0; + if (typeof window.groundDrops !== 'undefined') window.groundDrops.length = 0; + if (typeof vfx_slashes !== 'undefined') vfx_slashes.length = 0; + if (typeof window.vfx_slashes !== 'undefined') window.vfx_slashes.length = 0; + if (typeof vfx_lightnings !== 'undefined') vfx_lightnings.length = 0; + if (typeof window.vfx_lightnings !== 'undefined') window.vfx_lightnings.length = 0; + if (typeof vfx_spikes !== 'undefined') vfx_spikes.length = 0; + if (typeof window.vfx_spikes !== 'undefined') window.vfx_spikes.length = 0; + if (typeof damageTexts !== 'undefined') damageTexts.length = 0; + if (typeof window.damageTexts !== 'undefined') window.damageTexts.length = 0; + if (window.TargetDummyTelemetry && typeof window.TargetDummyTelemetry.reset === 'function') { + window.TargetDummyTelemetry.reset(); + } + // PoE2 Standard: Flush & refresh monster ecosystem matching target zone if (typeof window.initZoneMonsters === 'function') { window.initZoneMonsters(zoneId);