Code edit (9 edits merged)
Please save this source code
User prompt
Please fix the bug: 'Uncaught ReferenceError: statusDebugText is not defined' in or related to this line: 'if (statusDebugText) {' Line Number: 5188
User prompt
Please fix the bug: 'Uncaught ReferenceError: statusDebugText is not defined' in or related to this line: 'if (statusDebugText) {' Line Number: 5187
Code edit (1 edits merged)
Please save this source code
Code edit (11 edits merged)
Please save this source code
User prompt
AVA, we moved the status effect duration decrement logic (using self.activeDurationTimers[effectId].framesRemaining--) inside the specific update methods for TankEnemy and Enemy instances. However, the duration for the CONFUSE effect still fails to decrement on these enemies, while the same pattern for self.activeSkillCooldowns[skillName]-- does work inside Hero.update. What specific LK Engine difference explains why decrementing a numeric property within the instance's own update method works for the Hero but fails for TankEnemy and Enemy instances created via BaseEnemy.expand?
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
User prompt
Please fix the bug: 'menuContainer is not defined' in or related to this line: 'game.addChild(menuContainer); // For popups (skill tree, skill details)' Line Number: 3578
Code edit (2 edits merged)
Please save this source code
User prompt
Try again, the hero is remaining cloaked for longer than it should be.
User prompt
Please provide the specific, recommended LK Engine code pattern or function calls necessary within BaseEntity.prototype.updateStatusEffects to reliably: Iterate active status effects (using activeStatusEffects array and effectDurations map). Decrement the numeric duration value associated with each effect's ID in the hero.effectDurations map each frame. Detect when a duration reaches zero. Trigger the removal of the effect (calling removeStatusEffect, which handles cleanup). Reliably update the hero's visual properties (like alpha) based on the current presence or absence of effects like CLOAKED after updates and removals for the frame. Assume hero.effectDurations is a map like { effectId: numericFramesRemaining } and hero.activeStatusEffects is an array of objects like { id: effectId, type: 'typeName', ... }. Focus on the correct LK Engine syntax or methods for the decrement and state update part within the loop.
Code edit (2 edits merged)
Please save this source code
User prompt
but that didn't fix it.
Code edit (9 edits merged)
Please save this source code
User prompt
OH! it looks like we are accidently confusing duration and cooldown. You see, the cooldown changes to 50seconds and the "duration" should be how long the hero is cloaked for before going back to no tint/uncloaked which should be more like 3 seconds if it has 3 skill points in it.
User prompt
The hero is still staying cloaked after longer than 3 seconds when he should be uncloaked by then.
User prompt
Make it so when my Nightfall Veil activates it only lasts for the specified duration and then turns off.
User prompt
Make it so when my Nightfall Veil activates it only lasts for the specified duration and then turns off.
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'call')' in or related to this line: 'BaseEntity.prototype.update.call(self); // <<< CALL PARENT UPDATE FIRST (for modifiers)' Line Number: 564
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'call')' in or related to this line: 'BaseEntity.prototype.update.call(self); // <<< CALL PARENT UPDATE FIRST (for modifiers)' Line Number: 564
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'call')' in or related to this line: 'BaseEntity.prototype.update.call(self); // <<< CALL PARENT UPDATE FIRST (for modifiers)' Line Number: 564
/**** * Classes ****/ // AimingJoystick class var AimingJoystick = Container.expand(function () { var self = Container.call(this); var joystickBackground = self.attachAsset('joystickBackground', { anchorX: 0.5, anchorY: 0.5 }); var joystickHandle = self.attachAsset('joystickHandle', { anchorX: 0.5, anchorY: 0.5 }); self.x = roomWidth - joystickBackground.width / 2 - 100; // Use roomWidth self.y = roomHeight - joystickBackground.height / 2 - 100; // Use roomHeight var maxRadius = 100; var isDragging = false; self.down = function (x, y, obj) { isDragging = true; }; self.up = function (x, y, obj) { isDragging = false; joystickHandle.x = 0; joystickHandle.y = 0; }; self.move = function (x, y, obj) { if (isDragging && !isPopupActive && !isSkillDetailPopupActive) { // Check both flags var localPos = self.toLocal(obj.global); var dx = localPos.x; var dy = localPos.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > maxRadius) { var angle = Math.atan2(dy, dx); dx = maxRadius * Math.cos(angle); dy = maxRadius * Math.sin(angle); } joystickHandle.x = dx; joystickHandle.y = dy; } }; self.getDirection = function () { var magnitude = Math.sqrt(joystickHandle.x * joystickHandle.x + joystickHandle.y * joystickHandle.y); if (magnitude > 0) { return { x: joystickHandle.x / magnitude, y: joystickHandle.y / magnitude }; } return { x: 0, y: 0 }; }; self.getRotation = function () { if (joystickHandle.x === 0 && joystickHandle.y === 0) { return null; } // No direction return Math.atan2(joystickHandle.y, joystickHandle.x); }; self.hasInput = function () { return joystickHandle.x !== 0 || joystickHandle.y !== 0; }; return self; }); // --- Arrow Class (Modified) --- var Arrow = Container.expand(function () { var self = Container.call(this); self.attachAsset('arrow', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 15; self.baseDamage = 5; // Base damage before hero bonuses self.damageType = DamageType.PHYSICAL; // Default damage type for arrows self.critChance = 0; // Inherit from hero later self.critDamage = 1.5; // Inherit from hero later self.update = function () { self.x += Math.cos(self.rotation) * self.speed; self.y += Math.sin(self.rotation) * self.speed; // Check bounds if (self.x < 0 || self.x > roomWidth || self.y < 0 || self.y > roomHeight) { self.destroy(); } }; // Overload destroy to ensure removal from array var originalDestroy = self.destroy; self.destroy = function () { var index = arrows.indexOf(self); if (index > -1) { arrows.splice(index, 1); } if (originalDestroy) { originalDestroy.call(self); } else if (self.parent) { self.parent.removeChild(self); } }; return self; }); // BackButton class (for skill tree navigation) var BackButton = Container.expand(function () { var self = Container.call(this); self.attachAsset('backButton', { anchorX: 0.5, anchorY: 0.5, scaleX: 2, scaleY: 2 }); self.x = 150; self.y = 350; // Position might need adjustment depending on layout self.down = function () { if (self.parent) { menuContainer.removeChild(self.parent); // Remove the specific tree popup } menuContainer.addChild(skillTreePopup); // Add main selection screen back isPopupActive = true; // Main tree is still a popup isSkillDetailPopupActive = false; // Ensure detail flag is off }; return self; }); // Contains shared stats logic // --- Base Entity Class (Refactored with Methods on Prototype) --- var BaseEntity = Container.expand(function () { var self = Container.call(this); // Constructor: ONLY initialize properties here // --- Core Stats --- self.health = 100; self.maxHealth = 100; self.energy = 50; self.maxEnergy = 50; self.energyRegen = 0.5; self.armor = 10; self.magicResist = 5; self.movementSpeed = 3; self.attackSpeed = 1.0; self.damageBonus = 0; self.critChance = 0.05; self.critDamage = 1.5; self.dodgeChance = 0.0; self.lifeSteal = 0.0; self.thorns = 0; self.skillCooldownReduction = 0.0; // --- Resistances --- self.fireResist = 0.0; self.iceResist = 0.0; self.lightningResist = 0.0; self.poisonResist = 0.0; self.shadowResist = 0.0; self.chaosResist = 0.0; // --- State Tracking --- self.activeModifiers = []; // Stores { id, stat, value, duration, isPercentage, source } self.activeStatusEffects = []; // Stores { id, type, source, duration, magnitude, ticksRemaining, tickInterval, stackingType, data } // ---> ADD Duration Timers to BaseEntity <--- self.active = true; // Entities start active return self; }); // --- BaseEntity Prototype Methods --- // Update (Handles Modifiers and Status Effects) // --- Hero Class --- var Hero = BaseEntity.expand(function () { var self = BaseEntity.call(this); // Graphics Setup var heroGraphics = self.attachAsset('hero', { anchorX: 0.5, anchorY: 0.5 }); self.swordGraphics = heroGraphics.addChild(LK.getAsset('sword', { anchorX: 0.5, anchorY: 0.5 })); self.swordGraphics.x = heroGraphics.width / 2 + 20; self.swordGraphics.y = 0; self.bowGraphics = self.attachAsset('bow', { anchorX: 0.5, anchorY: 0.5 }); self.bowGraphics.x = heroGraphics.width / 2; self.bowGraphics.visible = false; // --- Hero Specific Stats & Properties (Initialize from Base Constants) --- self.health = HERO_BASE_HEALTH; self.maxHealth = HERO_BASE_HEALTH; self.energy = HERO_BASE_ENERGY; self.maxEnergy = HERO_BASE_ENERGY; self.energyRegen = HERO_BASE_ENERGY_REGEN; self.armor = HERO_BASE_ARMOR; self.magicResist = HERO_BASE_MAGIC_RESIST; self.movementSpeed = HERO_BASE_MOVE_SPEED; self.attackSpeed = HERO_BASE_ATTACK_SPEED; self.damageBonus = HERO_BASE_DAMAGE_BONUS; self.critChance = HERO_BASE_CRIT_CHANCE; self.critDamage = HERO_BASE_CRIT_DAMAGE; self.dodgeChance = HERO_BASE_DODGE_CHANCE; self.lifeSteal = HERO_BASE_LIFE_STEAL; self.thorns = HERO_BASE_THORNS; self.skillCooldownReduction = HERO_BASE_CDR; // Base Resistances self.fireResist = HERO_BASE_FIRE_RESIST; self.iceResist = HERO_BASE_ICE_RESIST; self.lightningResist = HERO_BASE_LIGHTNING_RESIST; self.poisonResist = HERO_BASE_POISON_RESIST; self.shadowResist = HERO_BASE_SHADOW_RESIST; self.chaosResist = HERO_BASE_CHAOS_RESIST; // Combat State self.attackCooldown = 0; self.isAttacking = false; self.attackDuration = 20; self.attackDurationCounter = 0; self.weaponType = 1; self.swordBaseDamage = 10; self.swordRange = 75; self.swordAttackSpeedFrames = 45; self.bowBaseDamage = 8; self.bowAttackSpeedFrames = 30; // References & Flags self.joystick = null; self.aimingJoystick = null; self.facingDirection = 0; self.canExitRoom = false; // ***** ADD ACTIVE SKILL COOLDOWN TRACKING ***** self.activeSkillCooldowns = {}; // Stores { skillName: framesRemaining } self.activeFamiliar = null; // ***** END ADDED TRACKING ***** // Recalculate Stats (Now implemented) self.recalculateStats = function () { // 1. Reset stats to base values self.maxHealth = HERO_BASE_HEALTH; self.maxEnergy = HERO_BASE_ENERGY; self.energyRegen = HERO_BASE_ENERGY_REGEN; self.armor = HERO_BASE_ARMOR; self.magicResist = HERO_BASE_MAGIC_RESIST; self.movementSpeed = HERO_BASE_MOVE_SPEED; self.attackSpeed = HERO_BASE_ATTACK_SPEED; // Base is 1.0 multiplier self.damageBonus = HERO_BASE_DAMAGE_BONUS; self.critChance = HERO_BASE_CRIT_CHANCE; self.critDamage = HERO_BASE_CRIT_DAMAGE; self.dodgeChance = HERO_BASE_DODGE_CHANCE; self.lifeSteal = HERO_BASE_LIFE_STEAL; self.thorns = HERO_BASE_THORNS; self.skillCooldownReduction = HERO_BASE_CDR; self.fireResist = HERO_BASE_FIRE_RESIST; self.iceResist = HERO_BASE_ICE_RESIST; self.lightningResist = HERO_BASE_LIGHTNING_RESIST; self.poisonResist = HERO_BASE_POISON_RESIST; self.shadowResist = HERO_BASE_SHADOW_RESIST; self.chaosResist = HERO_BASE_CHAOS_RESIST; self.thornsPercent = 0.0; // Reset thorns/reflect self.damageReduction = 0.0; // Reset damage reduction self.bonusElementalDamagePercent = 0.0; // Reset ele damage bonus self.bonusChromaticDamagePercent = 0.0; // Reset chromatic bonus self.bonusRedDamagePercent = 0.0; // Reset red radiance bonus self.igniteChanceOnHit = 0.0; self.bleedChanceOnHit = 0.0; // etc... // ***** ADD PLACEHOLDERS FOR STATUS EFFECT STATS ***** var HERO_BASE_IGNITE_CHANCE = 0.0; // Base chance is 0% var HERO_BASE_IGNITE_DAMAGE = 10; // Example base DoT damage per second var HERO_BASE_BLEED_CHANCE = 0.0; // Base chance is 0% var HERO_BASE_BLEED_DAMAGE = 12; // Example base DoT damage per second var HERO_BASE_CHAOTIC_SPARK_CHANCE = 0.0; var HERO_BASE_ELEMENTAL_INFUSION_CHANCE = 0.0; var HERO_BASE_ENTROPY_PROC = 0.0; var HERO_BASE_UNSTABLE_RIFT_CHANCE = 0.0; var HERO_BASE_ENTROPY_REFLECT = 0.0; var HERO_BASE_ENTROPY_BLOCKS = 0.0; self.igniteChanceOnHit = 0.0; // Base chance is 0 self.igniteDamagePerSec = 10; // Base damage (example) self.bleedChanceOnHit = 0.0; self.bleedDamagePerSec = 12; // Base damage (example) self.chaoticSparkChance = 0.0; // Chance for random elemental effect on hit self.elementalInfusionChance = 0.0; // Chance for random elemental effect on hit (Forge) self.unstableRiftChance = 0.0; // Chance for random debuff on hit self.entropyShieldProcChance = 0.0; // Chance to block/reflect when hit self.entropyShieldReflectPercent = 0.0; self.entropyShieldBlockCount = 0; self.umbralEchoesChance = 0.0; // Add others as needed (e.g., self.chillChanceOnHit) // ***** END PLACEHOLDERS ***** // 2. Calculate and apply bonuses from skills var skillBonusCritChance = 0; var skillBonusMoveSpeedPercent = 0; // Store as percentage to add to base multiplier later var skillBonusAttackSpeedPercent = 0; // Store as percentage var skillBonusEnergyRegenPercent = 0; // Store as percentage var skillBonusDodgeChance = 0; var skillBonusElementalCritChance = 0; var skillBonusElementalDamagePercent = 0; var skillBonusMeleeAttackSpeedPercent = 0; var skillBonusDamageReductionPercent = 0; // Spirit Guidance var skillBonusIgniteChance = 0; // Red Radiance var skillBonusRedDamagePercent = 0; // Red Radiance var skillBonusBleedChance = 0; // Need this for Serrated Shade var skillBonusBleedDPS = 0; // Need this for Serrated Shade var skillBonusChaoticSpark = 0; var skillBonusElementalInfusion = 0; var skillBonusUnstableRift = 0; var skillBonusEntropyShieldProc = 0; var skillBonusEntropyShieldReflect = 0; var skillBonusEntropyShieldBlocks = 0; var skillBonusArmorPercent = 0; // Blue Bulwark var skillBonusMagicResistPercent = 0; // Blue Bulwark var skillBonusReflectPercent = 0; // Indigo Insight var skillBonusChromaticDamagePercent = 0; // Chromatic Fusion var skillBonusEvadePercent = 0; // Whispered Steps (treat as Dodge for now) var skillBonusElementalDamagePercent = 0; var skillBonusChromaticDamagePercent = 0; var skillBonusRedDamagePercent = 0; var skillBonusUmbralEchoesProc = 0; // Iterate through ALL skill definitions to find relevant passives // This is less efficient than knowing which skills are passive, but simpler for now. for (var skillName in skillDefinitions) { if (skillDefinitions.hasOwnProperty(skillName)) { var skillData = skillDefinitions[skillName]; var points = skillData.points; if (points <= 0) { continue; } // Skip unallocated skills var calc = skillData.calcData; // Apply specific skill bonuses based on skillName switch (skillName) { // --- Circuit of Ascension --- case 'Data Spike': for (var i = 0; i < points; i++) { skillBonusCritChance += calc.increases[i] / 100; } // Convert % to decimal break; case 'Reflex Accelerator': for (var i = 0; i < points; i++) { skillBonusMoveSpeedPercent += calc.increases[i]; } break; case 'Overclock Core': for (var i = 0; i < points; i++) { skillBonusAttackSpeedPercent += calc.attackSpeedIncreases[i]; skillBonusEnergyRegenPercent += calc.energyRegenIncreases[i]; } break; case 'Dodge Matrix': for (var i = 0; i < points; i++) { skillBonusDodgeChance += calc.increases[i] / 100; } break; // --- Echoes of Ancestry --- case 'Ancient Wisdom': // Elemental Crit // Need a separate stat? Or add to general crit? Let's assume separate for now. for (var i = 0; i < points; i++) { skillBonusElementalCritChance += calc.increases[i] / 100; } break; case 'Elemental Mastery': for (var i = 0; i < points; i++) { skillBonusElementalDamagePercent += calc.increases[i]; } break; case 'Warrior Spirit': // Melee Attack Speed for (var i = 0; i < points; i++) { skillBonusMeleeAttackSpeedPercent += calc.increases[i]; } break; case 'Spirit Guidance': for (var i = 0; i < points; i++) { skillBonusDamageReductionPercent += calc.reductions[i] / 100; } break; // Herbal Remedies is regen - handle in update loop maybe? Or add HP5 stat? Let's skip for now. // --- Forge of Possibilities --- case 'Adaptive Construction': // Adds Fire Damage & Ignite Chance // Note: Fire damage bonus calculation needs to be added if not already there // This skill's ignite chance adds to the base ignite chance var acIgnite = 0; if (points > 0) { acIgnite = points * calc.ignitePerRank / 100; } skillBonusIgniteChance += acIgnite; // Also add fire damage bonus % here later break; case 'Elemental Infusion': var eiProc = 0; // Use the correct scaling from its calcData: procs: [10, 8, 6, 4, 2] for (var i = 0; i < points; i++) { // Sum the increases eiProc += calc.procs[i]; } skillBonusElementalInfusion = eiProc / 100; // Convert total % to decimal break; ; // Adaptive Construction adds fire dmg/ignite - handle in attack logic // Elemental Infusion adds proc - handle in attack logic // Modular Enhancement adds proc - handle in attack logic // Resourceful Salvage adds temp buff - handle on kill // Hybrid Crafting adds effects - handle in attack logic // Runic Slots adds drops - handle on kill // Masterwork Creation is active buff // --- Prism of Potential --- case 'Red Radiance': for (var i = 0; i < points; i++) { skillBonusIgniteChance += calc.igniteIncreases[i] / 100; skillBonusRedDamagePercent += calc.dmgIncreases[i]; } break; case 'Blue Bulwark': if (points > 0) { skillBonusArmorPercent += calc.defValues[points - 1]; skillBonusMagicResistPercent += calc.defValues[points - 1]; } // Block chance handled separately? break; // Green Growth is regen/proc - skip for now case 'Yellow Zephyr': for (var i = 0; i < points; i++) { skillBonusMoveSpeedPercent += calc.moveIncreases[i]; skillBonusAttackSpeedPercent += calc.atkIncreases[i]; } break; case 'Indigo Insight': for (var i = 0; i < points; i++) { skillBonusCritChance += calc.critIncreases[i] / 100; skillBonusReflectPercent += calc.reflectIncreases[i]; } break; case 'Chromatic Fusion': for (var i = 0; i < points; i++) { skillBonusChromaticDamagePercent += calc.dmgIncreases[i]; } break; // Prismatic Ascendance is active buff // --- Nexus of Chaos --- case 'Chaotic Spark': var csProc = 0; for (var i = 0; i < points; i++) { csProc += calc.procIncreases[i]; } skillBonusChaoticSpark = csProc / 100; break; case 'Entropy Shield': if (points > 0) { // Using the formula from descriptionFn logic: base + (ranks-1)*perRank skillBonusEntropyShieldProc = (calc.baseProc + (points - 1) * calc.procPerRank) / 100; skillBonusEntropyShieldReflect = (calc.baseReflect + (points - 1) * calc.reflectPerRank) / 100; skillBonusEntropyShieldBlocks = calc.blockCounts[points - 1]; } case 'Cascade of Disorder': if (points > 0) { skillBonusCascadeProc = (calc.baseProc + (points - 1) * calc.procPerRank) / 100; } break; break; case 'Unstable Rift': if (points > 0) { // Use value at current rank: base + (points - 1) * perRank skillBonusUnstableRift = (calc.baseProc + (points - 1) * calc.procPerRank) / 100; } break; // Mostly procs/actives // --- Symphony of Shadows --- case 'Whispered Steps': for (var i = 0; i < points; i++) { skillBonusEvadePercent += calc.procIncreases[i] / 100; } break; case 'Serrated Shade': if (points > 0) { skillBonusBleedChance = calc.procValues[points - 1] / 100; // Use value at current rank skillBonusBleedDPS = calc.dpsValues[points - 1]; // Use value at current rank } break; case 'Umbral Echoes': if (points > 0) { // Use value at current rank from calcData: procValues: [10, 13, 16, 19, 22] skillBonusUmbralEchoesProc = calc.procValues[points - 1] / 100; } break; case 'Shadow Familiar': success = executeShadowFamiliar(skillData); break; // Others are mostly procs/actives/summons } } } // 3. Apply calculated bonuses to instance stats // Note: For percentage bonuses, we modify the base multiplier (usually 1.0 for speed/atk speed) // or add directly for stats like crit/dodge chance. self.critChance += skillBonusCritChance + skillBonusElementalCritChance; // Combine crits for now? Or keep separate? self.dodgeChance += skillBonusDodgeChance + skillBonusEvadePercent; // Combine dodge/evade // Apply % speed bonuses multiplicatively to the base multiplier self.movementSpeed *= 1 + skillBonusMoveSpeedPercent / 100; // Combine general attack speed and melee attack speed for now (apply only if melee weapon equipped later?) self.attackSpeed *= 1 + (skillBonusAttackSpeedPercent + skillBonusMeleeAttackSpeedPercent) / 100; // Apply % energy regen bonuses self.energyRegen *= 1 + skillBonusEnergyRegenPercent / 100; // Apply % damage reduction (Need to modify takeDamage later) // self.damageReduction = skillBonusDamageReductionPercent; // Store it // Apply % Armor/MR bonuses (additively to base value for now, could be multiplicative) self.armor += HERO_BASE_ARMOR * (skillBonusArmorPercent / 100); self.magicResist += HERO_BASE_MAGIC_RESIST * (skillBonusMagicResistPercent / 100); self.thornsPercent = HERO_BASE_THORNS_PERCENT + skillBonusReflectPercent / 100; // Define base as 0 self.damageReduction = HERO_BASE_DAMAGE_REDUCTION + skillBonusDamageReductionPercent; // Define base as 0 self.bonusElementalDamagePercent = HERO_BASE_ELE_DMG_BONUS + skillBonusElementalDamagePercent; // Define base as 0 self.bonusChromaticDamagePercent = HERO_BASE_CHROM_DMG_BONUS + skillBonusChromaticDamagePercent; // Define base as 0 self.bonusRedDamagePercent = HERO_BASE_RED_DMG_BONUS + skillBonusRedDamagePercent; // Define base as 0 self.igniteChanceOnHit = HERO_BASE_IGNITE_CHANCE + skillBonusIgniteChance; // Define HERO_BASE_IGNITE_CHANCE = 0 if needed self.bleedChanceOnHit = HERO_BASE_BLEED_CHANCE + skillBonusBleedChance; // Define HERO_BASE_BLEED_CHANCE = 0 if needed self.bleedDamagePerSec = HERO_BASE_BLEED_DAMAGE + skillBonusBleedDPS; // Define HERO_BASE_BLEED_DAMAGE=12 if needed self.chaoticSparkChance = HERO_BASE_CHAOTIC_SPARK_CHANCE + skillBonusChaoticSpark; // Define base as 0 self.elementalInfusionChance = HERO_BASE_ELEMENTAL_INFUSION_CHANCE + skillBonusElementalInfusion; // Define base as 0 self.unstableRiftChance = HERO_BASE_UNSTABLE_RIFT_CHANCE + skillBonusUnstableRift; // Define base as 0 self.entropyShieldProcChance = HERO_BASE_ENTROPY_PROC + skillBonusEntropyShieldProc; // Define base as 0 self.entropyShieldReflectPercent = HERO_BASE_ENTROPY_REFLECT + skillBonusEntropyShieldReflect; // Define base as 0 self.entropyShieldBlockCount = HERO_BASE_ENTROPY_BLOCKS + skillBonusEntropyShieldBlocks; // Define base as 0 self.umbralEchoesChance = (HERO_BASE_UMBRAL_ECHO_CHANCE || 0.0) + skillBonusUmbralEchoesProc; // Define HERO_BASE_... = 0.0 if needed // --- Recalculate maxHealth/maxEnergy based on potential future skills --- // self.maxHealth = HERO_BASE_HEALTH * (1 + self.getStatModifierValue('maxHealthPercent', true)); // Example // self.maxEnergy = HERO_BASE_ENERGY * (1 + self.getStatModifierValue('maxEnergyPercent', true)); // Example // Clamp health/energy to new max values if they changed self.health = Math.min(self.health, self.maxHealth); self.energy = Math.min(self.energy, self.maxEnergy); }; // Die Method self.die = function (killer) { if (!self.active) { return; } self.active = false; isGameOver = true; LK.showGameOver(); stopRoomMusic(); }; // Update Method self.update = function () { if (!self.active) { return; } BaseEntity.prototype.update.call(self); // Run base updates (status effects, modifiers) // ***** ADD ACTIVE SKILL COOLDOWN DECREMENT ***** for (var skillName in self.activeSkillCooldowns) { if (self.activeSkillCooldowns.hasOwnProperty(skillName)) { if (self.activeSkillCooldowns[skillName] > 0) { self.activeSkillCooldowns[skillName]--; } } } // ***** END COOLDOWN DECREMENT ***** // Check if Stunned/Frozen var canAct = true; self.activeStatusEffects.forEach(function (effect) { if (effect.type === StatusEffectType.STUN || effect.type === StatusEffectType.FREEZE || effect.type === StatusEffectType.CONFUSE || effect.type === StatusEffectType.CHARM) { canAct = false; } }); // If stunned/frozen, graphics might change (e.g., show stun stars) // --- TODO: Add CC visual state --- // Movement (getStat handles speed reduction including CC making it 0) var moveDirection = { x: 0, y: 0 }; // Only process joystick if the entity *could* act (prevents weird facing direction changes while stunned) if (canAct && self.joystick) { moveDirection = self.joystick.getDirection(); } var currentMoveSpeed = self.getStat('movementSpeed'); var nextX = self.x + moveDirection.x * currentMoveSpeed; // Will be 0 if stunned/frozen var nextY = self.y + moveDirection.y * currentMoveSpeed; // Will be 0 if stunned/frozen // Wall Collision & Room Transition var onEntrance = false; if (nextX < wallThickness) { if (nextY >= entrances.left.yStart && nextY <= entrances.left.yEnd) { onEntrance = true; } else { nextX = wallThickness; } } else if (nextX > roomWidth - wallThickness) { if (nextY >= entrances.right.yStart && nextY <= entrances.right.yEnd) { onEntrance = true; } else { nextX = roomWidth - wallThickness; } } if (nextY < wallThickness) { if (nextX >= entrances.top.xStart && nextX <= entrances.top.xEnd) { onEntrance = true; } else { nextY = wallThickness; } } else if (nextY > roomHeight - wallThickness) { if (nextX >= entrances.bottom.xStart && nextX <= entrances.bottom.xEnd) { onEntrance = true; } else { nextY = roomHeight - wallThickness; } } self.x = nextX; self.y = nextY; if (self.canExitRoom && onEntrance) { var exitSide = ''; var entrySide = ''; if (self.y < wallThickness + 10) { exitSide = 'top'; entrySide = 'bottom'; } else if (self.y > roomHeight - wallThickness - 10) { exitSide = 'bottom'; entrySide = 'top'; } else if (self.x < wallThickness + 10) { exitSide = 'left'; entrySide = 'right'; } else if (self.x > roomWidth - wallThickness - 10) { exitSide = 'right'; entrySide = 'left'; } if (exitSide) { transitionToNewRoom(entrySide); return; } } // Rotation & Aiming (Only update rotation if able to act) var isMoving = moveDirection.x !== 0 || moveDirection.y !== 0; var isAiming = self.aimingJoystick && self.aimingJoystick.hasInput(); var aimAngle = isAiming ? self.aimingJoystick.getRotation() : null; if (isMoving && canAct) { self.facingDirection = Math.atan2(moveDirection.y, moveDirection.x); } if (isAiming && aimAngle !== null && canAct) { self.rotation = aimAngle; } else if (isMoving && canAct) { self.rotation = self.facingDirection; } // Update weapon graphics regardless of CC if (self.weaponType === 1) { self.swordGraphics.rotation = 0; } else { self.bowGraphics.rotation = 0; } // Attack Logic (Check canAct flag) if (canAct && !isPopupActive && !isSkillDetailPopupActive) { if (self.attackCooldown > 0) { self.attackCooldown--; } var attackAngle = isAiming && aimAngle !== null ? aimAngle : self.rotation; var currentAttackSpeedMult = self.getStat('attackSpeed'); // Sword if (self.weaponType === 1) { var swordCooldown = Math.round(self.swordAttackSpeedFrames / currentAttackSpeedMult); if (self.isAttacking) { if (self.attackDurationCounter > 0) { self.attackDurationCounter--; var swingProgress = (self.attackDuration - self.attackDurationCounter) / self.attackDuration; self.swordGraphics.rotation = Math.sin(swingProgress * Math.PI) * 1.2; if (swingProgress > 0.3 && swingProgress < 0.7 && self.attackDurationCounter % 5 === 0) { self.checkSwordHits(attackAngle); } } else { self.isAttacking = false; self.swordGraphics.rotation = 0; self.attackCooldown = swordCooldown; } } else if (self.attackCooldown <= 0 && isAiming) { self.isAttacking = true; self.attackDurationCounter = self.attackDuration; } } // Bow else if (self.weaponType === 2) { var bowCooldown = Math.round(self.bowAttackSpeedFrames / currentAttackSpeedMult); if (self.attackCooldown <= 0 && isAiming) { self.fireArrow(attackAngle); self.attackCooldown = bowCooldown; } } } else { // Reset attack state if stunned or popup active if (self.isAttacking) { self.isAttacking = false; self.swordGraphics.rotation = 0; self.attackDurationCounter = 0; } } }; // End of update function // Helper Methods self.checkSwordHits = function (attackAngle) { var swordDamageInfo = self.calculateOutgoingDamage(self.swordBaseDamage, DamageType.PHYSICAL); var hitEnemiesThisCheck = []; // Track enemies hit in this specific swing check to prevent multi-hits from one swing // Iterate through enemies in the current room for (var i = currentRoom.enemies.length - 1; i >= 0; i--) { var enemy = currentRoom.enemies[i]; // Basic validity checks if (!enemy || !enemy.active || hitEnemiesThisCheck.includes(enemy)) { continue; } // Calculate distance and angle to enemy var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distSqr = dx * dx + dy * dy; var rangeSqr = self.swordRange * self.swordRange; // Use squared range for efficiency // Check if enemy is within range if (distSqr < rangeSqr) { var angleToEnemy = Math.atan2(dy, dx); // Calculate angle difference relative to the sword swing direction var angleDifference = Math.atan2(Math.sin(angleToEnemy - attackAngle), Math.cos(angleToEnemy - attackAngle)); var swingArc = Math.PI / 3; // Example: 60-degree arc (PI/3 radians) // Check if enemy is within the swing arc if (Math.abs(angleDifference) < swingArc) { // Check if the enemy can actually take damage if (enemy.takeDamage) { // --- Apply Damage FIRST --- enemy.takeDamage(swordDamageInfo.damage, DamageType.PHYSICAL, self); hitEnemiesThisCheck.push(enemy); // Mark enemy as hit by this swing // --- Apply Lifesteal (Independent of Status Effects) --- var lifeStealAmount = swordDamageInfo.damage * self.getStat('lifeSteal'); if (lifeStealAmount > 0) { self.takeDamage(lifeStealAmount, DamageType.HEALING); } // --- APPLY ALL STATUS EFFECTS / PROCS (Now correctly placed) --- // Cascade of Disorder Check if (Math.random() < self.cascadeProcChance) { var points_CoD = skillDefinitions['Cascade of Disorder'].points; // Use distinct variable names var calc_CoD = skillDefinitions['Cascade of Disorder'].calcData; if (points_CoD > 0 && calc_CoD) { var dmgPercent_CoD = (calc_CoD.baseDmg + (points_CoD - 1) * calc_CoD.dmgPerRank) / 100; var radiusUnits_CoD = calc_CoD.baseRadius + (points_CoD - 1) * calc_CoD.radiusPerRank; var radiusPixels_CoD = radiusUnits_CoD * 50; // ADJUST unit-to-pixel conversion factor if needed executeCascadeExplosion(enemy.x, enemy.y, dmgPercent_CoD, radiusPixels_CoD, self); } } // Elemental Infusion Check if (Math.random() < self.elementalInfusionChance) { var randomEffectDataEI = getRandomElementalStatusData(self); if (randomEffectDataEI) { // Optional: Adjust duration for Elemental Infusion // randomEffectDataEI.duration = 3; enemy.applyStatusEffect(randomEffectDataEI); } } // Ignite Check if (Math.random() < self.igniteChanceOnHit) { enemy.applyStatusEffect({ type: StatusEffectType.IGNITE, source: self, duration: 5, magnitude: self.igniteDamagePerSec, tickInterval: 1, stackingType: 'Intensity' }); } // Chaotic Spark Check if (Math.random() < self.chaoticSparkChance) { var randomEffectDataCS = getRandomElementalStatusData(self); if (randomEffectDataCS) { randomEffectDataCS.duration = 2; // Override duration to 2 seconds enemy.applyStatusEffect(randomEffectDataCS); } } // Bleed Check (Corrected Duration to 5s) if (Math.random() < self.bleedChanceOnHit) { enemy.applyStatusEffect({ type: StatusEffectType.BLEED, source: self, duration: 5, // Use 5 seconds magnitude: self.bleedDamagePerSec, tickInterval: 1, stackingType: 'Intensity' }); } // Unstable Rift Check if (Math.random() < self.unstableRiftChance) { var randomDebuffData = getRandomDebuffData(self); if (randomDebuffData) { randomDebuffData.duration = 4; // Set duration to 4 seconds enemy.applyStatusEffect(randomDebuffData); } } // Umbral Echoes Check if (Math.random() < self.umbralEchoesChance) { var points_UE = skillDefinitions['Umbral Echoes'].points; var calc_UE = skillDefinitions['Umbral Echoes'].calcData; if (points_UE > 0 && calc_UE) { var confusionDur = calc_UE.confusionDurs[points_UE - 1]; // Spawn echo at the ENEMY's location spawnUmbralEcho(enemy.x, enemy.y, confusionDur, self); } } // --- Add other on-hit procs here --- } // End if (enemy.takeDamage) } // End if (angleDifference) } // End if (distSqr < rangeSqr) } // End FOR loop (enemies) }; // End self.checkSwordHits definition self.fireArrow = function (fireAngle) { if (self.weaponType !== 2) { return; } var arrow = new Arrow(); var bowDamageInfo = self.calculateOutgoingDamage(self.bowBaseDamage, DamageType.PHYSICAL); arrow.damage = bowDamageInfo.damage; arrow.damageType = DamageType.PHYSICAL; arrow.isCrit = bowDamageInfo.isCrit; arrow.critChance = self.getStat('critChance'); arrow.critDamage = self.getStat('critDamage'); arrow.source = self; arrow.x = self.x + Math.cos(fireAngle) * 30; arrow.y = self.y + Math.sin(fireAngle) * 30; arrow.rotation = fireAngle; arrows.push(arrow); gameContainer.addChild(arrow); }; self.switchWeapon = function () { self.weaponType = self.weaponType === 1 ? 2 : 1; self.swordGraphics.visible = self.weaponType === 1; self.bowGraphics.visible = self.weaponType === 2; self.isAttacking = false; self.attackDurationCounter = 0; self.attackCooldown = 0; }; return self; }); // --- Base Enemy Class --- var BaseEnemy = BaseEntity.expand(function () { var self = BaseEntity.call(this); // Default Enemy Stats self.health = 10; self.maxHealth = 10; self.armor = 5; self.magicResist = 0; self.movementSpeed = 1.0; self.damageBonus = 0; self.critChance = 0.0; self.critDamage = 1.5; self.dodgeChance = 0.0; // Enemy Specific Properties self.damageAmount = 5; self.damageCooldownTime = 60; self.damageCooldown = 0; // Shared Methods self.handleWallCollision = function () { var changed = false; if (self.x < wallThickness) { self.x = wallThickness; changed = true; } if (self.x > roomWidth - wallThickness) { self.x = roomWidth - wallThickness; changed = true; } if (self.y < wallThickness) { self.y = wallThickness; changed = true; } if (self.y > roomHeight - wallThickness) { self.y = roomHeight - wallThickness; changed = true; } return changed; }; self.moveTowards = function (targetX, targetY, moveSpeed) { var dx = targetX - self.x; var dy = targetY - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0.1) { self.x += dx / distance * moveSpeed; self.y += dy / distance * moveSpeed; self.rotation = Math.atan2(dy, dx); } return distance; }; // Die Method self.die = function (killer) { if (!self.active) { return; } self.active = false; // --- TODO: Add drop logic here --- if (currentRoom) { currentRoom.enemiesKilled++; currentRoom.enemyCounter--; updateRoomDisplay(); var index = currentRoom.enemies.indexOf(self); if (index > -1) { currentRoom.enemies.splice(index, 1); } checkRoomCleared(); } // Death Animation if (self.frameAssets) { self.frameAssets.forEach(function (frame) { frame.visible = false; }); } else if (self.graphics) { self.graphics.visible = false; } var deathVfx = LK.getAsset('box', { anchorX: 0.5, anchorY: 0.5, x: self.x, y: self.y, width: self.width || 50, height: self.height || 50, color: 0xFF0000, alpha: 0.8 }); gameContainer.addChild(deathVfx); var vfxDuration = 30; var frameCount = 0; var vfxInterval = LK.setInterval(function () { frameCount++; var progress = frameCount / vfxDuration; deathVfx.scaleX = 1 + progress * 1.5; deathVfx.scaleY = 1 + progress * 1.5; deathVfx.alpha = 0.8 * (1 - progress); if (frameCount >= vfxDuration) { LK.clearInterval(vfxInterval); if (deathVfx.parent) { deathVfx.parent.removeChild(deathVfx); } BaseEntity.prototype.die.call(self, killer); // Final removal call } }, 1000 / 60); }; // Default Update self.update = function () { // --- Initial Checks --- if (!self.active) { return; } // Don't update if inactive // We check hero existence within targeting logic now // --- Base Updates --- BaseEntity.prototype.update.call(self); // Runs BaseEntity update (duration timers, modifiers, DoTs, visual flags) // ---> ADD Duration Decrement/Removal Loop HERE <--- var effectIdsToRemove_ME = []; // Unique name if (self.activeDurationTimers) { for (var effectId_ME in self.activeDurationTimers) { if (self.activeDurationTimers.hasOwnProperty(effectId_ME)) { var timer_ME = self.activeDurationTimers[effectId_ME]; if (timer_ME && typeof timer_ME.framesRemaining === 'number') { timer_ME.framesRemaining--; if (timer_ME.framesRemaining <= 0) { effectIdsToRemove_ME.push(effectId_ME); } // --- Optional Debug Tint --- // var effectDataForTint_ME = self.activeStatusEffects.find(function(eff){ return eff && eff.id === effectId_ME; }); // if (effectDataForTint_ME && effectDataForTint_ME.type === StatusEffectType.CONFUSE) { /* ... change self.tint ... */ } // --- End Optional Debug --- } else { effectIdsToRemove_ME.push(effectId_ME); } } } } if (effectIdsToRemove_ME.length > 0) { for (var i_ME = 0; i_ME < effectIdsToRemove_ME.length; i_ME++) { self.removeStatusEffect(effectIdsToRemove_ME[i_ME]); } self.forceVisualRecalculation = true; } // --- END Status Effect Duration Logic for Melee Enemy --- if (self.damageCooldown > 0) { self.damageCooldown--; } // For enemies that use this cooldown // --- Check Status Effects --- var isConfused = self.activeStatusEffects.some(function (eff) { return eff && eff.type === StatusEffectType.CONFUSE; }); // Add checks for other disabling effects if needed (e.g., isStunned, isFrozen) // var isStunned = self.activeStatusEffects.some(function(eff){ return eff && eff.type === StatusEffectType.STUN; }); // if (isStunned || isFrozen) { /* Potentially skip AI entirely */ return; } // --- AI Behavior --- if (isConfused) { // --- Confused Behavior --- // Example: Wander randomly if (!self.wanderTarget || Math.random() < 0.02) { // Pick new wander target occasionally self.wanderTarget = { x: self.x + (Math.random() - 0.5) * 200, y: self.y + (Math.random() - 0.5) * 200 }; // Clamp wander target within walls if (self.wanderTarget.x < wallThickness) { self.wanderTarget.x = wallThickness; } if (self.wanderTarget.x > roomWidth - wallThickness) { self.wanderTarget.x = roomWidth - wallThickness; } if (self.wanderTarget.y < wallThickness) { self.wanderTarget.y = wallThickness; } if (self.wanderTarget.y > roomHeight - wallThickness) { self.wanderTarget.y = roomHeight - wallThickness; } } var currentMoveSpeed = self.getStat('movementSpeed'); // Still affected by slow etc. if (currentMoveSpeed > 0 && self.wanderTarget) { // Only move if able and target exists self.moveTowards(self.wanderTarget.x, self.wanderTarget.y, currentMoveSpeed * 0.6); // Move slower maybe? } self.rotation += 0.1; // Spin slowly? Indicate confusion visually // --- End Confused Behavior --- } else { // --- Normal (Non-Confused) Behavior --- var target = null; // Reset target decision // 1. Prioritize Decoys var closestDecoy = null; var minDistSqToDecoy = Infinity; if (activeDecoys && activeDecoys.length > 0) { // Check activeDecoys exists for (var i = 0; i < activeDecoys.length; i++) { var decoy = activeDecoys[i]; if (decoy) { // Check decoy exists var dx_d = decoy.x - self.x; var dy_d = decoy.y - self.y; var distSq_d = dx_d * dx_d + dy_d * dy_d; if (distSq_d < minDistSqToDecoy) { minDistSqToDecoy = distSq_d; closestDecoy = decoy; } } } if (closestDecoy) { // Check if a decoy was actually found target = closestDecoy; } } // 2. If no decoy targeted, check for VISIBLE hero if (!target && hero && hero.active) { // Check hero exists and is active var isHeroCloaked = hero.activeStatusEffects.some(function (effect) { return effect && effect.type === StatusEffectType.CLOAKED; // Check effect exists }); if (!isHeroCloaked) { target = hero; // Target hero only if active, not cloaked, and no decoy targeted } } // --- Act Based on Target --- if (target) { // Base Enemy doesn't have specific attack logic, just movement var currentMoveSpeed = self.getStat('movementSpeed'); if (currentMoveSpeed > 0) { // Only move if able // Move towards target's x/y (target can be hero or decoy) self.moveTowards(target.x, target.y, currentMoveSpeed); } else { // If speed is 0 (e.g., frozen), at least face the target var dx_t = target.x - self.x; var dy_t = target.y - self.y; if (dx_t !== 0 || dy_t !== 0) { // Avoid atan2(0,0) self.rotation = Math.atan2(dy_t, dx_t); } } } else { // No target (hero cloaked/inactive and no decoys) - Stand still or wander (optional) // For BaseEnemy, let's just stand still (do nothing) } // --- End Act Based on Target --- // --- End Normal Behavior --- } // --- Final Updates --- self.handleWallCollision(); // Always handle wall collision if (self.updateAnimation) { // Update animation if method exists (added in derived classes) self.updateAnimation(); } }; // --- End self.update for BaseEnemy --- // Animation Helpers self.setupAnimation = function (frameAssets) { self.frames = frameAssets; self.currentFrameIndex = 0; self.frameDelay = 30; self.frameCounter = 0; self.frameAssets = []; for (var i = 0; i < frameAssets.length; i++) { var frameAsset = self.attachAsset(frameAssets[i], { anchorX: 0.5, anchorY: 0.5 }); frameAsset.visible = i === 0; self.frameAssets.push(frameAsset); } if (self.frameAssets.length > 0) { self.graphics = self.frameAssets[0]; } }; self.updateAnimation = function () { if (!self.frames || self.frames.length === 0) { return; } self.frameCounter++; if (self.frameCounter >= self.frameDelay) { self.frameCounter = 0; if (self.frameAssets) { for (var i = 0; i < self.frameAssets.length; i++) { if (self.frameAssets[i]) { self.frameAssets[i].visible = false; } } self.currentFrameIndex = (self.currentFrameIndex + 1) % self.frames.length; if (self.frameAssets[self.currentFrameIndex]) { self.frameAssets[self.currentFrameIndex].visible = true; } } } }; return self; }); // --- TankEnemy Class --- var TankEnemy = BaseEnemy.expand(function () { var self = BaseEnemy.call(this); // Stat overrides self.health = 50; self.maxHealth = 50; self.armor = 20; self.magicResist = 10; self.movementSpeed = 0.8; self.damageAmount = 10; self.setupAnimation(['tank_run1', 'tank_run2', 'tank_run3', 'tank_run4']); self.minDistance = 80; // Update self.update = function () { // --- Initial Checks --- if (!self.active) { return; } // --- Base Updates --- BaseEntity.prototype.update.call(self); // Handles timers, modifiers, DoTs etc. // ---> COPY Duration Decrement/Removal Loop HERE <--- var effectIdsToRemove_TE = []; // Unique name if (self.activeDurationTimers) { for (var effectId_TE in self.activeDurationTimers) { if (self.activeDurationTimers.hasOwnProperty(effectId_TE)) { var timer_TE = self.activeDurationTimers[effectId_TE]; if (timer_TE && typeof timer_TE.framesRemaining === 'number') { timer_TE.framesRemaining--; if (timer_TE.framesRemaining <= 0) { effectIdsToRemove_TE.push(effectId_TE); } // --- Optional Debug Tint for Tank --- var effectDataForTint_TE = self.activeStatusEffects.find(function (eff) { return eff && eff.id === effectId_TE; }); if (effectDataForTint_TE && effectDataForTint_TE.type === StatusEffectType.CONFUSE) {/* ... change self.tint ... */} // --- End Optional Debug --- } else { effectIdsToRemove_TE.push(effectId_TE); } // Invalid timer } } } if (effectIdsToRemove_TE.length > 0) { for (var i_TE = 0; i_TE < effectIdsToRemove_TE.length; i_TE++) { self.removeStatusEffect(effectIdsToRemove_TE[i_TE]); } self.forceVisualRecalculation = true; // Trigger visual update check } // --- END Status Effect Duration Logic for TankEnemy --- if (self.damageCooldown > 0) { self.damageCooldown--; } self.updateAnimation(); // Update animation regardless of state // --- Check Status Effects --- var isConfused = self.activeStatusEffects.some(function (eff) { return eff && eff.type === StatusEffectType.CONFUSE; }); // Add isStunned/isFrozen checks if needed // --- AI Behavior --- if (isConfused) { // --- Confused Behavior --- if (!self.wanderTarget || Math.random() < 0.02) { self.wanderTarget = { x: self.x + (Math.random() - 0.5) * 200, y: self.y + (Math.random() - 0.5) * 200 }; // Clamp wander target if (self.wanderTarget.x < wallThickness) { self.wanderTarget.x = wallThickness; } if (self.wanderTarget.x > roomWidth - wallThickness) { self.wanderTarget.x = roomWidth - wallThickness; } if (self.wanderTarget.y < wallThickness) { self.wanderTarget.y = wallThickness; } if (self.wanderTarget.y > roomHeight - wallThickness) { self.wanderTarget.y = roomHeight - wallThickness; } } var currentMoveSpeedConfused = self.getStat('movementSpeed'); if (currentMoveSpeedConfused > 0 && self.wanderTarget) { self.moveTowards(self.wanderTarget.x, self.wanderTarget.y, currentMoveSpeedConfused * 0.6); } self.rotation += 0.1; // Spin slowly // --- End Confused Behavior --- } else { // --- Normal (Non-Confused) Behavior --- var target = null; // 1. Prioritize Decoys var closestDecoy = null; var minDistSqToDecoy = Infinity; if (activeDecoys && activeDecoys.length > 0) { for (var i = 0; i < activeDecoys.length; i++) { var decoy = activeDecoys[i]; if (decoy) { var dx_d = decoy.x - self.x; var dy_d = decoy.y - self.y; var distSq_d = dx_d * dx_d + dy_d * dy_d; if (distSq_d < minDistSqToDecoy) { minDistSqToDecoy = distSq_d; closestDecoy = decoy; } } } if (closestDecoy) { target = closestDecoy; } } // 2. If no decoy targeted, check for VISIBLE hero if (!target && hero && hero.active) { var isHeroCloaked = hero.activeStatusEffects.some(function (effect) { return effect && effect.type === StatusEffectType.CLOAKED; }); if (!isHeroCloaked) { target = hero; } } // --- Act Based on Target --- if (target) { // Tank has a target (decoy or hero) var targetX = target.x; var targetY = target.y; var distance = Math.sqrt(Math.pow(targetX - self.x, 2) + Math.pow(targetY - self.y, 2)); var currentMoveSpeedNormal = self.getStat('movementSpeed'); if (distance > self.minDistance) { // Move towards target if too far if (currentMoveSpeedNormal > 0) { self.moveTowards(targetX, targetY, currentMoveSpeedNormal); } else { // If speed is 0, face target if (targetX !== self.x || targetY !== self.y) { self.rotation = Math.atan2(targetY - self.y, targetX - self.x); } } } else { // In range, face target if (targetX !== self.x || targetY !== self.y) { self.rotation = Math.atan2(targetY - self.y, targetX - self.x); } // --- Attack Logic (Only attack HERO) --- if (target === hero) { // Check if the target IS the hero if (self.damageCooldown <= 0) { self.damageCooldown = self.damageCooldownTime; var outgoingDamageInfo = self.calculateOutgoingDamage(self.damageAmount, DamageType.PHYSICAL); if (target.takeDamage) { // Target is hero, has takeDamage target.takeDamage(outgoingDamageInfo.damage, DamageType.PHYSICAL, self); // Stun chance on hit var stunChance = 0.15; if (Math.random() < stunChance) { target.applyStatusEffect({ type: StatusEffectType.STUN, source: self, duration: 1.5, magnitude: null, stackingType: 'Duration' }); } } } } // If target is a decoy, tank just stands near it menacingly. // --- End Attack Logic --- } } else { // No target - Stand still } // --- End Act Based on Target --- // --- End Normal Behavior --- } // --- Final Updates --- self.handleWallCollision(); // Always handle wall collision }; // --- End self.update for TankEnemy --- return self; }); // --- RangedEnemy Class --- var RangedEnemy = BaseEnemy.expand(function () { var self = BaseEnemy.call(this); // Stat overrides self.health = 20; self.maxHealth = 20; self.armor = 0; self.magicResist = 5; self.movementSpeed = 1.2; self.damageAmount = 4; self.setupAnimation(['ranged_run1', 'ranged_run2', 'ranged_run3', 'ranged_run4']); self.projectileCooldownTime = 180; self.projectileCooldown = Math.random() * self.projectileCooldownTime; self.desiredDistance = 350; // Update self.update = function () { // --- Initial Checks --- if (!self.active) { return; } // --- Base Updates --- BaseEntity.prototype.update.call(self); // Handles timers, modifiers, DoTs etc. // Ranged enemy uses projectileCooldown, not damageCooldown from BaseEnemy typically // if (self.damageCooldown > 0) { self.damageCooldown--; } self.updateAnimation(); // Update animation regardless of state // --- Check Status Effects --- var isConfused = self.activeStatusEffects.some(function (eff) { return eff && eff.type === StatusEffectType.CONFUSE; }); // Add isStunned/isFrozen checks if needed // --- AI Behavior --- if (isConfused) { // --- Confused Behavior --- if (!self.wanderTarget || Math.random() < 0.02) { self.wanderTarget = { x: self.x + (Math.random() - 0.5) * 200, y: self.y + (Math.random() - 0.5) * 200 }; // Clamp wander target if (self.wanderTarget.x < wallThickness) { self.wanderTarget.x = wallThickness; } if (self.wanderTarget.x > roomWidth - wallThickness) { self.wanderTarget.x = roomWidth - wallThickness; } if (self.wanderTarget.y < wallThickness) { self.wanderTarget.y = wallThickness; } if (self.wanderTarget.y > roomHeight - wallThickness) { self.wanderTarget.y = roomHeight - wallThickness; } } var currentMoveSpeedConfused = self.getStat('movementSpeed'); if (currentMoveSpeedConfused > 0 && self.wanderTarget) { self.moveTowards(self.wanderTarget.x, self.wanderTarget.y, currentMoveSpeedConfused * 0.6); } self.rotation += 0.1; // Spin slowly // Tick down projectile cooldown even when confused? Maybe. if (self.projectileCooldown > 0) { self.projectileCooldown--; } // --- End Confused Behavior --- } else { // --- Normal (Non-Confused) Behavior --- var target = null; // 1. Prioritize Decoys var closestDecoy = null; var minDistSqToDecoy = Infinity; if (activeDecoys && activeDecoys.length > 0) { for (var i = 0; i < activeDecoys.length; i++) { var decoy = activeDecoys[i]; if (decoy) { var dx_d = decoy.x - self.x; var dy_d = decoy.y - self.y; var distSq_d = dx_d * dx_d + dy_d * dy_d; if (distSq_d < minDistSqToDecoy) { minDistSqToDecoy = distSq_d; closestDecoy = decoy; } } } if (closestDecoy) { target = closestDecoy; } } // 2. If no decoy targeted, check for VISIBLE hero if (!target && hero && hero.active) { var isHeroCloaked = hero.activeStatusEffects.some(function (effect) { return effect && effect.type === StatusEffectType.CLOAKED; }); if (!isHeroCloaked) { target = hero; } } // --- Act Based on Target --- if (target) { // Ranged has a target (decoy or hero) var targetX = target.x; var targetY = target.y; var dx_t = targetX - self.x; var dy_t = targetY - self.y; var distance = Math.sqrt(dx_t * dx_t + dy_t * dy_t); var currentMoveSpeedNormal = self.getStat('movementSpeed'); // --- Movement Logic (Maintain Distance) --- if (currentMoveSpeedNormal > 0) { // Only move if able if (distance < self.desiredDistance - 10) { // Move away if too close self.moveTowards(targetX, targetY, -currentMoveSpeedNormal); } else if (distance > self.desiredDistance + 10) { // Move closer if too far self.moveTowards(targetX, targetY, currentMoveSpeedNormal); } else { // In desired range, just face target if (dx_t !== 0 || dy_t !== 0) { self.rotation = Math.atan2(dy_t, dx_t); } } } else { // If speed is 0, just face target if (dx_t !== 0 || dy_t !== 0) { self.rotation = Math.atan2(dy_t, dx_t); } } // --- End Movement Logic --- // --- Shooting Logic (Shoot at target: hero or decoy) --- if (self.projectileCooldown <= 0) { self.shootProjectile(targetX, targetY); // Pass target coordinates var attackSpeedMult = self.getStat('attackSpeed'); // Consider enemy attack speed stat self.projectileCooldown = self.projectileCooldownTime / attackSpeedMult; } else { self.projectileCooldown--; } // --- End Shooting Logic --- } else { // No target - Stand still if (self.projectileCooldown > 0) { self.projectileCooldown--; } // Still tick cooldown } // --- End Act Based on Target --- // --- End Normal Behavior --- } // --- Final Updates --- self.handleWallCollision(); // Always handle wall collision }; // --- End self.update for RangedEnemy --- self.shootProjectile = function (targetX, targetY) { var projectile = new Projectile(); projectile.x = self.x; projectile.y = self.y; var outgoingDamageInfo = self.calculateOutgoingDamage(self.damageAmount, DamageType.PHYSICAL); projectile.damage = outgoingDamageInfo.damage; projectile.damageType = DamageType.PHYSICAL; projectile.source = self; var dx = targetX - self.x; var dy = targetY - self.y; var angle = Math.atan2(dy, dx); projectile.rotation = angle; projectile.vx = Math.cos(angle) * projectile.speed; projectile.vy = Math.sin(angle) * projectile.speed; gameContainer.addChild(projectile); }; return self; }); // --- Enemy Class (Melee - CORRECTED Movement & Inheritance) --- var Enemy = BaseEnemy.expand(function () { var self = BaseEnemy.call(this); // Inherit properties from BaseEnemy // --- Stat overrides --- self.health = 10; self.maxHealth = 10; self.armor = 5; self.magicResist = 0; self.movementSpeed = 1.5 + Math.random() * 0.3; self.damageAmount = 5; // damageCooldownTime and damageCooldown are inherited self.setupAnimation(['enemy_run1', 'enemy_run2', 'enemy_run3', 'enemy_run4']); // --- Update (Overrides BaseEnemy Update completely, but calls BaseEntity update) --- self.update = function () { if (!self.active || !hero) { return; } // Initial checks // 1. Update Status Effects & Modifiers from BaseEntity BaseEntity.prototype.update.call(self); // ---> COPY Duration Decrement/Removal Loop HERE <--- var effectIdsToRemove_ME = []; // Unique name if (self.activeDurationTimers) { for (var effectId_ME in self.activeDurationTimers) { if (self.activeDurationTimers.hasOwnProperty(effectId_ME)) { var timer_ME = self.activeDurationTimers[effectId_ME]; if (timer_ME && typeof timer_ME.framesRemaining === 'number') { timer_ME.framesRemaining--; if (timer_ME.framesRemaining <= 0) { effectIdsToRemove_ME.push(effectId_ME); } // --- Optional Debug Tint for Melee --- var effectDataForTint_ME = self.activeStatusEffects.find(function (eff) { return eff && eff.id === effectId_ME; }); if (effectDataForTint_ME && effectDataForTint_ME.type === StatusEffectType.CONFUSE) {/* ... change self.tint ... */} // --- End Optional Debug --- } else { effectIdsToRemove_ME.push(effectId_ME); } // Invalid timer } } } if (effectIdsToRemove_ME.length > 0) { for (var i_ME = 0; i_ME < effectIdsToRemove_ME.length; i_ME++) { self.removeStatusEffect(effectIdsToRemove_ME[i_ME]); } self.forceVisualRecalculation = true; // Trigger visual update check } // --- END Status Effect Duration Logic for Melee Enemy --- // 2. Decrement Melee Attack Cooldown if (self.damageCooldown > 0) { self.damageCooldown--; } // 3. Update Animation self.updateAnimation(); // --- Check Status Effects (AFTER duration processing) --- var isConfused = self.activeStatusEffects.some(function (eff) { return eff && eff.type === StatusEffectType.CONFUSE; }); // --- Check Status Effects (AFTER duration processing) --- var isConfused = self.activeStatusEffects.some(function (eff) { return eff && eff.type === StatusEffectType.CONFUSE; }); // --- AI Behavior --- if (isConfused) { // ---> COPY Confused Behavior from TankEnemy <--- if (!self.wanderTarget || Math.random() < 0.02) { // Pick new wander target occasionally self.wanderTarget = { x: self.x + (Math.random() - 0.5) * 200, y: self.y + (Math.random() - 0.5) * 200 }; // Clamp wander target within walls if (self.wanderTarget.x < wallThickness) { self.wanderTarget.x = wallThickness; } if (self.wanderTarget.x > roomWidth - wallThickness) { self.wanderTarget.x = roomWidth - wallThickness; } if (self.wanderTarget.y < wallThickness) { self.wanderTarget.y = wallThickness; } if (self.wanderTarget.y > roomHeight - wallThickness) { self.wanderTarget.y = roomHeight - wallThickness; } } var currentMoveSpeedConfused = self.getStat('movementSpeed'); if (currentMoveSpeedConfused > 0 && self.wanderTarget) { self.moveTowards(self.wanderTarget.x, self.wanderTarget.y, currentMoveSpeedConfused * 0.6); // Move slower maybe? } self.rotation += 0.1; // Spin slowly? Indicate confusion visually // ---> END Confused Behavior <--- } else { // --- Normal (Non-Confused) Behavior --- var target = null; // 1. Prioritize Decoys var closestDecoy = null; var minDistSqToDecoy = Infinity; if (activeDecoys && activeDecoys.length > 0) { for (var i = 0; i < activeDecoys.length; i++) {/* ... find closest decoy ... */} if (closestDecoy) { target = closestDecoy; } } // 2. Target Hero if no decoy and hero visible if (!target && hero && hero.active && !hero.activeStatusEffects.some(function (eff) { return eff && eff.type === StatusEffectType.CLOAKED; })) { target = hero; } if (target) { // --- Normal Move/Attack/Pushback Logic --- var targetX = target.x + (Math.random() - 0.5) * 20; // Add jitter var targetY = target.y + (Math.random() - 0.5) * 20; var currentMoveSpeedNormal = self.getStat('movementSpeed'); var distance = self.moveTowards(targetX, targetY, currentMoveSpeedNormal); // Move towards target var minDistance = 70; if (distance <= minDistance) { // If in range if (target === hero) { // Only attack/pushback HERO if (self.damageCooldown <= 0) { // Attack if cooldown ready self.damageCooldown = self.damageCooldownTime; var outgoingDamageInfo = self.calculateOutgoingDamage(self.damageAmount, DamageType.PHYSICAL); if (hero.takeDamage) { hero.takeDamage(outgoingDamageInfo.damage, DamageType.PHYSICAL, self); } } // Pushback logic var pushDx = self.x - target.x; var pushDy = self.y - target.y; var pushDist = Math.sqrt(pushDx * pushDx + pushDy * pushDy); if (pushDist > 0) { var pushAmount = (minDistance - distance) * 0.5; self.x += pushDx / pushDist * pushAmount; self.y += pushDy / pushDist * pushAmount; } } // If target is decoy, just stop near it. } // --- End Normal Move/Attack/Pushback --- } else { // No target - Stand still } // --- End Normal Behavior --- } // --- Final Updates --- self.handleWallCollision(); // Always apply wall collision }; // --- End self.update for Enemy (Melee) --- return self; }); // Joystick class var Joystick = Container.expand(function () { var self = Container.call(this); var joystickBackground = self.attachAsset('joystickBackground', { anchorX: 0.5, anchorY: 0.5 }); var joystickHandle = self.attachAsset('joystickHandle', { anchorX: 0.5, anchorY: 0.5 }); self.x = joystickBackground.width / 2 + 100; self.y = roomHeight - joystickBackground.height / 2 - 100; var maxRadius = 100; var isDragging = false; self.down = function (x, y, obj) { isDragging = true; }; self.up = function (x, y, obj) { isDragging = false; joystickHandle.x = 0; joystickHandle.y = 0; }; self.move = function (x, y, obj) { if (isDragging && !isPopupActive && !isSkillDetailPopupActive) { var localPos = self.toLocal(obj.global); var dx = localPos.x; var dy = localPos.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > maxRadius) { var angle = Math.atan2(dy, dx); dx = maxRadius * Math.cos(angle); dy = maxRadius * Math.sin(angle); } joystickHandle.x = dx; joystickHandle.y = dy; } }; self.getDirection = function () { var magnitude = Math.sqrt(joystickHandle.x * joystickHandle.x + joystickHandle.y * joystickHandle.y); if (magnitude > 0) { return { x: joystickHandle.x / magnitude, y: joystickHandle.y / magnitude }; } return { x: 0, y: 0 }; }; return self; }); // Projectile class (Enemy ranged attack) var Projectile = Container.expand(function () { var self = Container.call(this); self.attachAsset('ranged_attack', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 5; self.damage = 5; self.damageType = DamageType.PHYSICAL; self.source = null; self.vx = 0; self.vy = 0; self.update = function () { self.x += self.vx; self.y += self.vy; self.rotation += 0.1; if (self.x < 0 || self.x > roomWidth || self.y < 0 || self.y > roomHeight) { if (self.parent) { self.parent.removeChild(self); } return; } if (hero && hero.active && self.intersects && self.intersects(hero)) { if (hero.takeDamage) { hero.takeDamage(self.damage, self.damageType, self.source); // ***** APPLY SHOCK ON HIT ***** if (Math.random() < self.shockChance) { hero.applyStatusEffect({ type: StatusEffectType.SHOCK, source: self.source || self, duration: 6, // Duration from Table 1 magnitude: 0.15, // Example: +15% damage taken stackingType: 'Duration' // Refresh }); // console.log("Applied Shock!"); // Debug } // ***** END APPLY SHOCK ***** } if (self.parent) { self.parent.removeChild(self); } } }; return self; }); // --- Shadow Familiar Class --- var ShadowFamiliar = Container.expand(function (sourceHero, durationSeconds, slowPercent) { var self = Container.call(this); // Basic container, doesn't need full BaseEntity stats (unless it can be attacked?) // --- Properties --- self.sourceHero = sourceHero; self.durationMillis = durationSeconds * 1000; self.slowMagnitude = slowPercent / 100; // Convert percent to decimal for status effect self.auraRadius = 150; // How close enemies need to be to get slowed (adjust) self.auraTickInterval = 30; // How often to check for enemies in aura (frames, e.g., twice per second) self.auraTickCounter = 0; self.active = true; // Mark as active // --- Visuals --- // Use the ShadowFamiliar icon? Or another graphic? Let's use the icon. var familiarVisual = self.attachAsset('ShadowFamiliar_icon', { anchorX: 0.5, anchorY: 0.5, alpha: 0.8, // Slightly transparent scaleX: 1.5, scaleY: 1.5 // Make it a bit bigger maybe }); // Make it float near the hero? Or stay in one place? Let's have it follow. self.followOffset = { x: -60, y: -60 }; // Example offset from hero // --- Timeout for Despawn --- var despawnTimeoutId = LK.setTimeout(function () { self.destroy(); // Call destroy when duration ends }, self.durationMillis); // --- Update Method --- self.update = function () { if (!self.active || !self.sourceHero || !self.sourceHero.active) { // If familiar is inactive or hero disappears, destroy self self.destroy(); return; } // Follow the hero self.x = self.sourceHero.x + self.followOffset.x; self.y = self.sourceHero.y + self.followOffset.y; // Bobbing/floating animation? (Optional visual flair) self.y += Math.sin(game.frame * 0.05) * 3; // Simple vertical bob // Aura Check Timer self.auraTickCounter--; if (self.auraTickCounter <= 0) { self.auraTickCounter = self.auraTickInterval; // Reset timer self.applySlowAura(); // Apply slow to nearby enemies } }; // --- Helper to Apply Slow Aura --- self.applySlowAura = function () { if (!currentRoom || !currentRoom.enemies) { return; } currentRoom.enemies.forEach(function (enemy) { if (enemy && enemy.active) { var dx = enemy.x - self.x; // Distance from familiar var dy = enemy.y - self.y; var distSq = dx * dx + dy * dy; if (distSq < self.auraRadius * self.auraRadius) { // Enemy is in range, apply SLOW effect enemy.applyStatusEffect({ type: StatusEffectType.SLOW, source: self.sourceHero, // Attributed to the hero duration: 2, // Short duration, reapplied frequently by aura magnitude: { speed: self.slowMagnitude, attack: self.slowMagnitude * 0.5 }, // Slow movement more than attack? Example values stackingType: 'Duration' // Refresh duration }); } } }); }; // --- Destroy Method --- var originalDestroy = self.destroy; // Store original if exists self.destroy = function () { self.active = false; // Clear the despawn timeout if destroyed manually/early if (despawnTimeoutId) { LK.clearTimeout(despawnTimeoutId); despawnTimeoutId = null; } // ---> ADD THIS CHECK <--- if (self.sourceHero && self.sourceHero.activeFamiliar === self) { self.sourceHero.activeFamiliar = null; // Clear reference on hero } // ---> END ADDITION <--- // Remove from parent container if (self.parent) { self.parent.removeChild(self); } // Call original destroy if needed (though Container might not have one) // if (originalDestroy) { originalDestroy.call(self); } // Remove from any global tracking arrays if necessary (we aren't using one here) }; return self; }); // StartGameButton class var StartGameButton = Container.expand(function () { var self = Container.call(this); self.attachAsset('startGameButton', { anchorX: 0.5, anchorY: 0.5, scaleX: 3, scaleY: 3 }); self.x = roomWidth / 2; self.y = roomHeight - 400; self.down = function () { if (self.parent) { menuContainer.removeChild(self.parent); } isPopupActive = false; isSkillDetailPopupActive = false; initializeGame(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 // Default background color }); /**** * Game Code ****/ // --- BaseEntity Prototype Methods (CORRECTED PLACEMENT) --- /**** * Game Configuration & Data ****/ // Adjusted default size // Add containers to the game stage // Made it grey // -- Circuit of Ascension Icons -- // -- Echoes of Ancestry Icons -- // -- Forge of Possibilities Icons -- // -- Prism of Potential Icons -- // -- Nexus of Chaos Icons -- // -- Symphony of Shadows Icons -- // -- Circuit of Ascension Icons -- // -- Echoes of Ancestry Icons -- // -- Forge of Possibilities Icons -- // -- Prism of Potential Icons -- // -- Nexus of Chaos Icons -- // -- Symphony of Shadows Icons -- // KEEPING THIS FOR NOW - REPLACE WITH 'hero_topdown' LATER // LK.init.image('hero_topdown', {width:100, height:100, id:'YOUR_NEW_HERO_ASSET_ID'}) // Placeholder for new hero asset /**** * Global Constants ****/ // Define Damage Types /**** * Global Constants ****/ // Define Damage Types function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } var DamageType = { PHYSICAL: 'physical', FIRE: 'fire', ICE: 'ice', LIGHTNING: 'lightning', POISON: 'poison', SHADOW: 'shadow', CHAOS: 'chaos', HEALING: 'healing', TRUE: 'true' }; // Define Status Effect Types (aligned with Table 1) var StatusEffectType = { IGNITE: 'ignite', CHILL: 'chill', FREEZE: 'freeze', SHOCK: 'shock', POISON: 'poison', BLEED: 'bleed', CONFUSE: 'confuse', STUN: 'stun', SLOW: 'slow', // If needed as separate from Chill CLOAKED: 'cloaked', // Effect type for Nightfall Veil invisibility CHARM: 'charm', DISORIENT: 'disorient', VULNERABLE: 'vulnerable' // Add more as needed }; // --- Base Hero Stats Constants --- var HERO_BASE_HEALTH = 100; var HERO_BASE_ENERGY = 50; var HERO_BASE_ENERGY_REGEN = 1.0; // Per second (adjust implementation later) var HERO_BASE_ARMOR = 5; var HERO_BASE_MAGIC_RESIST = 5; var HERO_BASE_MOVE_SPEED = 3.0; var HERO_BASE_ATTACK_SPEED = 1.0; // Multiplier var HERO_BASE_DAMAGE_BONUS = 0.0; // Percentage var HERO_BASE_CRIT_CHANCE = 0.05; // 5% var HERO_BASE_CRIT_DAMAGE = 1.5; // 150% var HERO_BASE_DODGE_CHANCE = 0.0; var HERO_BASE_LIFE_STEAL = 0.0; var HERO_BASE_THORNS = 0; var HERO_BASE_CDR = 0.0; var HERO_BASE_THORNS_PERCENT = 0.0; // For Indigo Insight reflect calculation var HERO_BASE_DAMAGE_REDUCTION = 0.0; // For Spirit Guidance calculation var HERO_BASE_ELE_DMG_BONUS = 0.0; // For Elemental Mastery calculation var HERO_BASE_CHROM_DMG_BONUS = 0.0; // For Chromatic Fusion calculation var HERO_BASE_RED_DMG_BONUS = 0.0; // For Red Radiance damage calculation var HERO_BASE_CHAOTIC_SPARK_CHANCE = 0.0; // For Chaotic Spark calculation var HERO_BASE_ELEMENTAL_INFUSION_CHANCE = 0.0; // For Elemental Infusion calculation var HERO_BASE_UNSTABLE_RIFT_CHANCE = 0.0; // For Unstable Rift calculation var HERO_BASE_ENTROPY_PROC = 0.0; // For Entropy Shield calculation var HERO_BASE_ENTROPY_REFLECT = 0.0; // For Entropy Shield calculation var HERO_BASE_ENTROPY_BLOCKS = 0; // For Entropy Shield calculation var HERO_BASE_UMBRAL_ECHO_CHANCE = 0.0; // Base chance for Umbral Echoes proc // Base Resistances var HERO_BASE_FIRE_RESIST = 0.0; var HERO_BASE_ICE_RESIST = 0.0; var HERO_BASE_LIGHTNING_RESIST = 0.0; var HERO_BASE_POISON_RESIST = 0.0; var HERO_BASE_SHADOW_RESIST = 0.0; var HERO_BASE_CHAOS_RESIST = 0.0; // --- BaseEntity Prototype Methods --- // Update (Handles Modifiers and Status Effects) BaseEntity.prototype.update = function () { if (!this.active) { return; } // --- Update Modifiers (Existing) --- this.updateModifiers(); // --- Process Status Effect Durations --- var effectIdsToRemove = []; if (this.activeDurationTimers) { for (var effectId in this.activeDurationTimers) { if (this.activeDurationTimers.hasOwnProperty(effectId)) { var timer = this.activeDurationTimers[effectId]; if (timer && typeof timer.framesRemaining === 'number') { timer.framesRemaining--; if (timer.framesRemaining <= 0) { effectIdsToRemove.push(effectId); } } else { effectIdsToRemove.push(effectId); } } } } if (effectIdsToRemove.length > 0) { for (var i = 0; i < effectIdsToRemove.length; i++) { this.removeStatusEffect(effectIdsToRemove[i]); } this.forceVisualRecalculation = true; } // --- Update Status Effects (Existing - Focuses on DoT/Visuals) --- this.updateStatusEffects(); // Apply DoTs, update FINAL visuals based on current effects }; // Die (Handles removing object from parent) BaseEntity.prototype.die = function (killer) { if (!this.active) { return; } // Use 'this' // active flag should be set false by the child's die method before calling this if (this.parent) { this.parent.removeChild(this); } }; // Stat Calculation Helper BaseEntity.prototype.getStat = function (statName) { // Use 'this' to access properties within prototype methods var baseValue = this[statName] || 0; var additiveBonus = 0; var multiplicativeBonus = 1.0; // Apply active STAT modifiers this.activeModifiers.forEach(function (mod) { // 'this' is still accessible here if needed, or use self=this pattern if callback scope changes if (mod.stat === statName) { if (mod.isPercentage) { multiplicativeBonus += mod.value; } else { additiveBonus += mod.value; } } }); var finalValue = (baseValue + additiveBonus) * multiplicativeBonus; // APPLY STATUS EFFECT MODIFIERS var speedMultiplier = 1.0; var canActMultiplier = 1.0; // Use 0 if stunned/frozen this.activeStatusEffects.forEach(function (effect) { // Speed reduction checks (Apply multiplicatively - strongest slow matters most) if (statName === 'movementSpeed' || statName === 'attackSpeed') { // Magnitude should be an object like { speed: 0.3, attack: 0.2 } for Chill/Slow var slowFactor = 1.0; if (effect.type === StatusEffectType.CHILL) { var _effect$magnitude2, _effect$magnitude3; slowFactor = statName === 'movementSpeed' ? 1.0 - (((_effect$magnitude2 = effect.magnitude) === null || _effect$magnitude2 === void 0 ? void 0 : _effect$magnitude2.speed) || 0.3) : 1.0 - (((_effect$magnitude3 = effect.magnitude) === null || _effect$magnitude3 === void 0 ? void 0 : _effect$magnitude3.attack) || 0.2); } else if (effect.type === StatusEffectType.SLOW) { var _effect$magnitude4, _effect$magnitude5; slowFactor = statName === 'movementSpeed' ? 1.0 - (((_effect$magnitude4 = effect.magnitude) === null || _effect$magnitude4 === void 0 ? void 0 : _effect$magnitude4.speed) || 0.5) : 1.0 - (((_effect$magnitude5 = effect.magnitude) === null || _effect$magnitude5 === void 0 ? void 0 : _effect$magnitude5.attack) || 0.3); } speedMultiplier *= slowFactor; // Apply the slow factor } // Action prevention checks if (effect.type === StatusEffectType.STUN || effect.type === StatusEffectType.FREEZE || effect.type === StatusEffectType.CONFUSE || effect.type === StatusEffectType.CHARM) { // If any of these hard CCs are active, entity cannot perform standard actions canActMultiplier = 0; } // Disorient check (Reduces damage dealt, handled in calculateOutgoingDamage) }); finalValue *= speedMultiplier; // If checking speed/attack speed, also apply hard CC multiplier if (statName === 'movementSpeed' || statName === 'attackSpeed') { finalValue *= canActMultiplier; } // Clamping specific stats if (statName === 'critChance' || statName === 'dodgeChance' || statName === 'skillCooldownReduction' || statName.includes('Resist')) { finalValue = Math.max(0, Math.min(finalValue, 1.0)); } if (statName === 'attackSpeed') { finalValue = Math.max(0.1, finalValue); } if (statName === 'movementSpeed') { // Allow 0 speed if stunned/frozen, otherwise clamp above a minimum finalValue = canActMultiplier === 0 ? 0 : Math.max(0.5, finalValue); } return finalValue; }; // Modifier Management (For STAT Buffs/Debuffs) BaseEntity.prototype.applyStatModifier = function (stat, value, duration, isPercentage, source) { var modifier = { id: Date.now().toString(36) + Math.random().toString(36).substr(2), stat: stat, value: value, duration: duration, isPercentage: isPercentage, source: source || 'unknown' }; this.activeModifiers.push(modifier); // Use 'this' }; BaseEntity.prototype.removeStatModifier = function (modifierId) { var index = this.activeModifiers.findIndex(function (mod) { return mod.id === modifierId; }); if (index > -1) { this.activeModifiers.splice(index, 1); } // Use 'this' }; BaseEntity.prototype.updateModifiers = function () { for (var i = this.activeModifiers.length - 1; i >= 0; i--) { var mod = this.activeModifiers[i]; mod.duration--; if (mod.duration <= 0) { this.removeStatModifier(mod.id); } // Use 'this' } }; // Status Effect Management BaseEntity.prototype.applyStatusEffect = function (effectData) { if (!this.active || !effectData || !effectData.type) { return; } var self = this; // Capture 'this' for use in setTimeout callback var effectType = effectData.type; var source = effectData.source; var durationSeconds = effectData.duration || 1; // Use duration in seconds var durationFrames = Math.round(durationSeconds * 60); // Keep frames for potential use? Or remove? Let's keep for now maybe. var durationMillis = durationSeconds * 1000; // Duration for setTimeout var magnitude = effectData.magnitude; var tickInterval = Math.round((effectData.tickInterval || 1) * 60); var stackingType = effectData.stackingType || 'Duration'; var existingEffect = null; var existingEffectId = null; var existingTimeoutId = null; // Find existing effect from the same source for (var i = 0; i < self.activeStatusEffects.length; i++) { if (self.activeStatusEffects[i] && self.activeStatusEffects[i].type === effectType && self.activeStatusEffects[i].source === source) { existingEffect = self.activeStatusEffects[i]; existingEffectId = existingEffect.id; existingTimeoutId = existingEffect.timeoutId; // Get existing timeout ID break; } } if (existingEffect && existingEffectId) { // Effect exists, update magnitude existingEffect.magnitude = magnitude; // --- Handle Timeout Refresh based on Stacking --- if (stackingType === 'Duration' || stackingType === 'Intensity') { // Clear the old timeout if (existingTimeoutId) { LK.clearTimeout(existingTimeoutId); } // Start a new timeout with the new (or potentially refreshed) duration var newTimeoutId = LK.setTimeout(function () { // IMPORTANT: Check if the effect STILL exists before removing // It might have been manually removed between timeout start and callback var effectStillExists = self.activeStatusEffects.some(function (eff) { return eff && eff.id === existingEffectId; }); if (effectStillExists) { self.removeStatusEffect(existingEffectId); } }, durationMillis); // Use new duration in ms // Store the NEW timeout ID on the effect object existingEffect.timeoutId = newTimeoutId; } // If stackingType is 'None', do nothing to the timer } else { // Create new effect var newEffectId = Date.now().toString(36) + Math.random().toString(36).substr(2); var newEffect = { // Static data + timeoutId placeholder id: newEffectId, type: effectType, source: source, magnitude: magnitude, ticksRemaining: 0, tickInterval: tickInterval, stackingType: stackingType, data: {}, timeoutId: null // Placeholder }; // --- Start Timeout for Duration --- var timeoutId = LK.setTimeout(function () { // Check if the effect still exists when callback runs var effectStillExists = self.activeStatusEffects.some(function (eff) { return eff && eff.id === newEffectId; }); if (effectStillExists) { self.removeStatusEffect(newEffectId); // Call remove when timer expires } }, durationMillis); // Use duration in ms // Store the timeout ID on the effect object newEffect.timeoutId = timeoutId; // Add the effect object (with timeoutId) to the array self.activeStatusEffects.push(newEffect); // Initial TINT visual cue start switch (effectType) { /* ... tint cases from previous correct version ... */ case StatusEffectType.IGNITE: self.tint = 0xFF8800; break; case StatusEffectType.BLEED: self.tint = 0xCC0000; break; case StatusEffectType.FREEZE: self.tint = 0x00AAFF; break; case StatusEffectType.POISON: self.tint = 0x00CC00; break; case StatusEffectType.SHOCK: self.tint = 0xFFFF00; break; case StatusEffectType.STUN: self.tint = 0xAAAA00; break; case StatusEffectType.CONFUSE: self.tint = 0xAA00AA; break; case StatusEffectType.CHARM: self.tint = 0xFF88CC; break; } } // Force a visual recalculation check self.forceVisualRecalculation = true; }; BaseEntity.prototype.removeStatusEffect = function (effectId) { if (!effectId) { return; } var index = -1; var timeoutIdToClear = null; // Find the index AND get the timeoutId before removing for (var i = 0; i < this.activeStatusEffects.length; i++) { if (this.activeStatusEffects[i] && this.activeStatusEffects[i].id === effectId) { index = i; timeoutIdToClear = this.activeStatusEffects[i].timeoutId; // Get the stored ID break; } } if (index > -1) { // --- Clear the associated Timeout --- if (timeoutIdToClear) { LK.clearTimeout(timeoutIdToClear); // Prevent expiration callback if manually removed } // Remove from the main effects array this.activeStatusEffects.splice(index, 1); // Force visual update check this.forceVisualRecalculation = true; } // No need to interact with activeDurationTimers anymore }; // It focuses on DoTs and visual updates based on the current effects list. BaseEntity.prototype.updateStatusEffects = function () { // Flag to trigger tint update if needed (set by apply/removeStatusEffect) var applyVisualUpdate = this.forceVisualRecalculation || false; this.forceVisualRecalculation = false; // Reset flag for next frame var isCloakedNow = false; // Track current cloak status for visuals // ---> Iterate effects ONLY for DoTs and determining current state <--- // We loop through the array as effects might be added/removed externally too for (var i = this.activeStatusEffects.length - 1; i >= 0; i--) { var effectData = this.activeStatusEffects[i]; if (!effectData) { continue; } // Safety check // Check for cloak presence for visual update later if (effectData.type === StatusEffectType.CLOAKED) { isCloakedNow = true; } // --- Handle DoT Ticking --- // Check if this effect is a Damage Over Time type var isDoT = effectData.type === StatusEffectType.IGNITE || effectData.type === StatusEffectType.POISON || effectData.type === StatusEffectType.BLEED; if (isDoT) { // Ensure ticksRemaining is initialized if needed effectData.ticksRemaining = effectData.ticksRemaining || 0; // Decrement tick timer effectData.ticksRemaining--; // Apply damage if tick timer reaches zero if (effectData.ticksRemaining <= 0) { var dotDamage = 0; // Safely access magnitude, checking for 'dot' property if it's an object if (effectData.magnitude) { if (_typeof(effectData.magnitude) === 'object' && effectData.magnitude.dot !== undefined) { dotDamage = effectData.magnitude.dot; } else if (typeof effectData.magnitude === 'number') { dotDamage = effectData.magnitude; // Use magnitude directly if it's just a number } } if (dotDamage > 0) { var damageType = DamageType.TRUE; // Default damage type // Determine correct damage type based on DoT effect if (effectData.type === StatusEffectType.IGNITE) { damageType = DamageType.FIRE; } else if (effectData.type === StatusEffectType.POISON) { damageType = DamageType.POISON; } else if (effectData.type === StatusEffectType.BLEED) { damageType = DamageType.PHYSICAL; } // Apply the DoT damage (bypassing dodge) this.takeDamage(dotDamage, damageType, effectData.source, true); } // Reset tick timer (use tickInterval from the effect data, default to 60 frames/1 sec) effectData.ticksRemaining = effectData.tickInterval || 60; } } // --- End DoT Ticking --- } // End loop through activeStatusEffects // ----- Update Debug Text (Shows presence, NOT duration) ----- // Keep this removed if you previously removed it. If kept for testing: /* if (statusDebugText && this === hero) { var statusString = "Status: "; if (this.activeStatusEffects.length > 0) { statusString += this.activeStatusEffects.map(function(eff){ return eff ? eff.type : '???'; }).join(", "); } else { statusString += "[]"; } if (statusString.length > 60) statusString = statusString.substring(0, 57) + "..."; statusDebugText.setText(statusString); statusDebugText.fill = 0x00FFFF; } else if (statusDebugText) { statusDebugText.setText("Status: ---"); } */ // ----- End Debug Text Update ----- // ----- Update Visuals (Alpha for Hero, Tint for all) ----- // Update alpha for hero based purely on the presence check done above if (this === hero) { this.alpha = isCloakedNow ? 0.3 : 1.0; } // Update tint only if flagged (apply/remove happened or forced) if (applyVisualUpdate) { var finalTint = 0xFFFFFF; // Default White var highestPriority = -1; // Check remaining effects in activeStatusEffects array for tint calculation this.activeStatusEffects.forEach(function (eff) { if (!eff) { return; } // Safety check var effectTint = 0xFFFFFF; var priority = 0; // Assign tint and priority based on effect type switch (eff.type) { case StatusEffectType.FREEZE: effectTint = 0x00AAFF; priority = 5; break; case StatusEffectType.STUN: effectTint = 0xAAAA00; priority = 5; break; case StatusEffectType.IGNITE: effectTint = 0xFF8800; priority = 4; break; case StatusEffectType.BLEED: effectTint = 0xCC0000; priority = 4; break; case StatusEffectType.POISON: effectTint = 0x00CC00; priority = 3; break; case StatusEffectType.SHOCK: effectTint = 0xFFFF00; priority = 3; break; case StatusEffectType.CHILL: case StatusEffectType.SLOW: effectTint = 0x00AAFF; priority = 2; break; case StatusEffectType.CHARM: effectTint = 0xFF88CC; priority = 1; break; case StatusEffectType.CONFUSE: effectTint = 0xAA00AA; priority = 1; break; // Added confuse tint back // CLOAKED has no tint, handled by alpha } // Update finalTint if current effect has higher priority if (priority > highestPriority) { highestPriority = priority; finalTint = effectTint; } }); // Apply the calculated final tint this.tint = finalTint; } // ----- End Visual Update ----- }; // End NEW BaseEntity.prototype.updateStatusEffects // Damage Calculation Logic BaseEntity.prototype.calculateDamageTaken = function (amount, damageType, attackerStats) { if (damageType === DamageType.TRUE) { return amount; } if (damageType === DamageType.HEALING) { return -amount; } var effectiveDamage = amount; var resistance = 0; var defense = 0; var damageTakenMultiplier = 1.0; this.activeStatusEffects.forEach(function (effect) { // Use 'this' if (effect.type === StatusEffectType.SHOCK) { damageTakenMultiplier += effect.magnitude || 0.15; // Example: Shock adds 15% } // Check for Vulnerable type matching incoming damageType here later }); effectiveDamage *= damageTakenMultiplier; switch (damageType) { case DamageType.PHYSICAL: defense = this.getStat('armor'); effectiveDamage *= 1 - defense / (defense + 100); break; case DamageType.FIRE: resistance = this.getStat('fireResist'); defense = this.getStat('magicResist'); break; case DamageType.ICE: resistance = this.getStat('iceResist'); defense = this.getStat('magicResist'); break; case DamageType.LIGHTNING: resistance = this.getStat('lightningResist'); defense = this.getStat('magicResist'); break; case DamageType.POISON: resistance = this.getStat('poisonResist'); defense = this.getStat('magicResist'); break; case DamageType.SHADOW: resistance = this.getStat('shadowResist'); defense = this.getStat('magicResist'); break; case DamageType.CHAOS: resistance = this.getStat('chaosResist'); defense = this.getStat('magicResist'); break; default: resistance = 0; defense = this.getStat('magicResist'); break; } if (damageType !== DamageType.PHYSICAL && defense > 0) { effectiveDamage *= 1 - defense / (defense + 100); } effectiveDamage *= 1 - resistance; return Math.max(0, effectiveDamage); }; BaseEntity.prototype.calculateOutgoingDamage = function (baseAmount, damageType) { var finalDamage = baseAmount; var isCrit = false; finalDamage *= 1 + this.getStat('damageBonus'); // Use 'this' // ***** APPLY BONUS ELEMENTAL DAMAGE (from Hero Skills) ***** if (this === hero) { // Only hero has these skills currently // Check if the base damage type is elemental var isElemental = damageType === DamageType.FIRE || damageType === DamageType.ICE || damageType === DamageType.LIGHTNING || damageType === DamageType.POISON || damageType === DamageType.SHADOW || damageType === DamageType.CHAOS; // Elemental Mastery Bonus (applies only to elemental damage) if (isElemental && this.bonusElementalDamagePercent > 0) { finalDamage *= 1 + this.bonusElementalDamagePercent / 100; } // Chromatic Fusion Bonus (applies to ALL damage types as additional elemental damage) // This is tricky. Does it add a % of base damage AS elemental, or boost existing elemental? // Let's assume it boosts existing elemental damage for now. // A separate implementation would be needed if it ADDS damage of a random type. if (isElemental && this.bonusChromaticDamagePercent > 0) { finalDamage *= 1 + this.bonusChromaticDamagePercent / 100; } // Red Radiance Damage bonus (is this fire specific or general?) // Assuming general for now based on description, apply multiplicatively? if (this.bonusRedDamagePercent > 0) { finalDamage *= 1 + this.bonusRedDamagePercent / 100; } } // ***** END BONUS ELEMENTAL DAMAGE ***** // Check for critical hit var critChance = this.getStat('critChance'); // --- TODO: Add check for separate Elemental Crit Chance from Ancient Wisdom --- // if (isElemental && Math.random() < this.elementalCritChance) isCrit = true; // else if (Math.random() < critChance) isCrit = true; if (Math.random() < critChance) { // Using combined crit for now isCrit = true; var critMultiplier = this.getStat('critDamage'); finalDamage *= critMultiplier; } return { damage: Math.max(0, finalDamage), isCrit: isCrit }; }; // Take Damage Method BaseEntity.prototype.takeDamage = function (amount, damageType, source) { var bypassDodge = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; if (!this.active) { return; } // Apply Thorns/Reflection FIRST (based on raw incoming damage before defenses?) // Let's apply it based on raw incoming damage 'amount' if (this === hero && damageType !== DamageType.HEALING && damageType !== DamageType.TRUE && source && source.takeDamage && this.thornsPercent > 0) { var reflectDamage = amount * this.thornsPercent; if (reflectDamage > 0) { // Reflect as TRUE damage? Or Physical? Let's use TRUE for now. source.takeDamage(reflectDamage, DamageType.TRUE, this, true); // Bypass dodge on reflect } } // ***** ADD ENTROPY SHIELD CHECK (If target is Hero) ***** if (this === hero) { // Only hero has Entropy Shield skill currently if (Math.random() < this.entropyShieldProcChance) { // Proc successful! Block/Reflect logic needed // --- Block Logic --- // For now, just negate the damage. Later, use blockCounts. // We need a way to track blocks per shield instance if blockCounts > 1 console.log("Damage Blocked by Entropy Shield!"); amount = 0; // Negate incoming damage this time // --- Reflect Logic --- var reflectDamage = amount * this.entropyShieldReflectPercent; // Calculate reflect amount (based on ORIGINAL damage before block) if (reflectDamage > 0 && source && source.takeDamage) { console.log("Reflecting", reflectDamage.toFixed(1), "damage!"); // Reflect damage back - usually physical or maybe same type? Let's use TRUE for now. source.takeDamage(reflectDamage, DamageType.TRUE, this); } // --- TODO: Add Block VFX/SFX --- // --- TODO: Implement block count logic --- // Since damage might be blocked, we can potentially return early OR // continue to apply healing reduction/calculate 0 effective damage. // Let's continue for now, effectiveAmount will be 0 if blocked. // Alternatively: return; // Exit function immediately after block/reflect } } // ***** END ENTROPY SHIELD CHECK ***** // ***** HANDLE HEALING REDUCTION (e.g., Poison) ***** var healingMultiplier = 1.0; if (damageType === DamageType.HEALING) { this.activeStatusEffects.forEach(function (effect) { // Use 'this' if (effect.type === StatusEffectType.POISON) { // Magnitude might be { dot: X, healReduction: Y } var _effect$magnitude; healingMultiplier *= 1.0 - (((_effect$magnitude = effect.magnitude) === null || _effect$magnitude === void 0 ? void 0 : _effect$magnitude.healReduction) || 0.5); // Example: Poison reduces healing by 50% } }); amount *= healingMultiplier; // Reduce healing amount if (amount <= 0) { return; } // No healing occurs } // ***** END HEALING REDUCTION ***** if (!bypassDodge && damageType !== DamageType.HEALING && damageType !== DamageType.TRUE) { var dodgeChance = this.getStat('dodgeChance'); // Use 'this' if (Math.random() < dodgeChance) { return; } } var effectiveAmount = this.calculateDamageTaken(amount, damageType, source === null || source === void 0 ? void 0 : source.stats); // ***** APPLY DAMAGE REDUCTION (e.g., Spirit Guidance) ***** if (this === hero && this.damageReduction > 0) { effectiveAmount *= 1.0 - this.damageReduction; } // ***** END DAMAGE REDUCTION ***** this.health -= effectiveAmount; // Use 'this' // --- TODO: Add visual feedback (hit flash, damage numbers) --- this.health = Math.min(this.health, this.getStat('maxHealth')); // Use 'this' if (this.health <= 0) { this.health = 0; this.die(source); // Use 'this' to call die } if (this === hero) { updateRoomDisplay(); } // Use 'this' }; BaseEntity.prototype.update = function () { if (!this.active) { return; } // Use 'this' inside prototype methods this.updateModifiers(); // Update durations and remove expired buffs/debuffs // --- Update Status Effects (Still needed for DoTs/Visuals) --- this.updateStatusEffects(); }; // Define the die method on the PROTOTYPE BaseEntity.prototype.die = function (killer) { if (!this.active) { return; } // Still good to prevent multiple calls at this stage // Base cleanup: Remove from parent container if it exists if (this.parent) { this.parent.removeChild(this); } }; var DamageType = { PHYSICAL: 'physical', FIRE: 'fire', ICE: 'ice', LIGHTNING: 'lightning', POISON: 'poison', SHADOW: 'shadow', CHAOS: 'chaos', // Special type for Nexus skills? HEALING: 'healing', // For effects that restore health TRUE: 'true' // Damage that ignores defenses }; var roomWidth = 2048; var roomHeight = 2732; var wallThickness = 400; var entranceWidth = 200; var entrances = { top: { xStart: (roomWidth - entranceWidth) / 2, xEnd: (roomWidth + entranceWidth) / 2, y: wallThickness }, bottom: { xStart: (roomWidth - entranceWidth) / 2, xEnd: (roomWidth + entranceWidth) / 2, y: roomHeight - wallThickness }, left: { yStart: (roomHeight - entranceWidth) / 2, yEnd: (roomHeight + entranceWidth) / 2, x: wallThickness }, right: { yStart: (roomHeight - entranceWidth) / 2, yEnd: (roomHeight + entranceWidth) / 2, x: roomWidth - wallThickness } }; var spawnPoints = { topLeft: { x: wallThickness + 50, y: wallThickness + 50 }, // Offset slightly from corner topRight: { x: roomWidth - wallThickness - 50, y: wallThickness + 50 }, bottomLeft: { x: wallThickness + 50, y: roomHeight - wallThickness - 50 }, bottomRight: { x: roomWidth - wallThickness - 50, y: roomHeight - wallThickness - 50 } }; var roomBackgroundImages = ['backgroundImage1', 'backgroundImage2', 'backgroundImage3', 'backgroundImage4', 'backgroundImage5']; var totalRooms = 5; // --- Centralized Skill Data --- var heroSkillPoints = 3; // Starting skill points var skillDefinitions = { // --- Circuit of Ascension --- 'Data Spike': { points: 0, max: 5, tree: 'The Circuit of Ascension', calcData: { increases: [5, 4, 3, 2, 1] }, descriptionFn: function descriptionFn(points, calcData) { var currentBenefit = 0; for (var i = 0; i < points; i++) { currentBenefit += calcData.increases[i]; } if (points < this.max) { var nextIncrease = calcData.increases[points]; return points === 0 ? 'Increases critical strike chance by 5%.' : 'Increases critical strike chance by ' + nextIncrease + '%. (Currently ' + currentBenefit + '%)'; } else { return 'Maximum rank reached. Your critical hit chance is increased by 15%.'; } } }, 'Reflex Accelerator': { points: 0, max: 5, tree: 'The Circuit of Ascension', calcData: { increases: [10, 8, 6, 4, 2] }, descriptionFn: function descriptionFn(points, calcData) { var currentBenefit = 0; for (var i = 0; i < points; i++) { currentBenefit += calcData.increases[i]; } if (points < this.max) { var nextIncrease = calcData.increases[points]; return points === 0 ? 'Boosts movement speed by 10%.' : 'Boosts movement speed by ' + nextIncrease + '% (Currently ' + currentBenefit + '%)'; } else { return 'Maximum rank reached. Your movement speed is increased by 30%.'; } } }, 'Overclock Core': { points: 0, max: 5, tree: 'The Circuit of Ascension', calcData: { attackSpeedIncreases: [2.5, 2.0, 1.5, 1.0, 0.5], energyRegenIncreases: [2.0, 1.6, 1.2, 0.8, 0.4] }, descriptionFn: function descriptionFn(points, calcData) { var currentAttackSpeedBenefit = 0; var currentEnergyRegenBenefit = 0; for (var i = 0; i < points; i++) { currentAttackSpeedBenefit += calcData.attackSpeedIncreases[i]; currentEnergyRegenBenefit += calcData.energyRegenIncreases[i]; } if (points < this.max) { var nextAttackSpeedIncrease = calcData.attackSpeedIncreases[points]; var nextEnergyRegenIncrease = calcData.energyRegenIncreases[points]; return points === 0 ? 'Increases attack speed by ' + nextAttackSpeedIncrease.toFixed(1) + '% and energy regeneration by ' + nextEnergyRegenIncrease.toFixed(1) + '%.' : 'Increases attack speed by ' + nextAttackSpeedIncrease.toFixed(1) + '% and energy regeneration by ' + nextEnergyRegenIncrease.toFixed(1) + '% (Currently ' + currentAttackSpeedBenefit.toFixed(1) + '% / ' + currentEnergyRegenBenefit.toFixed(1) + '%).'; } else { return 'Maximum rank reached. Your attack speed is increased by 7.5% and energy regeneration by 6.0%.'; } } }, 'Neural Hijack': { points: 0, max: 5, tree: 'The Circuit of Ascension', calcData: { durationIncreases: [3, 4, 5, 6, 8], cooldownDecreases: [30, 27, 24, 21, 15] }, descriptionFn: function descriptionFn(points, calcData) { var currentDuration = 0; var currentCooldown = 0; if (points > 0) { currentDuration = calcData.durationIncreases[points - 1]; currentCooldown = calcData.cooldownDecreases[points - 1]; } if (points < this.max) { var nextDuration = calcData.durationIncreases[points]; var nextCooldown = calcData.cooldownDecreases[points]; return points === 0 ? 'Temporarily converts an enemy to fight for you for ' + nextDuration + ' seconds (Cooldown: ' + nextCooldown + 's).' : 'Converts an enemy for ' + nextDuration + ' seconds (Cooldown: ' + nextCooldown + 's) (Currently ' + currentDuration + 's / ' + currentCooldown + 's).'; } else { return 'Maximum rank reached. Converts an enemy to fight for you for 8 seconds with a 15 second cooldown.'; } } }, 'Dodge Matrix': { points: 0, max: 5, tree: 'The Circuit of Ascension', calcData: { increases: [10, 8, 6, 4, 2] }, descriptionFn: function descriptionFn(points, calcData) { var currentBenefit = 0; for (var i = 0; i < points; i++) { currentBenefit += calcData.increases[i]; } if (points < this.max) { var nextIncrease = calcData.increases[points]; return points === 0 ? 'Grants a 10% chance to dodge enemy attacks.' : 'Grants an additional ' + nextIncrease + '% chance to dodge enemy attacks (Currently ' + currentBenefit + '%)'; } else { return 'Maximum rank reached. Your chance to dodge enemy attacks is increased by 30%.'; } } }, 'Power Surge': { points: 0, max: 5, tree: 'The Circuit of Ascension', calcData: { // Existing calcData, just add cooldown damageMultipliers: [1.0, 1.25, 1.5, 1.75, 2.0], radiusSizes: [2.0, 2.5, 3.0, 3.5, 4.0], cooldown: 10 // Cooldown in seconds }, descriptionFn: function descriptionFn(points, calcData) { var currentDamageMultiplier = 0; var currentRadius = 0; if (points > 0) { currentDamageMultiplier = calcData.damageMultipliers[points - 1]; currentRadius = calcData.radiusSizes[points - 1]; } if (points < this.max) { var nextDamageMultiplier = calcData.damageMultipliers[points]; var nextRadius = calcData.radiusSizes[points]; return points === 0 ? 'Releases an energy burst dealing 100% magic damage in a 2.0-unit radius.' : 'Deals ' + (nextDamageMultiplier * 100).toFixed(0) + '% magic damage in a ' + nextRadius.toFixed(1) + '-unit radius (Currently ' + (currentDamageMultiplier * 100).toFixed(0) + '% / ' + currentRadius.toFixed(1) + ')'; } else { return 'Maximum rank reached. Your energy burst deals 200% magic damage in a 4.0-unit radius.'; } } }, 'Overload Blast': { points: 0, max: 1, tree: 'The Circuit of Ascension', calcData: { // ADDED calcData block cooldown: 60 // Cooldown in seconds }, descriptionFn: function descriptionFn(points, calcData) { return 'Unleashes a massive explosion, dealing 300% fire damage to all enemies in the room (Cooldown: 60s).'; } }, // --- Echoes of Ancestry --- 'Ancient Wisdom': { points: 0, max: 5, tree: 'The Echoes of Ancestry', calcData: { increases: [5, 4, 3, 2, 1] }, descriptionFn: function descriptionFn(points, calcData) { var currentBenefit = 0; for (var i = 0; i < points; i++) { currentBenefit += calcData.increases[i]; } if (points < this.max) { var nextIncrease = calcData.increases[points]; return points === 0 ? 'Increases elemental critical hit chance by 5%.' : 'Increases elemental critical hit chance by ' + nextIncrease + '% (Currently ' + currentBenefit + '%).'; } else { return 'Maximum rank reached. Increases elemental critical hit chance by 15%.'; } } }, 'Elemental Mastery': { points: 0, max: 5, tree: 'The Echoes of Ancestry', calcData: { increases: [10, 8, 6, 4, 2] }, descriptionFn: function descriptionFn(points, calcData) { var currentBenefit = 0; for (var i = 0; i < points; i++) { currentBenefit += calcData.increases[i]; } if (points < this.max) { var nextIncrease = calcData.increases[points]; return points === 0 ? 'Increases elemental damage by 10%.' : 'Increases elemental damage by ' + nextIncrease + '% (Currently ' + currentBenefit + '%).'; } else { return 'Maximum rank reached. Increases elemental damage by 30%.'; } } }, 'Warrior Spirit': { points: 0, max: 5, tree: 'The Echoes of Ancestry', calcData: { increases: [10, 8, 6, 4, 2] }, descriptionFn: function descriptionFn(points, calcData) { var currentBenefit = 0; for (var i = 0; i < points; i++) { currentBenefit += calcData.increases[i]; } if (points < this.max) { var nextIncrease = calcData.increases[points]; return points === 0 ? 'Increases melee attack speed by 10%.' : 'Increases melee attack speed by ' + nextIncrease + '% (Currently ' + currentBenefit + '%).'; } else { return 'Maximum rank reached. Increases melee attack speed by 30%.'; } } }, 'Beast Tamer': { points: 0, max: 5, tree: 'The Echoes of Ancestry', calcData: { durations: [2, 3, 4, 4, 4], cooldowns: [20, 18, 16, 16, 16] }, descriptionFn: function descriptionFn(points, calcData) { var currentDuration = 0; var currentCooldown = 0; if (points > 0) { currentDuration = calcData.durations[points - 1]; currentCooldown = calcData.cooldowns[points - 1]; } if (points < this.max) { var nextDuration = calcData.durations[points]; var nextCooldown = calcData.cooldowns[points]; return points === 0 ? 'Summons a spirit beast for 2 seconds (Cooldown: 20s).' : 'Summons a spirit beast for ' + nextDuration + ' seconds (Cooldown: ' + nextCooldown + 's) (Currently ' + currentDuration + 's / ' + currentCooldown + 's).'; } else { return 'Maximum rank reached. Summons a spirit beast for 4 seconds (Cooldown: 16s).'; } } }, 'Herbal Remedies': { points: 0, max: 5, tree: 'The Echoes of Ancestry', calcData: { increases: [0.5, 0.6, 0.7, 0.8, 1.0] }, descriptionFn: function descriptionFn(points, calcData) { var currentBenefit = 0; if (points > 0) { currentBenefit = calcData.increases[points - 1]; } if (points < this.max) { var nextIncrease = calcData.increases[points]; return points === 0 ? 'Heals 0.5% of max HP per second during combat.' : 'Heals ' + nextIncrease.toFixed(1) + '% of max HP per second (Currently ' + currentBenefit.toFixed(1) + '%).'; } else { return 'Maximum rank reached. Heals 1.0% of max HP per second during combat.'; } } }, 'Spirit Guidance': { points: 0, max: 5, tree: 'The Echoes of Ancestry', calcData: { reductions: [5, 4, 3, 2, 1] }, descriptionFn: function descriptionFn(points, calcData) { var currentBenefit = 0; for (var i = 0; i < points; i++) { currentBenefit += calcData.reductions[i]; } if (points < this.max) { var nextReduction = calcData.reductions[points]; return points === 0 ? 'Reduces incoming damage by 5%.' : 'Reduces incoming damage by ' + nextReduction + '% (Currently ' + currentBenefit + '%).'; } else { return 'Maximum rank reached. Reduces incoming damage by 15%.'; } } }, 'Legendary Ancestor': { points: 0, max: 1, tree: 'The Echoes of Ancestry', calcData: { // ADDED calcData block cooldown: 60 // Cooldown in seconds }, descriptionFn: function descriptionFn(points, calcData) { return 'Temporarily increases your damage by 100% and defense by 50% for 6 seconds (Cooldown: 60s).'; } }, // --- Forge of Possibilities --- 'Adaptive Construction': { points: 0, max: 5, tree: 'The Forge of Possibilities', calcData: { fireDmgPerRank: 5, ignitePerRank: 2 }, descriptionFn: function descriptionFn(points, calcData) { var currentFire = points * calcData.fireDmgPerRank; var currentIgnite = points * calcData.ignitePerRank; if (points === 0) { return "Imbue your weapon with searing flame, adding 5% fire damage and 2% chance to ignite on hit."; } else if (points < this.max) { return "Imbue your weapon with searing flame, adding " + (points + 1) * calcData.fireDmgPerRank + "% fire damage and " + (points + 1) * calcData.ignitePerRank + "% chance to ignite on hit (Currently " + currentFire + "% fire damage / " + currentIgnite + "% ignite)."; } else { return "Maximum rank reached. Imbue your weapon with searing flame, adding 25% fire damage and 10% chance to ignite on hit."; } } }, 'Elemental Infusion': { points: 0, max: 5, tree: 'The Forge of Possibilities', calcData: { procs: [10, 8, 6, 4, 2] }, descriptionFn: function descriptionFn(points, calcData) { var currentProc = 0; for (var i = 0; i < points; i++) { currentProc += calcData.procs[i]; } if (points === 0) { return "Every attack has a 10% chance to inflict a random elemental effect."; } else if (points < this.max) { var nextProc = calcData.procs[points]; return "Every attack has a " + (currentProc + nextProc) + "% chance to inflict a random elemental effect (Currently " + currentProc + "%)."; } else { return "Maximum rank reached. Every attack has a 30% chance to inflict a random elemental effect."; } } }, 'Modular Enhancement': { points: 0, max: 5, tree: 'The Forge of Possibilities', calcData: { procPerRank: 3 }, descriptionFn: function descriptionFn(points, calcData) { var currentProc = points * calcData.procPerRank; if (points === 0) { return "Each hit has a 3% chance to instantly ready your weapon for an extra strike."; } else if (points < this.max) { return "Each hit has a " + (points + 1) * calcData.procPerRank + "% chance to instantly ready your weapon for an extra strike (Currently " + currentProc + "%)."; } else { return "Maximum rank reached. Each hit has a 15% chance to instantly ready your weapon for an extra strike."; } } }, 'Resourceful Salvage': { points: 0, max: 5, tree: 'The Forge of Possibilities', calcData: { procChances: [2, 3, 4, 5, 6], buffStrengths: [10, 12, 14, 16, 20], buffDurations: [5, 7, 9, 12, 15] }, descriptionFn: function descriptionFn(points, calcData) { var currentProc = 0, currentBuff = 0, currentDur = 0; if (points > 0) { currentProc = calcData.procChances[points - 1]; currentBuff = calcData.buffStrengths[points - 1]; currentDur = calcData.buffDurations[points - 1]; } if (points === 0) { return "Killing an enemy has a 2% chance to grant +10% skill‑point gain for 5 seconds."; } else if (points < this.max) { var nextProc = calcData.procChances[points]; var nextBuff = calcData.buffStrengths[points]; var nextDur = calcData.buffDurations[points]; return "Killing an enemy has a " + nextProc + "% chance to grant +" + nextBuff + "% skill‑point gain for " + nextDur + " seconds (Currently " + currentProc + "% / +" + currentBuff + "% / " + currentDur + "s)."; } else { return "Maximum rank reached. Killing an enemy has a 6% chance to grant +20% skill‑point gain for 15 seconds."; } } }, 'Hybrid Crafting': { // NOTE: Original code had max 5, description implies max 1. Assuming max 1. points: 0, max: 1, tree: 'The Forge of Possibilities', descriptionFn: function descriptionFn(points, calcData) { return 'Fuse melee and ranged traits: your arrows cleave a nearby target at short range (30% splash), and sword swings have a 20% chance to fire a projectile at 50% damage.'; } }, 'Runic Slots': { // Renamed from Gem Harvester based on original code usage points: 0, max: 5, tree: 'The Forge of Possibilities', calcData: { dropChances: [5, 8, 12, 16, 20], buffPotencies: [5, 7, 9, 12, 15], buffDurations: [3, 4, 5, 6, 8] }, descriptionFn: function descriptionFn(points, calcData) { var currentDrop = 0, currentBuff = 0, currentDur = 0; if (points > 0) { currentDrop = calcData.dropChances[points - 1]; currentBuff = calcData.buffPotencies[points - 1]; currentDur = calcData.buffDurations[points - 1]; } if (points === 0) { return "Killing an enemy has a 5% chance to drop a runic gem granting a random +5% buff for 3 seconds."; } else if (points < this.max) { var nextDrop = calcData.dropChances[points]; var nextBuff = calcData.buffPotencies[points]; var nextDur = calcData.buffDurations[points]; return "Killing an enemy has an " + nextDrop + "% chance to drop a runic gem granting a random +" + nextBuff + "% buff for " + nextDur + " seconds (Currently " + currentDrop + "% / +" + currentBuff + "% / " + currentDur + "s)."; } else { return "Maximum rank reached. Killing an enemy has a 20% chance to drop a runic gem granting a random +15% buff for 8 seconds."; } } }, 'Masterwork Creation': { points: 0, max: 3, // Corrected max based on calcData length tree: 'The Forge of Possibilities', calcData: { // Existing calcData, just add cooldown dmgBonuses: [30, 60, 90], cleaveCounts: [1, 2, 3], shockwaveRadii: [1, 2, 3], cooldown: 60 // Cooldown in seconds }, descriptionFn: function descriptionFn(points, calcData) { if (points === 0) { return "Transforms your weapon for 6s, granting +30% damage, arrows cleave 1 extra target, and melee triggers a 1‑unit shockwave (Cooldown: 60s)."; } else if (points < this.max) { var currentDmg = calcData.dmgBonuses[points - 1]; var currentCleave = calcData.cleaveCounts[points - 1]; var currentRadius = calcData.shockwaveRadii[points - 1]; var nextDmg = calcData.dmgBonuses[points]; var nextCleave = calcData.cleaveCounts[points]; var nextRadius = calcData.shockwaveRadii[points]; return "Transforms your weapon for 6s, granting +" + nextDmg + "% damage, arrows cleave " + nextCleave + " extra target" + (nextCleave > 1 ? "s" : "") + ", and melee triggers a " + nextRadius + "-unit shockwave (Currently +" + currentDmg + "% / " + currentCleave + " cleave / " + currentRadius + "-unit shockwave). Cooldown: 60s."; } else { return "Maximum rank reached. Transforms your weapon for 6s, granting +90% damage, arrows cleave 3 extra targets, and melee triggers a 3‑unit shockwave (Cooldown: 60s)."; } } }, // --- Prism of Potential --- (Redefined with specific dynamic scaling like Data Spike example) 'Red Radiance': { points: 0, max: 5, tree: 'The Prism of Potential', calcData: { igniteIncreases: [4, 3, 2, 2, 1], // Ignite % gain per rank dmgIncreases: [3, 2, 2, 1, 1] // Damage % gain per rank }, descriptionFn: function descriptionFn(points, calcData) { // Named function for clarity var currentIgnite = 0, currentDmg = 0; var totalIgnite = 0, totalDmg = 0; // Calculate current and total benefits by summing increases for (var i = 0; i < this.max; i++) { if (i < points) { currentIgnite += calcData.igniteIncreases[i]; currentDmg += calcData.dmgIncreases[i]; } totalIgnite += calcData.igniteIncreases[i]; totalDmg += calcData.dmgIncreases[i]; } if (points < this.max) { var nextIgniteGain = calcData.igniteIncreases[points]; var nextDmgGain = calcData.dmgIncreases[points]; var nextTotalIgnite = currentIgnite + nextIgniteGain; var nextTotalDmg = currentDmg + nextDmgGain; // Format matches user examples if (points === 0) { // For rank 0 -> 1, show the values for rank 1 directly return 'Adds ' + nextTotalIgnite + '% chance to ignite enemies and +' + nextTotalDmg + '% damage.'; } else { // For ranks 1+, show gain and current total return 'Adds ' + nextIgniteGain + '% ignite chance and +' + nextDmgGain + '% damage (Currently ' + currentIgnite + '% / +' + currentDmg + '%).'; } } else { // Show totals at max rank return 'Maximum rank reached. Adds ' + totalIgnite + '% chance to ignite and +' + totalDmg + '% damage.'; } } }, 'Blue Bulwark': { points: 0, max: 5, tree: 'The Prism of Potential', calcData: { // Values *at* each rank (1 to 5) derived from user description (+8%, +6%, etc.) defValues: [8, 14, 19, 23, 25], // Total Def % at ranks 1-5 procValues: [5, 7, 9, 11, 13], // Total Proc % at ranks 1-5 blockCounts: [1, 1, 1, 2, 3] // Blocks at ranks 1-5 }, descriptionFn: function descriptionFn(points, calcData) { var currentDef = 0, currentProc = 0, currentBlocks = 0; var maxDef = calcData.defValues[this.max - 1]; var maxProc = calcData.procValues[this.max - 1]; var maxBlocks = calcData.blockCounts[this.max - 1]; if (points > 0) { // Get values for the current rank currentDef = calcData.defValues[points - 1]; currentProc = calcData.procValues[points - 1]; currentBlocks = calcData.blockCounts[points - 1]; } if (points < this.max) { // Get values for the next rank var nextDef = calcData.defValues[points]; var nextProc = calcData.procValues[points]; var nextBlocks = calcData.blockCounts[points]; var nextBlockText = nextBlocks + (nextBlocks > 1 ? ' attacks' : ' attack'); // Format matches user examples if (points === 0) { // For rank 0 -> 1 return 'Increases armor & magic resist by ' + nextDef + '%; ' + nextProc + '% chance to block ' + nextBlockText + ' with an ice shield.'; } else { // For ranks 1+ var currentBlockText = currentBlocks + (currentBlocks > 1 ? ' attacks' : ' attack'); // Show gain for def, show next value for proc chance/blocks var defGain = nextDef - currentDef; return 'Increases armor & magic resist by ' + defGain + '%; ' + nextProc + '% chance to block ' + nextBlockText + ' (Currently ' + currentDef + '% / ' + currentProc + '% / blocks ' + currentBlockText + ').'; } } else { var maxBlockText = maxBlocks + (maxBlocks > 1 ? ' attacks' : ' attack'); return 'Maximum rank reached. Increases armor & magic resist by ' + maxDef + '%; ' + maxProc + '% chance to block ' + maxBlockText + '.'; } } }, 'Green Growth': { points: 0, max: 5, tree: 'The Prism of Potential', calcData: { // Values *at* each rank (1 to 5) based on user description (decreasing values) regenValues: [1.5, 1.3, 1.1, 0.8, 0.5], // % MaxHP/sec procValues: [6, 5, 4, 3, 2] // % Proc chance }, descriptionFn: function descriptionFn(points, calcData) { var currentRegen = 0, currentProc = 0; var maxRegen = calcData.regenValues[this.max - 1]; // Value at rank 5 var maxProc = calcData.procValues[this.max - 1]; // Value at rank 5 if (points > 0) { // Get values for the current rank currentRegen = calcData.regenValues[points - 1]; currentProc = calcData.procValues[points - 1]; } if (points < this.max) { // Get values for the next rank var nextRegen = calcData.regenValues[points]; var nextProc = calcData.procValues[points]; // Format matches user examples if (points === 0) { // For rank 0 -> 1 return 'Restores ' + nextRegen.toFixed(1) + '% max HP/sec (for 5s) and grants a ' + nextProc + '% chance for attacks to apply a minor HoT.'; } else { // For ranks 1+ return 'Restores ' + nextRegen.toFixed(1) + '% max HP/sec (for 5s) with a ' + nextProc + '% chance (Currently ' + currentRegen.toFixed(1) + '% / ' + currentProc + '%).'; } } else { return 'Maximum rank reached. Restores ' + maxRegen.toFixed(1) + '% max HP/sec (for 5s) and grants a ' + maxProc + '% chance for attacks to apply HoT.'; } } }, 'Yellow Zephyr': { points: 0, max: 5, tree: 'The Prism of Potential', calcData: { moveIncreases: [6, 5, 4, 3, 2], // Move Spd % gain per rank atkIncreases: [6, 5, 4, 3, 2] // Attack Spd % gain per rank }, descriptionFn: function descriptionFn(points, calcData) { var currentMove = 0, currentAtk = 0; var totalMove = 0, totalAtk = 0; // Calculate current and total benefits for (var i = 0; i < this.max; i++) { if (i < points) { currentMove += calcData.moveIncreases[i]; currentAtk += calcData.atkIncreases[i]; } totalMove += calcData.moveIncreases[i]; totalAtk += calcData.atkIncreases[i]; } if (points < this.max) { var nextMoveGain = calcData.moveIncreases[points]; var nextAtkGain = calcData.atkIncreases[points]; var nextTotalMove = currentMove + nextMoveGain; var nextTotalAtk = currentAtk + nextAtkGain; // Format matches user examples if (points === 0) { // For rank 0 -> 1 return 'Boosts movement & attack speed by ' + nextTotalMove + '%.'; } else { // For ranks 1+ // Use gain for both here as per example "Boosts movement & attack speed by 5%" return 'Boosts movement & attack speed by ' + nextMoveGain + '% (Currently +' + currentMove + '% / +' + currentAtk + '%).'; } } else { return 'Maximum rank reached. Boosts movement speed by ' + totalMove + '% and attack speed by ' + totalAtk + '%.'; } } }, 'Indigo Insight': { points: 0, max: 5, tree: 'The Prism of Potential', calcData: { critIncreases: [5, 4, 3, 2, 1], // Crit % gain per rank reflectIncreases: [10, 8, 6, 4, 2] // Reflect % gain per rank }, descriptionFn: function descriptionFn(points, calcData) { var currentCrit = 0, currentReflect = 0; var totalCrit = 0, totalReflect = 0; // Calculate current and total benefits for (var i = 0; i < this.max; i++) { if (i < points) { currentCrit += calcData.critIncreases[i]; currentReflect += calcData.reflectIncreases[i]; } totalCrit += calcData.critIncreases[i]; totalReflect += calcData.reflectIncreases[i]; } if (points < this.max) { var nextCritGain = calcData.critIncreases[points]; var nextReflectGain = calcData.reflectIncreases[points]; var nextTotalCrit = currentCrit + nextCritGain; var nextTotalReflect = currentReflect + nextReflectGain; // Format matches user examples if (points === 0) { // For rank 0 -> 1 return 'Increases crit chance by ' + nextTotalCrit + '% and reflects ' + nextTotalReflect + '% of incoming damage.'; } else { // For ranks 1+ return 'Increases crit chance by ' + nextCritGain + '% and reflects ' + nextReflectGain + '% damage (Currently +' + currentCrit + '% / ' + currentReflect + '%).'; } } else { return 'Maximum rank reached. Increases crit chance by ' + totalCrit + '% and reflects ' + totalReflect + '% damage.'; } } }, 'Chromatic Fusion': { points: 0, max: 5, tree: 'The Prism of Potential', calcData: { dmgIncreases: [10, 8, 6, 4, 2] // Bonus Elemental Damage % gain per rank }, descriptionFn: function descriptionFn(points, calcData) { var currentDmg = 0; var totalDmg = 0; // Calculate current and total benefits for (var i = 0; i < this.max; i++) { if (i < points) { currentDmg += calcData.dmgIncreases[i]; } totalDmg += calcData.dmgIncreases[i]; } if (points < this.max) { var nextDmgGain = calcData.dmgIncreases[points]; var nextTotalDmg = currentDmg + nextDmgGain; // Format matches user examples if (points === 0) { // For rank 0 -> 1 return 'All attacks deal an additional ' + nextTotalDmg + '% elemental damage.'; } else { // For ranks 1+ return 'All attacks deal an additional ' + nextDmgGain + '% elemental damage (Currently +' + currentDmg + '%).'; } } else { return 'Maximum rank reached. All attacks deal an additional ' + totalDmg + '% elemental damage.'; } } }, 'Prismatic Ascendance': { points: 0, max: 5, tree: 'The Prism of Potential', calcData: { // Existing calcData, just add cooldown potencyValues: [12, 10, 8, 6, 4], durationValues: [6, 8, 10, 12, 15], cooldown: 45 // Example Cooldown in seconds }, descriptionFn: function descriptionFn(points, calcData) { var currentPotency = 0, currentDuration = 0; var maxPotency = calcData.potencyValues[this.max - 1]; // Value at rank 5 var maxDuration = calcData.durationValues[this.max - 1]; // Value at rank 5 if (points > 0) { // Get values for the current rank currentPotency = calcData.potencyValues[points - 1]; currentDuration = calcData.durationValues[points - 1]; } if (points < this.max) { // Get values for the next rank var nextPotency = calcData.potencyValues[points]; var nextDuration = calcData.durationValues[points]; // Format matches user examples if (points === 0) { // For rank 0 -> 1 return 'Empowers all elemental buffs by +' + nextPotency + '% for ' + nextDuration + ' seconds.'; } else { // For ranks 1+ return 'Empowers all elemental buffs by +' + nextPotency + '% for ' + nextDuration + ' seconds (Currently +' + currentPotency + '% / ' + currentDuration + 's).'; } } else { return 'Maximum rank reached. Empowers elemental buffs by +' + maxPotency + '% for ' + maxDuration + ' seconds.'; } } }, // --- End Prism of Potential --- // --- The Nexus of Chaos --- (Redefined with specific dynamic scaling) 'Chaotic Spark': { points: 0, max: 5, tree: 'The Nexus of Chaos', calcData: { procIncreases: [5, 4, 3, 2, 1] // Proc % gain per rank (Total 15%) }, descriptionFn: function descriptionFn(points, calcData) { var currentProc = 0; var totalProc = 0; for (var i = 0; i < this.max; i++) { if (i < points) { currentProc += calcData.procIncreases[i]; } totalProc += calcData.procIncreases[i]; } if (points < this.max) { var nextProcGain = calcData.procIncreases[points]; var nextTotalProc = currentProc + nextProcGain; var desc = 'Your attacks have a ' + nextProcGain + '% chance to inflict a random elemental effect (fire, ice, lightning, or poison) for 2 seconds.'; if (points === 0) { // For rank 0 -> 1 desc = 'Your attacks have a ' + nextTotalProc + '% chance to inflict a random elemental effect for 2 seconds.'; } else { // For ranks 1+ desc = 'Your attacks have an additional ' + nextProcGain + '% chance to inflict a random elemental effect for 2 seconds (Currently ' + currentProc + '%).'; } return desc; } else { return 'Maximum rank reached. Your attacks have a ' + totalProc + '% chance to inflict a random elemental effect for 2 seconds.'; } } }, 'Entropy Shield': { points: 0, max: 5, tree: 'The Nexus of Chaos', calcData: { baseProc: 5, procPerRank: 2, // Proc scales 5, 7, 9, 11, 13 baseReflect: 5, reflectPerRank: 2, // Reflect scales 5, 7, 9, 11, 13 blockCounts: [1, 1, 1, 2, 3] // Blocks at ranks 1-5 }, descriptionFn: function descriptionFn(points, calcData) { var currentProc = calcData.baseProc + (points > 0 ? (points - 1) * calcData.procPerRank : -calcData.procPerRank); // Handle rank 0 case for current var currentReflect = calcData.baseReflect + (points > 0 ? (points - 1) * calcData.reflectPerRank : -calcData.reflectPerRank); var currentBlocks = points > 0 ? calcData.blockCounts[points - 1] : 0; // Blocks at current rank (0 if rank 0) var maxProc = calcData.baseProc + (this.max - 1) * calcData.procPerRank; var maxReflect = calcData.baseReflect + (this.max - 1) * calcData.reflectPerRank; var maxBlocks = calcData.blockCounts[this.max - 1]; if (points < this.max) { var nextProc = calcData.baseProc + points * calcData.procPerRank; // Proc at next rank var nextReflect = calcData.baseReflect + points * calcData.reflectPerRank; // Reflect at next rank var nextBlocks = calcData.blockCounts[points]; // Blocks at next rank var nextBlockText = nextBlocks + (nextBlocks > 1 ? ' hits' : ' hit'); if (points === 0) { return 'When struck, there is a ' + nextProc + '% chance to block the attack and reflect ' + nextReflect + '% of its damage back (blocks ' + nextBlockText + ').'; } else { var currentBlockText = currentBlocks + (currentBlocks > 1 ? ' hits' : ' hit'); // Show next total values, compare to current in parentheses return 'When struck, there is a ' + nextProc + '% chance to block (' + nextBlockText + ') and reflect ' + nextReflect + '% damage (Currently ' + currentProc + '% / ' + currentReflect + '% / blocks ' + currentBlockText + ').'; } } else { var maxBlockText = maxBlocks + (maxBlocks > 1 ? ' hits' : ' hit'); return 'Maximum rank reached. ' + maxProc + '% chance to block (' + maxBlockText + ') and reflect ' + maxReflect + '% damage.'; } } }, 'Unstable Rift': { points: 0, max: 5, tree: 'The Nexus of Chaos', calcData: { baseProc: 5, procPerRank: 2 // Proc scales 5, 7, 9, 11, 13 }, descriptionFn: function descriptionFn(points, calcData) { var currentProc = calcData.baseProc + (points > 0 ? (points - 1) * calcData.procPerRank : -calcData.procPerRank); var maxProc = calcData.baseProc + (this.max - 1) * calcData.procPerRank; if (points < this.max) { var nextProc = calcData.baseProc + points * calcData.procPerRank; // Proc at next rank if (points === 0) { return 'On hit, there is a ' + nextProc + '% chance to open a chaotic rift that applies a random debuff for 4 seconds.'; } else { return 'On hit, there is a ' + nextProc + '% chance to open a chaotic rift (random debuff, 4s) (Currently ' + currentProc + '%).'; } } else { return 'Maximum rank reached. ' + maxProc + '% chance on hit to open a chaotic rift (random debuff, 4s).'; } } }, 'Anomaly Shift': { points: 0, max: 5, tree: 'The Nexus of Chaos', calcData: { baseProc: 5, procPerRank: 2, // Proc scales 5, 7, 9, 11, 13 baseDur: 3, durPerRank: 1 // Duration scales 3, 4, 5, 6, 7 }, descriptionFn: function descriptionFn(points, calcData) { var currentProc = calcData.baseProc + (points > 0 ? (points - 1) * calcData.procPerRank : -calcData.procPerRank); var currentDur = calcData.baseDur + (points > 0 ? (points - 1) * calcData.durPerRank : -calcData.durPerRank); var maxProc = calcData.baseProc + (this.max - 1) * calcData.procPerRank; var maxDur = calcData.baseDur + (this.max - 1) * calcData.durPerRank; if (points < this.max) { var nextProc = calcData.baseProc + points * calcData.procPerRank; // Proc at next rank var nextDur = calcData.baseDur + points * calcData.durPerRank; // Duration at next rank if (points === 0) { return 'After killing an enemy, you have a ' + nextProc + '% chance to gain a random buff (speed, regen, or crit) for ' + nextDur + ' seconds.'; } else { return 'After killing an enemy, ' + nextProc + '% chance to gain a random buff for ' + nextDur + ' seconds (Currently ' + currentProc + '% / ' + currentDur + 's).'; } } else { return 'Maximum rank reached. ' + maxProc + '% chance on kill to gain a random buff for ' + maxDur + ' seconds.'; } } }, 'Cascade of Disorder': { points: 0, max: 5, tree: 'The Nexus of Chaos', calcData: { baseProc: 5, procPerRank: 3, // Proc scales 5, 8, 11, 14, 17 baseDmg: 50, dmgPerRank: 12.5, // Damage scales 50, 62.5, 75, 87.5, 100 baseRadius: 1, radiusPerRank: 0.2 // Radius scales 1, 1.2, 1.4, 1.6, 1.8 }, descriptionFn: function descriptionFn(points, calcData) { var currentProc = calcData.baseProc + (points > 0 ? (points - 1) * calcData.procPerRank : -calcData.procPerRank); var currentDmg = calcData.baseDmg + (points > 0 ? (points - 1) * calcData.dmgPerRank : -calcData.dmgPerRank); var currentRadius = calcData.baseRadius + (points > 0 ? (points - 1) * calcData.radiusPerRank : -calcData.radiusPerRank); var maxProc = calcData.baseProc + (this.max - 1) * calcData.procPerRank; var maxDmg = calcData.baseDmg + (this.max - 1) * calcData.dmgPerRank; var maxRadius = calcData.baseRadius + (this.max - 1) * calcData.radiusPerRank; if (points < this.max) { var nextProc = calcData.baseProc + points * calcData.procPerRank; var nextDmg = calcData.baseDmg + points * calcData.dmgPerRank; var nextRadius = calcData.baseRadius + points * calcData.radiusPerRank; if (points === 0) { return 'Your attacks have a ' + nextProc + '% chance to trigger a chain explosion dealing ' + nextDmg.toFixed(1) + '% AoE damage in a ' + nextRadius.toFixed(1) + '‑unit radius.'; } else { return 'Your attacks have a ' + nextProc + '% chance to trigger a chain explosion (' + nextDmg.toFixed(1) + '% AoE / ' + nextRadius.toFixed(1) + 'u radius) (Currently ' + currentProc + '% / ' + currentDmg.toFixed(1) + '% / ' + currentRadius.toFixed(1) + 'u).'; } } else { return 'Maximum rank reached. ' + maxProc + '% chance to trigger explosion (' + maxDmg.toFixed(1) + '% AoE / ' + maxRadius.toFixed(1) + 'u radius).'; } } }, 'Fractured Fate': { points: 0, max: 5, tree: 'The Nexus of Chaos', calcData: { // Use calculated increases to reach totals specified (3->12% proc, 10->20% CDR) baseProc: 3, procIncreases: [2, 2, 2, 3], // Sum = 9. Base+Sum = 3+9 = 12 baseCDR: 10, cdrIncreases: [2, 2, 3, 3] // Sum = 10. Base+Sum = 10+10 = 20 }, descriptionFn: function descriptionFn(points, calcData) { var currentProc = calcData.baseProc; var currentCDR = calcData.baseCDR; var totalProc = calcData.baseProc; var totalCDR = calcData.baseCDR; for (var i = 0; i < calcData.procIncreases.length; i++) { if (i < points - 1) { // Sum increases *up to the previous rank* for current value currentProc += calcData.procIncreases[i]; currentCDR += calcData.cdrIncreases[i]; } // Sum all increases for total value at max rank totalProc += calcData.procIncreases[i]; totalCDR += calcData.cdrIncreases[i]; } // Adjust currentProc/CDR if points is 0 if (points === 0) { currentProc = 0; // Or display base? Example implies showing next level directly currentCDR = 0; } if (points < this.max) { // Calculate next level's values var nextProc = calcData.baseProc; var nextCDR = calcData.baseCDR; for (var j = 0; j < points; j++) { // Sum increases up to the *next* rank nextProc += calcData.procIncreases[j]; nextCDR += calcData.cdrIncreases[j]; } if (points === 0) { return 'Each hit has a ' + nextProc + '% chance to strike twice or reduce all cooldowns by ' + nextCDR + '%.'; } else { // Show next total values, compare to current in parentheses return 'Each hit has a ' + nextProc + '% chance to strike twice or reduce cooldowns by ' + nextCDR + '% (Currently ' + currentProc + '% / ' + currentCDR + '%).'; } } else { return 'Maximum rank reached. ' + totalProc + '% chance to strike twice or reduce cooldowns by ' + totalCDR + '%.'; } } }, 'Reality Collapse': { points: 0, max: 3, tree: 'The Nexus of Chaos', // Max rank is 3 based on provided data calcData: { // Explicit values at ranks 1, 2, 3 durations: [6, 8, 10], defIgnores: [10, 20, 30], extraEffectChances: [15, 30, 50], disorientChances: [20, 30, 40], // Disorient duration fixed at 2s procBuffs: [10, 20, 30], // % Buff to other Nexus procs cooldowns: [60, 50, 40] }, descriptionFn: function descriptionFn(points, calcData) { var currentStats = {}; var maxStats = {}; // Get stats for max rank (index 2) maxStats.dur = calcData.durations[this.max - 1]; maxStats.def = calcData.defIgnores[this.max - 1]; maxStats.eff = calcData.extraEffectChances[this.max - 1]; maxStats.dis = calcData.disorientChances[this.max - 1]; maxStats.buf = calcData.procBuffs[this.max - 1]; maxStats.cd = calcData.cooldowns[this.max - 1]; if (points > 0) { // Get stats for current rank (index points-1) currentStats.dur = calcData.durations[points - 1]; currentStats.def = calcData.defIgnores[points - 1]; currentStats.eff = calcData.extraEffectChances[points - 1]; currentStats.dis = calcData.disorientChances[points - 1]; currentStats.buf = calcData.procBuffs[points - 1]; currentStats.cd = calcData.cooldowns[points - 1]; } if (points < this.max) { // Get stats for next rank (index points) var nextStats = {}; nextStats.dur = calcData.durations[points]; nextStats.def = calcData.defIgnores[points]; nextStats.eff = calcData.extraEffectChances[points]; nextStats.dis = calcData.disorientChances[points]; nextStats.buf = calcData.procBuffs[points]; nextStats.cd = calcData.cooldowns[points]; // Construct the description string piece by piece var desc = 'For ' + nextStats.dur + 's, ignore ' + nextStats.def + '% enemy defense, ' + nextStats.eff + '% chance on hit for extra chaotic effect, ' + nextStats.dis + '% chance to disorient enemies (2s), ' + 'and other Nexus skill proc chances increase by ' + nextStats.buf + '% ' + '(CD: ' + nextStats.cd + 's).'; if (points > 0) { // Add current stats in parentheses for comparison desc += ' (Currently ' + currentStats.dur + 's / ' + currentStats.def + '% / ' + currentStats.eff + '% / ' + currentStats.dis + '% / +' + currentStats.buf + '% / ' + currentStats.cd + 's CD)'; } return desc; } else { // Max rank description return 'Maximum rank reached. For ' + maxStats.dur + 's, ignore ' + maxStats.def + '% defense, ' + maxStats.eff + '% extra effect chance, ' + maxStats.dis + '% disorient chance (2s), ' + 'Nexus procs + ' + maxStats.buf + '% (CD: ' + maxStats.cd + 's).'; } } }, // --- End The Nexus of Chaos --- // --- The Symphony of Shadows --- (Redefined with specific dynamic scaling) 'Whispered Steps': { points: 0, max: 5, tree: 'The Symphony of Shadows', calcData: { procIncreases: [5, 4, 3, 2, 1] // Evade % gain per rank (Total 15%) }, descriptionFn: function descriptionFn(points, calcData) { var currentProc = 0; var totalProc = 0; for (var i = 0; i < this.max; i++) { if (i < points) { currentProc += calcData.procIncreases[i]; } totalProc += calcData.procIncreases[i]; } if (points < this.max) { var nextProcGain = calcData.procIncreases[points]; var nextTotalProc = currentProc + nextProcGain; if (points === 0) { // For rank 0 -> 1 return 'Move with Whispered Steps, evading ' + nextTotalProc + '% of incoming attacks.'; } else { // For ranks 1+ return 'Move with Whispered Steps, increasing evade chance by ' + nextProcGain + '% (Currently ' + currentProc + '% total).'; } } else { return 'Maximum rank reached. Move with Whispered Steps, evading ' + totalProc + '% of incoming attacks.'; } } }, 'Nightfall Veil': { points: 0, max: 5, tree: 'The Symphony of Shadows', calcData: { // Explicit values at ranks 1-5 durations: [2, 2.5, 3, 3.5, 4], // seconds cooldowns: [60, 55, 50, 45, 40] // seconds }, descriptionFn: function descriptionFn(points, calcData) { var currentDur = 0, currentCD = 0; var maxDur = calcData.durations[this.max - 1]; var maxCD = calcData.cooldowns[this.max - 1]; if (points > 0) { currentDur = calcData.durations[points - 1]; currentCD = calcData.cooldowns[points - 1]; } if (points < this.max) { var nextDur = calcData.durations[points]; var nextCD = calcData.cooldowns[points]; if (points === 0) { return 'Activate Nightfall Veil to cloak for ' + nextDur.toFixed(1) + ' seconds (Cooldown: ' + nextCD + 's).'; } else { return 'Activate Nightfall Veil to cloak for ' + nextDur.toFixed(1) + ' seconds (Cooldown: ' + nextCD + 's) (Currently ' + currentDur.toFixed(1) + 's / ' + currentCD + 's).'; } } else { return 'Maximum rank reached. Cloak for ' + maxDur.toFixed(1) + ' seconds (Cooldown: ' + maxCD + 's).'; } } }, 'Serrated Shade': { points: 0, max: 5, tree: 'The Symphony of Shadows', calcData: { // Explicit values at ranks 1-5 procValues: [20, 25, 30, 35, 40], // % Proc chance dpsValues: [2, 3, 4, 5, 6] // % Bleed DPS }, descriptionFn: function descriptionFn(points, calcData) { var currentProc = 0, currentDPS = 0; var maxProc = calcData.procValues[this.max - 1]; var maxDPS = calcData.dpsValues[this.max - 1]; if (points > 0) { currentProc = calcData.procValues[points - 1]; currentDPS = calcData.dpsValues[points - 1]; } if (points < this.max) { var nextProc = calcData.procValues[points]; var nextDPS = calcData.dpsValues[points]; if (points === 0) { return 'Attacks have a ' + nextProc + '% chance to cause bleed, dealing ' + nextDPS + '% of base damage per second for 5 seconds.'; } else { return 'Attacks have a ' + nextProc + '% chance to cause bleed, dealing ' + nextDPS + '% DPS for 5 seconds (Currently ' + currentProc + '% / ' + currentDPS + '%).'; } } else { return 'Maximum rank reached. ' + maxProc + '% chance to cause bleed, dealing ' + maxDPS + '% DPS for 5 seconds.'; } } }, 'Shadow Familiar': { points: 0, max: 5, tree: 'The Symphony of Shadows', calcData: { // Explicit values at ranks 1-5 durations: [5, 6, 7, 8, 10], // seconds slowValues: [10, 12, 14, 16, 20] // % Slow strength }, descriptionFn: function descriptionFn(points, calcData) { var currentDur = 0, currentSlow = 0; var maxDur = calcData.durations[this.max - 1]; var maxSlow = calcData.slowValues[this.max - 1]; if (points > 0) { currentDur = calcData.durations[points - 1]; currentSlow = calcData.slowValues[points - 1]; } if (points < this.max) { var nextDur = calcData.durations[points]; var nextSlow = calcData.slowValues[points]; if (points === 0) { return 'Summon your Shadow Familiar for ' + nextDur + ' seconds, slowing nearby enemies by ' + nextSlow + '%.'; } else { return 'Summon your Shadow Familiar for ' + nextDur + ' seconds, slowing enemies by ' + nextSlow + '% (Currently ' + currentDur + 's / ' + currentSlow + '%).'; } } else { return 'Maximum rank reached. Summon Familiar for ' + maxDur + ' seconds (slows ' + maxSlow + '%).'; } } }, 'Shadowstep': { // Note: Replacing Shadowstep based on your prompt points: 0, max: 5, tree: 'The Symphony of Shadows', calcData: { // Explicit values at ranks 1-5 distances: [3, 4, 5, 6, 8], // units imageDurs: [2, 2.5, 3, 3.5, 4], // seconds cooldowns: [20, 18, 16, 14, 12] // seconds }, descriptionFn: function descriptionFn(points, calcData) { var currentDist = 0, currentImgDur = 0, currentCD = 0; var maxDist = calcData.distances[this.max - 1]; var maxImgDur = calcData.imageDurs[this.max - 1]; var maxCD = calcData.cooldowns[this.max - 1]; if (points > 0) { currentDist = calcData.distances[points - 1]; currentImgDur = calcData.imageDurs[points - 1]; currentCD = calcData.cooldowns[points - 1]; } if (points < this.max) { var nextDist = calcData.distances[points]; var nextImgDur = calcData.imageDurs[points]; var nextCD = calcData.cooldowns[points]; if (points === 0) { return 'Use Shadowstep to blink ' + nextDist + ' units, leaving a distracting afterimage for ' + nextImgDur.toFixed(1) + ' seconds (Cooldown: ' + nextCD + 's).'; } else { return 'Use Shadowstep to blink ' + nextDist + ' units, leaving a ' + nextImgDur.toFixed(1) + 's afterimage (Cooldown: ' + nextCD + 's) (Currently ' + currentDist + 'u / ' + currentImgDur.toFixed(1) + 's / ' + currentCD + 's).'; } } else { return 'Maximum rank reached. Blink ' + maxDist + ' units, ' + maxImgDur.toFixed(1) + 's afterimage (Cooldown: ' + maxCD + 's).'; } } }, 'Umbral Echoes': { points: 0, max: 5, tree: 'The Symphony of Shadows', calcData: { // Explicit values at ranks 1-5 procValues: [10, 13, 16, 19, 22], // % Proc chance confusionDurs: [2, 2.5, 3, 3.5, 4] // seconds Duration }, descriptionFn: function descriptionFn(points, calcData) { var currentProc = 0, currentConfDur = 0; var maxProc = calcData.procValues[this.max - 1]; var maxConfDur = calcData.confusionDurs[this.max - 1]; if (points > 0) { currentProc = calcData.procValues[points - 1]; currentConfDur = calcData.confusionDurs[points - 1]; } if (points < this.max) { var nextProc = calcData.procValues[points]; var nextConfDur = calcData.confusionDurs[points]; if (points === 0) { return 'On hit, ' + nextProc + '% chance to spawn an echo that confuses enemies for ' + nextConfDur.toFixed(1) + ' seconds.'; } else { return 'On hit, ' + nextProc + '% chance to spawn an echo that confuses enemies for ' + nextConfDur.toFixed(1) + ' seconds (Currently ' + currentProc + '% / ' + currentConfDur.toFixed(1) + 's).'; } } else { return 'Maximum rank reached. ' + maxProc + '% chance on hit to spawn echo (confuses ' + maxConfDur.toFixed(1) + 's).'; } } }, 'Midnight Crescendo': { points: 0, max: 5, tree: 'The Symphony of Shadows', calcData: { // Explicit values at ranks 1-5 durations: [6, 7, 8, 9, 10], // seconds armorIgnores: [10, 15, 20, 25, 30], // % Armor Ignore speedBonuses: [10, 12, 14, 16, 18], // % Move Speed disorientChances: [20, 25, 30, 35, 40] // % Disorient Chance // Cooldown fixed at 60s }, descriptionFn: function descriptionFn(points, calcData) { var currentStats = { dur: 0, arm: 0, spd: 0, dis: 0 }; var maxStats = { dur: calcData.durations[this.max - 1], arm: calcData.armorIgnores[this.max - 1], spd: calcData.speedBonuses[this.max - 1], dis: calcData.disorientChances[this.max - 1] }; if (points > 0) { currentStats.dur = calcData.durations[points - 1]; currentStats.arm = calcData.armorIgnores[points - 1]; currentStats.spd = calcData.speedBonuses[points - 1]; currentStats.dis = calcData.disorientChances[points - 1]; } if (points < this.max) { var nextStats = { dur: calcData.durations[points], arm: calcData.armorIgnores[points], spd: calcData.speedBonuses[points], dis: calcData.disorientChances[points] }; var desc = 'Activate Midnight Crescendo for ' + nextStats.dur + ' seconds: ignore ' + nextStats.arm + '% armor, +' + nextStats.spd + '% move speed, and ' + nextStats.dis + '% chance to disorient on hit (Cooldown: 60s).'; if (points > 0) { desc += ' (Currently ' + currentStats.dur + 's / ' + currentStats.arm + '% / +' + currentStats.spd + '% / ' + currentStats.dis + '%)'; } return desc; } else { return 'Maximum rank reached. For ' + maxStats.dur + 's: ignore ' + maxStats.arm + '% armor, +' + maxStats.spd + '% move speed, and ' + maxStats.dis + '% disorient chance (Cooldown: 60s).'; } } } // --- End The Symphony of Shadows --- }; // --- Centralized Skill Tree Layout Data --- // This structure holds the background image, list of skills (nodes), // node positions, and the mapping of skill names to their icon asset names for each tree. var skillTreeLayouts = { 'The Circuit of Ascension': { backgroundAsset: 'popup_circuitofascension', nodes: ['Data Spike', 'Reflex Accelerator', 'Overclock Core', 'Neural Hijack', 'Dodge Matrix', 'Power Surge', 'Overload Blast'], positions: { // Positions from original code 'Data Spike': { x: 2048 / 4 - 50, y: 2732 / 6 - 50 + 100 }, 'Reflex Accelerator': { x: 2048 / 2, y: 2732 / 6 - 50 + 100 }, 'Overclock Core': { x: 3 * 2048 / 4 + 50, y: 2732 / 6 - 50 + 100 }, 'Neural Hijack': { x: 2048 / 4 - 50, y: 2 * 2732 / 6 + 100 }, 'Dodge Matrix': { x: 2048 / 2, y: 2 * 2732 / 6 + 100 }, 'Power Surge': { x: 3 * 2048 / 4 + 50, y: 2 * 2732 / 6 + 100 }, 'Overload Blast': { x: 2048 / 2, y: 3 * 2732 / 6 + 50 + 100 } }, icons: { // *** Replace these values with your actual asset names defined in LK.init.image *** 'Data Spike': 'DataSpike_icon', 'Reflex Accelerator': 'ReflexAccelerator_icon', 'Overclock Core': 'OverclockCore_icon', 'Neural Hijack': 'NeuralHijack_icon', 'Dodge Matrix': 'DodgeMatrix_icon', 'Power Surge': 'PowerSurge_icon', 'Overload Blast': 'OverloadBlast_icon' } }, 'The Echoes of Ancestry': { backgroundAsset: 'popup_echoesofancestry', nodes: ['Ancient Wisdom', 'Elemental Mastery', 'Warrior Spirit', 'Beast Tamer', 'Herbal Remedies', 'Spirit Guidance', 'Legendary Ancestor'], positions: { // Positions from original code 'Ancient Wisdom': { x: 2048 / 4 - 50, y: 2732 / 6 - 50 + 100 }, 'Elemental Mastery': { x: 2048 / 2, y: 2732 / 6 - 50 + 100 }, 'Warrior Spirit': { x: 3 * 2048 / 4 + 50, y: 2732 / 6 - 50 + 100 }, 'Beast Tamer': { x: 2048 / 4 - 50, y: 2 * 2732 / 6 + 100 }, 'Herbal Remedies': { x: 2048 / 2, y: 2 * 2732 / 6 + 100 }, 'Spirit Guidance': { x: 3 * 2048 / 4 + 50, y: 2 * 2732 / 6 + 100 }, 'Legendary Ancestor': { x: 2048 / 2, y: 3 * 2732 / 6 + 50 + 100 } }, icons: { // *** Replace these values with your actual asset names defined in LK.init.image *** 'Ancient Wisdom': 'AncientWisdom_icon', 'Elemental Mastery': 'ElementalMastery_icon', 'Warrior Spirit': 'WarriorSpirit_icon', 'Beast Tamer': 'BeastTamer_icon', 'Herbal Remedies': 'HerbalRemedies_icon', 'Spirit Guidance': 'SpiritGuidance_icon', 'Legendary Ancestor': 'LegendaryAncestor_icon' } }, 'The Forge of Possibilities': { backgroundAsset: 'popup_forgeofpossibilities', nodes: ['Adaptive Construction', 'Elemental Infusion', 'Modular Enhancement', 'Resourceful Salvage', 'Hybrid Crafting', 'Runic Slots', 'Masterwork Creation'], positions: { // Positions from original code 'Adaptive Construction': { x: 2048 / 4 - 50, y: 2732 / 6 - 50 + 100 }, 'Elemental Infusion': { x: 2048 / 2, y: 2732 / 6 - 50 + 100 }, 'Modular Enhancement': { x: 3 * 2048 / 4 + 50, y: 2732 / 6 - 50 + 100 }, 'Resourceful Salvage': { x: 2048 / 4 - 50, y: 2 * 2732 / 6 + 100 }, 'Hybrid Crafting': { x: 2048 / 2, y: 2 * 2732 / 6 + 100 }, 'Runic Slots': { x: 3 * 2048 / 4 + 50, y: 2 * 2732 / 6 + 100 }, 'Masterwork Creation': { x: 2048 / 2, y: 3 * 2732 / 6 + 50 + 100 } }, icons: { // *** Replace these values with your actual asset names defined in LK.init.image *** 'Adaptive Construction': 'AdaptiveConstruction_icon', 'Elemental Infusion': 'ElementalInfusion_icon', 'Modular Enhancement': 'ModularEnhancement_icon', 'Resourceful Salvage': 'ResourcefulSalvage_icon', 'Hybrid Crafting': 'HybridCrafting_icon', 'Runic Slots': 'RunicSlots_icon', 'Masterwork Creation': 'MasterworkCreation_icon' } }, 'The Prism of Potential': { backgroundAsset: 'popup_prismofpotential', nodes: ['Red Radiance', 'Blue Bulwark', 'Green Growth', 'Yellow Zephyr', 'Indigo Insight', 'Chromatic Fusion', 'Prismatic Ascendance'], positions: { // Positions from original code 'Red Radiance': { x: 2048 / 4 - 50, y: 2732 / 6 - 50 + 100 }, 'Blue Bulwark': { x: 2048 / 2, y: 2732 / 6 - 50 + 100 }, 'Green Growth': { x: 3 * 2048 / 4 + 50, y: 2732 / 6 - 50 + 100 }, 'Yellow Zephyr': { x: 2048 / 4 - 50, y: 2 * 2732 / 6 + 100 }, 'Indigo Insight': { x: 2048 / 2, y: 2 * 2732 / 6 + 100 }, 'Chromatic Fusion': { x: 3 * 2048 / 4 + 50, y: 2 * 2732 / 6 + 100 }, 'Prismatic Ascendance': { x: 2048 / 2, y: 3 * 2732 / 6 + 50 + 100 } }, icons: { // *** Replace these values with your actual asset names defined in LK.init.image *** 'Red Radiance': 'RedRadiance_icon', 'Blue Bulwark': 'BlueBulwark_icon', 'Green Growth': 'GreenGrowth_icon', 'Yellow Zephyr': 'YellowZephyr_icon', 'Indigo Insight': 'IndigoInsight_icon', 'Chromatic Fusion': 'ChromaticFusion_icon', 'Prismatic Ascendance': 'PrismaticAscendance_icon' } }, 'The Nexus of Chaos': { backgroundAsset: 'popup_nexusofchaos', nodes: ['Chaotic Spark', 'Entropy Shield', 'Unstable Rift', 'Anomaly Shift', 'Cascade of Disorder', 'Fractured Fate', 'Reality Collapse'], positions: { // Positions from original code 'Chaotic Spark': { x: 2048 / 4 - 50, y: 2732 / 6 - 50 + 100 }, 'Entropy Shield': { x: 2048 / 2, y: 2732 / 6 - 50 + 100 }, 'Unstable Rift': { x: 3 * 2048 / 4 + 50, y: 2732 / 6 - 50 + 100 }, 'Anomaly Shift': { x: 2048 / 4 - 50, y: 2 * 2732 / 6 + 100 }, 'Cascade of Disorder': { x: 2048 / 2, y: 2 * 2732 / 6 + 100 }, 'Fractured Fate': { x: 3 * 2048 / 4 + 50, y: 2 * 2732 / 6 + 100 }, 'Reality Collapse': { x: 2048 / 2, y: 3 * 2732 / 6 + 50 + 100 } }, icons: { // *** Replace these values with your actual asset names defined in LK.init.image *** 'Chaotic Spark': 'ChaoticSpark_icon', 'Entropy Shield': 'EntropyShield_icon', 'Unstable Rift': 'UnstableRift_icon', 'Anomaly Shift': 'AnomalyShift_icon', 'Cascade of Disorder': 'CascadeofDisorder_icon', 'Fractured Fate': 'FracturedFate_icon', 'Reality Collapse': 'RealityCollapse_icon' } }, 'The Symphony of Shadows': { backgroundAsset: 'popup_symphonyofshadows', nodes: ['Whispered Steps', 'Nightfall Veil', 'Serrated Shade', 'Shadow Familiar', 'Shadowstep', 'Umbral Echoes', 'Midnight Crescendo'], positions: { // Positions from original code 'Whispered Steps': { x: 2048 / 4 - 50, y: 2732 / 6 - 50 + 100 }, 'Nightfall Veil': { x: 2048 / 2, y: 2732 / 6 - 50 + 100 }, 'Serrated Shade': { x: 3 * 2048 / 4 + 50, y: 2732 / 6 - 50 + 100 }, 'Shadow Familiar': { x: 2048 / 4 - 50, y: 2 * 2732 / 6 + 100 }, 'Shadowstep': { x: 2048 / 2, y: 2 * 2732 / 6 + 100 }, 'Umbral Echoes': { x: 3 * 2048 / 4 + 50, y: 2 * 2732 / 6 + 100 }, 'Midnight Crescendo': { x: 2048 / 2, y: 3 * 2732 / 6 + 50 + 100 } }, icons: { // *** Replace these values with your actual asset names defined in LK.init.image *** 'Whispered Steps': 'WhisperedSteps_icon', 'Nightfall Veil': 'NightfallVeil_icon', 'Serrated Shade': 'SerratedShade_icon', 'Shadow Familiar': 'ShadowFamiliar_icon', 'Shadowstep': 'MuffledStrikes_icon', 'Umbral Echoes': 'UmbralEchoes_icon', 'Midnight Crescendo': 'MidnightCrescendo_icon' } } }; /**** * Global Variables (Gameplay State) ****/ var arrows = []; var activeDecoys = []; // <--- ADD THIS LINE var currentRoom; var visitedRooms = []; var currentRoomMusic = null; // Reference to currently playing music instance (if needed for stopping) var hero; var isPopupActive = false; // General flag for any major popup (main skill tree, specific tree) var isSkillDetailPopupActive = false; // Specific flag for the skill *detail* popup var gameContainer = new Container(); var uiContainer = new Container(); var menuContainer = new Container(); // Define menuContainer before using it // UI Element References (initialized later) var skillTreePopup = null; // Main selection screen var skillPointsDisplay = null; // Text on main selection screen var skillTreePointsText = null; // Text on individual tree screens var roomDisplay = null; var healthText = null; var isGameOver = false; // ---> ADDED: Skill Button Management <--- var MAX_ACTIVE_SKILL_SLOTS = 4; var activeSkillSlots = []; // Array to hold skill names assigned to slots [slot0, slot1, slot2, slot3] var skillButtonReferences = {}; // Map: { skillName: buttonContainerObject } var skillButtonPositions = []; // Array to store calculated positions [{x, y}, {x, y}, ...] // ---> END Skill Button Management <--- game.addChild(gameContainer); // For gameplay elements (hero, enemies, projectiles) game.addChild(uiContainer); // For UI elements (joysticks, buttons not part of popups) game.addChild(menuContainer); // For popups (skill tree, skill details) /**** * Helper Functions & Game Logic ****/ function getRandomElement(arr) { if (!arr || arr.length === 0) { return null; } return arr[Math.floor(Math.random() * arr.length)]; } // Gets data for a random elemental status effect (excluding physical/healing/true) function getRandomElementalStatusData(source) { var elementalTypes = [StatusEffectType.IGNITE, StatusEffectType.CHILL, StatusEffectType.SHOCK, StatusEffectType.POISON]; // Add others? Shadow? Chaos? var randomType = getRandomElement(elementalTypes); var effectData = { source: source, stackingType: 'Duration' }; // Default stacking switch (randomType) { case StatusEffectType.IGNITE: effectData.type = StatusEffectType.IGNITE; effectData.duration = 5; effectData.magnitude = 10; // Example base ignite damage effectData.tickInterval = 1; effectData.stackingType = 'Intensity'; // Maybe intensity for DoTs? break; case StatusEffectType.CHILL: effectData.type = StatusEffectType.CHILL; effectData.duration = 4; effectData.magnitude = { speed: 0.3, attack: 0.2 }; break; case StatusEffectType.SHOCK: effectData.type = StatusEffectType.SHOCK; effectData.duration = 6; effectData.magnitude = 0.15; // 15% increased damage taken break; case StatusEffectType.POISON: effectData.type = StatusEffectType.POISON; effectData.duration = 8; effectData.magnitude = { dot: 8, healReduction: 0.5 }; // Example base poison effectData.tickInterval = 1; effectData.stackingType = 'Intensity'; break; default: return null; // Should not happen if array is populated } return effectData; } // Gets data for a random debuff status effect function getRandomDebuffData(source) { var debuffTypes = [StatusEffectType.CHILL, StatusEffectType.SHOCK, StatusEffectType.SLOW, StatusEffectType.DISORIENT, StatusEffectType.VULNERABLE]; // Add more? var randomType = getRandomElement(debuffTypes); var effectData = { source: source, stackingType: 'Duration' }; switch (randomType) { case StatusEffectType.CHILL: effectData.type = StatusEffectType.CHILL; effectData.duration = 4; effectData.magnitude = { speed: 0.3, attack: 0.2 }; break; case StatusEffectType.SHOCK: effectData.type = StatusEffectType.SHOCK; effectData.duration = 6; effectData.magnitude = 0.15; break; case StatusEffectType.SLOW: effectData.type = StatusEffectType.SLOW; effectData.duration = 5; effectData.magnitude = { speed: 0.5, attack: 0.3 }; break; case StatusEffectType.DISORIENT: // Placeholder effect effectData.type = StatusEffectType.DISORIENT; effectData.duration = 6; effectData.magnitude = { accuracy: 0.3 }; break; // Example: 30% accuracy reduction case StatusEffectType.VULNERABLE: // Placeholder effect - need specific damage type effectData.type = StatusEffectType.VULNERABLE; effectData.duration = 8; effectData.magnitude = { type: DamageType.PHYSICAL, increase: 0.25 }; break; // Example: +25% Physical taken default: return null; } return effectData; } function activateSkill(skillName) { // --- Initial Checks --- if (!hero || !hero.active || isPopupActive || isSkillDetailPopupActive || isGameOver) { // Optional: Add visual feedback like a "cannot use" sound/flash return; // Cannot use skills if dead, in menu, game over, or hero missing } var skillData = skillDefinitions[skillName]; if (!skillData || skillData.points <= 0) { // Optional: Add visual feedback "skill not learned" return; // Skill not learned or not defined } // --- Cooldown Check --- var cooldownRemaining = hero.activeSkillCooldowns[skillName] || 0; if (cooldownRemaining > 0) { // Optional: Add visual feedback "skill on cooldown" (greyed button/sound) return; // On cooldown } // --- Execute Skill Logic --- var success = false; // Flag to check if skill execution logic ran successfully switch (skillName) { case 'Power Surge': success = executePowerSurge(skillData); break; case 'Overload Blast': success = executeOverloadBlast(skillData); break; // Added case 'Legendary Ancestor': success = executeLegendaryAncestor(skillData); break; case 'Masterwork Creation': success = executeMasterworkCreation(skillData); break; // Added case 'Prismatic Ascendance': success = executePrismaticAscendance(skillData); break; // Added case 'Nightfall Veil': success = executeNightfallVeil(skillData); break; case 'Shadowstep': success = executeShadowstep(skillData); break; // Added // --- Placeholder Cases for other known Active Skills --- // case 'Neural Hijack': success = executeNeuralHijack(skillData); break; // case 'Beast Tamer': success = executeBeastTamer(skillData); break; // case 'Shadow Familiar': success = executeShadowFamiliar(skillData); break; // case 'Reality Collapse': success = executeRealityCollapse(skillData); break; // case 'Midnight Crescendo': success = executeMidnightCrescendo(skillData); break; // --- Add more as they get implemented --- default: // Skill exists in definitions but has no execute function yet // Optional: Log this state if debugging were possible success = false; // Treat as unsuccessful if no implementation break; } // --- Set Cooldown if Skill Executed Successfully --- if (success) { var baseCooldownSeconds = 60; // Default fallback cooldown var calcData = skillData.calcData; var points = skillData.points; // Need points for scaling cooldowns // --- Fetch Cooldown from calcData --- if (calcData) { // 1. Check for scaling cooldown arrays (most specific) if (skillName === 'Nightfall Veil' && calcData.cooldowns && points > 0) { baseCooldownSeconds = calcData.cooldowns[Math.min(points - 1, calcData.cooldowns.length - 1)]; } else if (skillName === 'Shadowstep' && calcData.cooldowns && points > 0) { baseCooldownSeconds = calcData.cooldowns[Math.min(points - 1, calcData.cooldowns.length - 1)]; } else if (skillName === 'Neural Hijack' && calcData.cooldownDecreases && points > 0) { // Example for Neural Hijack baseCooldownSeconds = calcData.cooldownDecreases[Math.min(points - 1, calcData.cooldownDecreases.length - 1)]; } else if (skillName === 'Reality Collapse' && calcData.cooldowns && points > 0) { // Example for Reality Collapse baseCooldownSeconds = calcData.cooldowns[Math.min(points - 1, calcData.cooldowns.length - 1)]; // Add other skills with scaling cooldown arrays here... // 2. Check for fixed cooldown property } else if (calcData.cooldown !== undefined) { // Check if the property exists baseCooldownSeconds = calcData.cooldown; } else if (skillName === 'Shadow Familiar') { baseCooldownSeconds = 30; } // 3. If neither specific array nor fixed property found, use the default 60s } // --- End Fetch Cooldown --- var cooldownReduction = hero.getStat('skillCooldownReduction'); // Get CDR stat (0.0 to 1.0) var finalCooldownSeconds = Math.max(0, baseCooldownSeconds * (1 - cooldownReduction)); // Ensure cooldown isn't negative hero.activeSkillCooldowns[skillName] = Math.round(finalCooldownSeconds * 60); // Set cooldown in frames } } // Placeholder helper for Nightfall Veil cooldown (as an example) function calcNightfallVeilCooldown(points) { var _skillDefinitions$Nig; var calc = (_skillDefinitions$Nig = skillDefinitions['Nightfall Veil']) === null || _skillDefinitions$Nig === void 0 ? void 0 : _skillDefinitions$Nig.calcData; if (!calc || points <= 0) { return 60; } // Default return calc.cooldowns[Math.min(points - 1, calc.cooldowns.length - 1)]; // Get cooldown for current rank } // --- Specific Skill Execution Functions --- function executePowerSurge(skillData) { if (!hero) { return false; } var points = skillData.points; var calc = skillData.calcData; if (!calc || points <= 0) { return false; } var radius = calc.radiusSizes[points - 1] * 50; // Example: Convert units to pixels (adjust multiplier) var damageMultiplier = calc.damageMultipliers[points - 1]; var baseMagicDamage = 50; // Define a base damage value for the skill var damageAmount = baseMagicDamage * damageMultiplier; // --- TODO: Add Power Surge VFX --- var surgeVFX = LK.getAsset('box', { color: 0x00FFFF, x: hero.x, y: hero.y, width: 10, height: 10, anchorX: 0.5, anchorY: 0.5, alpha: 0.8 }); gameContainer.addChild(surgeVFX); // Simple flash var vfxDuration = 15; var frameCount = 0; var vfxInterval = LK.setInterval(function () { frameCount++; var progress = frameCount / vfxDuration; surgeVFX.scaleX = 1 + progress * (radius / 5); surgeVFX.scaleY = 1 + progress * (radius / 5); surgeVFX.alpha = 0.8 * (1 - progress); if (frameCount >= vfxDuration) { LK.clearInterval(vfxInterval); if (surgeVFX.parent) { surgeVFX.parent.removeChild(surgeVFX); } } }, 1000 / 60); // --- End VFX --- // Find enemies in radius if (currentRoom && currentRoom.enemies) { currentRoom.enemies.forEach(function (enemy) { if (enemy && enemy.active) { var dx = enemy.x - hero.x; var dy = enemy.y - hero.y; if (dx * dx + dy * dy < radius * radius) { // Calculate final damage using hero's stats (e.g., bonus elemental damage) // For now, just use the calculated amount. Assume MAGIC type. var outgoingDamage = hero.calculateOutgoingDamage(damageAmount, DamageType.LIGHTNING); // Example type enemy.takeDamage(outgoingDamage.damage, DamageType.LIGHTNING, hero); } } }); } // --- TODO: Add Power Surge SFX --- return true; // Skill executed } function executeOverloadBlast(skillData) { if (!hero || !currentRoom || !currentRoom.enemies) { return false; } var points = skillData.points; // Max 1 for this skill if (points <= 0) { return false; } var damagePercent = 3.00; // 300% base damage var baseFireDamage = 80; // Define a base value for scaling - ADJUST AS NEEDED var damageAmount = baseFireDamage * damagePercent; // TODO: Add Overload Blast AoE Fire VFX (large explosion centered on hero?) // Damage all enemies in the room currentRoom.enemies.forEach(function (enemy) { if (enemy && enemy.active) { var outgoingDamage = hero.calculateOutgoingDamage(damageAmount, DamageType.FIRE); enemy.takeDamage(outgoingDamage.damage, DamageType.FIRE, hero); } }); // TODO: Add Overload Blast SFX return true; // Skill executed } function executeMasterworkCreation(skillData) { if (!hero) { return false; } var points = skillData.points; var calc = skillData.calcData; if (!calc || points <= 0) { return false; } var durationSeconds = 6; // Fixed duration var damageBonusPercent = calc.dmgBonuses[points - 1] / 100; // Convert % to decimal multiplier // Effect implementation (only damage buff for now) hero.applyStatModifier('damageBonus', damageBonusPercent, durationSeconds * 60, true, 'Masterwork Creation'); // Percentage bonus // TODO: Implement weapon effect changes (arrow cleave, melee shockwave) later // TODO: Add Masterwork Creation VFX/SFX start/end hero.tint = 0xC0C0C0; // Silver tint LK.setTimeout(function () { if (hero.tint === 0xC0C0C0) { hero.tint = 0xFFFFFF; } }, durationSeconds * 1000); return true; // Skill executed } function executePrismaticAscendance(skillData) { if (!hero) { return false; } var points = skillData.points; var calc = skillData.calcData; if (!calc || points <= 0) { return false; } // Potency seems to *decrease* per rank in descriptionFn? Let's assume it's meant to increase overall effectiveness. // We'll interpret "Empowers elemental buffs by +X%" as increasing *outgoing* elemental damage by Y% for the duration. // Let's use a simple scaling bonus based on rank for now, ignoring potencyValues directly. var durationSeconds = calc.durationValues[points - 1]; var elementalDamageBonusPercent = (10 + points * 4) / 100; // Example: 14% at rank 1, up to 30% at rank 5 // Apply temporary damage bonus to each element type // NOTE: This requires stats like 'bonusFireDamagePercent', 'bonusIceDamagePercent' etc. // OR a modification to calculateOutgoingDamage to check for this active buff. // Let's use applyStatModifier on 'damageBonus' for now, but specify source. // A better approach later might be a dedicated buff type. hero.applyStatModifier('damageBonus', elementalDamageBonusPercent, durationSeconds * 60, true, 'Prismatic Ascendance'); // TODO: Implement proper elemental damage boost interaction later // TODO: Add Prismatic Ascendance VFX/SFX start/end hero.tint = 0xFFFFFF; // Bright white flash/tint? LK.setTimeout(function () { if (hero.tint === 0xFFFFFF) { hero.tint = 0xFFFFFF; } // Revert tint if needed }, durationSeconds * 1000); return true; // Skill executed } function executeShadowFamiliar(skillData) { if (!hero) { return false; } var points = skillData.points; var calc = skillData.calcData; if (!calc || points <= 0) { return false; } // --- Check if a familiar already exists? Prevent multiple summons? --- // We need a way to track the active familiar if only one is allowed. // Let's add a reference to the hero object. if (hero.activeFamiliar && hero.activeFamiliar.active) { // Familiar already active, maybe refresh duration? Or do nothing? // For now, let's prevent summoning a new one. return false; // Indicate skill didn't "execute" fully (no cooldown trigger) } // --- End Check --- var duration = calc.durations[points - 1]; var slowPercent = calc.slowValues[points - 1]; // Create the familiar instance var familiar = new ShadowFamiliar(hero, duration, slowPercent); // Add to game container gameContainer.addChild(familiar); // Store reference on hero to prevent duplicates and allow easy access/removal hero.activeFamiliar = familiar; // Add a listener to remove the reference when it's destroyed // Requires an 'ondestroy' event or similar. If LK doesn't have one, // we need to manually clear hero.activeFamiliar in ShadowFamiliar.destroy // Let's modify ShadowFamiliar.destroy: /* Add this inside ShadowFamiliar.destroy, before removing from parent: if (self.sourceHero && self.sourceHero.activeFamiliar === self) { self.sourceHero.activeFamiliar = null; // Clear reference on hero } */ // TODO: Add Familiar Summon SFX return true; // Skill executed successfully } function executeShadowstep(skillData) { if (!hero || !hero.joystick) { return false; } // Need joystick for direction var points = skillData.points; var calc = skillData.calcData; if (!calc || points <= 0) { return false; } var distanceUnits = calc.distances[points - 1]; var pixelDistance = distanceUnits * 100; // Example: Convert units (ADJUST multiplier) var imageDurationSeconds = calc.imageDurs[points - 1]; var imageDurationFrames = Math.round(imageDurationSeconds * 60); // Duration in frames // Get blink direction var moveDirection = hero.joystick.getDirection(); var blinkAngle = moveDirection.x !== 0 || moveDirection.y !== 0 ? Math.atan2(moveDirection.y, moveDirection.x) : hero.rotation; // Calculate target position var targetX = hero.x + Math.cos(blinkAngle) * pixelDistance; var targetY = hero.y + Math.sin(blinkAngle) * pixelDistance; // Store original position for decoy var originalX = hero.x; var originalY = hero.y; // --- Wall Collision Check for Blink Target --- if (targetX < wallThickness) { targetX = wallThickness; } if (targetX > roomWidth - wallThickness) { targetX = roomWidth - wallThickness; } if (targetY < wallThickness) { targetY = wallThickness; } if (targetY > roomHeight - wallThickness) { targetY = roomHeight - wallThickness; } // TODO: Add "poof" VFX at original location // TODO: Add "poof" VFX at new location // Teleport the hero hero.x = targetX; hero.y = targetY; // --- Create Afterimage VISUAL --- var afterimageVisual = LK.getAsset('hero', { // Use hero graphic anchorX: 0.5, anchorY: 0.5, x: originalX, y: originalY, alpha: 0.5, tint: 0xAAAAFF // Semi-transparent, bluish }); gameContainer.addChild(afterimageVisual); // --- Create Afterimage DATA object for targeting & lifespan --- var decoyData = { id: Date.now().toString(36) + Math.random().toString(36).substr(2), // Unique ID x: originalX, y: originalY, isDecoy: true, // Flag for identification remainingDuration: imageDurationFrames, visual: afterimageVisual // Link to the visual object for removal }; // ---> Add to the global decoy array <--- activeDecoys.push(decoyData); // TODO: Add Shadowstep SFX return true; // Skill executed } function executeCascadeExplosion(centerX, centerY, damagePercent, radiusPixels, sourceHero) { if (!currentRoom || !currentRoom.enemies || !sourceHero) { return; } // TODO: Add Cascade Explosion VFX at centerX, centerY (e.g., expanding circle) var explosionVFX = LK.getAsset('box', { // Placeholder VFX color: 0xFF00FF, x: centerX, y: centerY, width: 10, height: 10, anchorX: 0.5, anchorY: 0.5, alpha: 0.9 }); gameContainer.addChild(explosionVFX); var vfxDuration = 20; var frameCount = 0; var vfxInterval = LK.setInterval(function () { frameCount++; var progress = frameCount / vfxDuration; explosionVFX.scaleX = 1 + progress * (radiusPixels / 5); explosionVFX.scaleY = 1 + progress * (radiusPixels / 5); explosionVFX.alpha = 0.9 * (1 - progress); if (frameCount >= vfxDuration) { LK.clearInterval(vfxInterval); if (explosionVFX.parent) { explosionVFX.parent.removeChild(explosionVFX); } } }, 1000 / 60); // --- End Placeholder VFX --- // Calculate actual damage amount (using hero's base damage? Or a skill base?) // Let's use a skill base damage for now, adjustable later var skillBaseDamage = 30; // Example base damage for the explosion var damageAmount = skillBaseDamage * damagePercent; // Find enemies within the radius currentRoom.enemies.forEach(function (enemy) { if (enemy && enemy.active) { var dx = enemy.x - centerX; var dy = enemy.y - centerY; var distSq = dx * dx + dy * dy; if (distSq < radiusPixels * radiusPixels) { // Apply damage (using hero's stats for calculation, e.g., crit, bonuses) // Damage type could be Chaos or Physical? Let's use Chaos. var outgoingDamage = sourceHero.calculateOutgoingDamage(damageAmount, DamageType.CHAOS); enemy.takeDamage(outgoingDamage.damage, DamageType.CHAOS, sourceHero); } } }); // TODO: Add Cascade Explosion SFX } function spawnUmbralEcho(spawnX, spawnY, confusionDurationSeconds, sourceHero) { if (!currentRoom || !currentRoom.enemies || !sourceHero) { return; } var echoRadius = 100; // Radius around the echo to apply confusion (adjust as needed) var echoVisualDuration = 1.5; // How long the echo visual persists (seconds) // --- Create Echo Visual --- var echoVisual = LK.getAsset('hero', { // Use hero graphic as placeholder? Or dedicated echo asset? anchorX: 0.5, anchorY: 0.5, x: spawnX, y: spawnY, alpha: 0.4, // More transparent tint: 0x6600CC // Deep purple/indigo tint }); // Make it pulse slightly? (Optional) echoVisual.scaleX = 1.0; echoVisual.scaleY = 1.0; gameContainer.addChild(echoVisual); // Timer to fade out and remove visual var fadeFrames = echoVisualDuration * 60; var currentFrame = 0; var fadeInterval = LK.setInterval(function () { currentFrame++; if (currentFrame >= fadeFrames || !echoVisual || !echoVisual.parent) { LK.clearInterval(fadeInterval); if (echoVisual && echoVisual.parent) { echoVisual.parent.removeChild(echoVisual); } return; } echoVisual.alpha = 0.4 * (1 - currentFrame / fadeFrames); // Fade out alpha // Optional pulsing scale: // var scalePulse = 1.0 + Math.sin(currentFrame * 0.3) * 0.1; // echoVisual.scale.set(scalePulse); }, 1000 / 60); // --- End Echo Visual --- // --- Apply Confusion to nearby enemies --- currentRoom.enemies.forEach(function (enemy) { if (enemy && enemy.active) { var dx = enemy.x - spawnX; var dy = enemy.y - spawnY; var distSq = dx * dx + dy * dy; if (distSq < echoRadius * echoRadius) { // Apply Confuse status effect enemy.applyStatusEffect({ type: StatusEffectType.CONFUSE, source: sourceHero, // Source is the hero triggering the echo duration: confusionDurationSeconds, magnitude: null, // No magnitude needed for confuse usually stackingType: 'Duration' // Refresh duration if hit again }); } } }); // TODO: Add Umbral Echo spawn SFX? } function executeLegendaryAncestor(skillData) { if (!hero) { return false; } var points = skillData.points; // Max 1 for this skill if (points <= 0) { return false; } var durationSeconds = 6; var damageBonus = 1.0; // +100% var defenseBonus = 0.5; // +50% // Apply buffs using StatModifier system hero.applyStatModifier('damageBonus', damageBonus, durationSeconds * 60, true, 'Legendary Ancestor'); // Percentage bonus hero.applyStatModifier('armor', hero.getStat('armor') * defenseBonus, durationSeconds * 60, false, 'Legendary Ancestor'); // Flat bonus based on current armor hero.applyStatModifier('magicResist', hero.getStat('magicResist') * defenseBonus, durationSeconds * 60, false, 'Legendary Ancestor'); // Flat bonus based on current MR // --- TODO: Add Legendary Ancestor VFX/SFX start --- hero.tint = 0xFFD700; // Gold tint LK.setTimeout(function () { if (hero.tint === 0xFFD700) { hero.tint = 0xFFFFFF; // Revert to white (no tint) instead of null } }, durationSeconds * 1000); // Remove tint after duration (basic visual) return true; // Skill executed } function executeNightfallVeil(skillData) { if (!hero) { return false; } var points = skillData.points; var calc = skillData.calcData; if (!calc || points <= 0) { return false; } var durationSeconds = calc.durations[points - 1]; // Apply Cloaked status effect hero.applyStatusEffect({ type: StatusEffectType.CLOAKED, source: hero, duration: durationSeconds, magnitude: null, // No magnitude needed stackingType: 'Duration' // Refresh if somehow activated again }); // --- TODO: Add Nightfall Veil VFX/SFX start (e.g., fade out hero alpha) --- hero.alpha = 0.3; // Make hero semi-transparent // Need logic in removeStatusEffect to restore alpha return true; // Skill executed } // Modify removeStatusEffect to handle alpha restoration BaseEntity.prototype.removeStatusEffect = function (effectId) { var index = this.activeStatusEffects.findIndex(function (eff) { return eff.id === effectId; }); if (index > -1) { var removedEffectType = this.activeStatusEffects[index].type; this.activeStatusEffects.splice(index, 1); // ***** VISUAL CUE END LOGIC ***** if (removedEffectType === StatusEffectType.CLOAKED && this === hero) { // Check if STILL cloaked by another source? var stillCloaked = this.activeStatusEffects.some(function (eff) { return eff.type === StatusEffectType.CLOAKED; }); if (!stillCloaked) { // console.log("Restoring alpha in removeStatusEffect"); // Internal log this.alpha = 1.0; // Restore visibility } } // Re-evaluate tint based on remaining effects var currentTint = 0xFFFFFF; // Default to white (no tint) var tintPriority = -1; this.activeStatusEffects.forEach(function (eff) { var effectTint = 0xFFFFFF; var priority = 0; switch /* ... assign tints/priorities ... */ (eff.type) {} if (priority > tintPriority) { tintPriority = priority; currentTint = effectTint; } }); this.tint = currentTint; // ***** END VISUAL CUE END ***** } }; // --- TODO: Modify Enemy AI --- // Enemy targeting logic needs to check if hero has CLOAKED status effect. // If cloaked, enemies should stop targeting/moving towards the hero. // This modification would likely happen inside enemy update/moveTowards methods. // --- Skill System Functions --- // Creates the popup container for skill details function createSkillPopup(skillTitle, skillDescription) { var skillPopup = new Container(); skillPopup.width = roomWidth; skillPopup.height = roomHeight; skillPopup.interactive = true; // Prevent clicks going through var skillData = skillDefinitions[skillTitle]; // Get skill data // Translucent Background var translucentBackground = LK.getAsset('box', { anchorX: 0.5, anchorY: 0.5, width: roomWidth, height: roomHeight, color: 0x000000, alpha: 0.7 }); translucentBackground.x = roomWidth / 2; translucentBackground.y = roomHeight / 2; skillPopup.addChild(translucentBackground); // Title Text var skillTitleText = new Text2(skillTitle, { size: 120, fill: 0xFFFFFF, fontWeight: "bold", fontFamily: "Techno, sans-serif", letterSpacing: 2, stroke: 0x00FFCC, strokeThickness: 3, align: 'center' }); skillTitleText.x = roomWidth / 2; skillTitleText.y = roomHeight / 2 - 600; // Position higher up skillTitleText.anchor.set(0.5, 0.5); skillPopup.addChild(skillTitleText); // Description Text (Give it a name) var skillDescriptionText = new Text2(skillDescription, { size: 70, fill: 0xFFFFFF, fontFamily: "Arial, sans-serif", letterSpacing: 1, stroke: 0x00FFCC, strokeThickness: 2, wordWrap: true, wordWrapWidth: 1800, align: 'center' }); skillDescriptionText.x = roomWidth / 2; skillDescriptionText.y = skillTitleText.y + 300; // Position below title skillDescriptionText.anchor.set(0.5, 0.5); skillDescriptionText.name = "skillDescriptionText"; // Name for updating skillPopup.addChild(skillDescriptionText); // Add helper method to find children by name skillPopup.getChildByName = function (name) { for (var i = 0; i < this.children.length; i++) { if (this.children[i].name === name) { return this.children[i]; } } return null; }; // Allocation Text (Give it a name) if (skillData) { var points = skillData.points; var maxPoints = skillData.max; var allocationText = new Text2(points + '/' + maxPoints, { size: 80, fill: 0xFFFFFF, fontWeight: "bold", stroke: 0x00FFCC, strokeThickness: 2 }); allocationText.x = roomWidth / 2; allocationText.y = skillDescriptionText.y + skillDescriptionText.height / 2 + 100; // Below description allocationText.anchor.set(0.5, 0.5); allocationText.name = "allocationText"; // Name for updating skillPopup.addChild(allocationText); } // --- Buttons --- var buttonY = roomHeight / 2 + 450; // Position buttons lower // Exit Button var exitButton = LK.getAsset('cancelButton', { anchorX: 0.5, anchorY: 0.5, scaleX: 3, scaleY: 3 }); exitButton.x = roomWidth / 2 - 400; // Spread buttons exitButton.y = buttonY; exitButton.down = function () { menuContainer.removeChild(skillPopup); isSkillDetailPopupActive = false; // Reset the specific flag updateSkillNodePointDisplays(); // Update points on the tree screen when closing }; skillPopup.addChild(exitButton); // --- Modified Spend Button --- var spendButton = LK.getAsset('spendButton', { anchorX: 0.5, anchorY: 0.5, scaleX: 3, scaleY: 3 }); spendButton.x = roomWidth / 2 + 400; // Spread buttons spendButton.y = buttonY; // Use the calculated button Y position spendButton.down = function () { // ** Check skillData exists first ** if (!skillData) { return; } if (heroSkillPoints > 0 && skillData.points < skillData.max) { var isFirstPoint = skillData.points === 0; // Check if this is the FIRST point allocated heroSkillPoints--; skillData.points++; // Allocate point // ---> ADD Button Creation/Assignment Logic <--- if (isFirstPoint && isSkillActive(skillTitle)) { // Check if it's an active skill using a helper assignSkillToNextSlot(skillTitle); // Assign skill and potentially create button } // ---> END Button Logic <--- // Recalculate Stats AFTER allocating point if (hero && hero.recalculateStats) { hero.recalculateStats(); updateRoomDisplay(); } // Refresh global skill point displays refreshSkillPointsText(); // Updates display on popup and underlying screens // --- Update EXISTING popup content using direct variables --- // ** No need for getChildByName ** // Update Description Text if (skillDescriptionText && skillData.descriptionFn) { // Use skillDescriptionText directly // Regenerate description with new point count var newDescription = skillData.descriptionFn(skillData.points, skillData.calcData); skillDescriptionText.setText(newDescription); // Update the text // Adjust allocation text position relative to potentially resized description if (allocationText) { // Check if allocationText exists before accessing its y property allocationText.y = skillDescriptionText.y + skillDescriptionText.height / 2 + 100; } } // Update Allocation Text if (allocationText) { // Use allocationText directly allocationText.setText(skillData.points + '/' + skillData.max); } // --- End Update EXISTING popup content --- // Update the points on the node behind this popup immediately updateSkillNodePointDisplays(); // Update nodes on the tree behind this popup } else {/* Cannot spend */} }; skillPopup.addChild(spendButton); // Skill Points Text (for this popup) - Ensure it gets updated var skillPointsTextPopup = new Text2('Skill Points: ' + heroSkillPoints, { size: 70, fill: 0xFFFFFF, fontWeight: "bold" }); skillPointsTextPopup.x = roomWidth / 2; skillPointsTextPopup.y = roomHeight - 100; // bottom area of popup skillPointsTextPopup.anchor.set(0.5, 1); // Anchor at bottom center skillPointsTextPopup.name = "popupSkillPointsText"; // Name it skillPopup.addChild(skillPointsTextPopup); return skillPopup; } // Displays the details for a specific skill (Called by node clicks) function displaySkillDetailPopup(skillName) { if (isSkillDetailPopupActive) { return; } // Prevent multiple detail popups var skillData = skillDefinitions[skillName]; if (!skillData) { return; } var currentPoints = skillData.points; var description = skillData.descriptionFn ? skillData.descriptionFn(currentPoints, skillData.calcData) : "No description available."; var skillPopup = createSkillPopup(skillName, description); menuContainer.addChild(skillPopup); isSkillDetailPopupActive = true; // Set the specific flag isPopupActive = false; // Ensure general flag is off if only detail popup is shown } // Creates a single node in the skill tree view (Modified for unique icons AND scaling) function createSkillTreeNode(skillName, position, parentPopup) { var skillData = skillDefinitions[skillName]; if (!skillData) { return; } // --- Get Specific Icon Asset Name --- var iconAssetId = 'skill_node_placeholder'; // Default to placeholder var treeLayout = skillTreeLayouts[skillData.tree]; if (treeLayout && treeLayout.icons && treeLayout.icons[skillName]) { iconAssetId = treeLayout.icons[skillName]; // Use specific asset name if mapped } else { // console.log("Warning: Icon asset mapping missing for skill: " + skillName); } // --- Create Node Graphic --- var skillNodeGraphic = null; var nodeScale = 3.0; // <<< SET YOUR DESIRED SCALE HERE (e.g., 1.25, 1.5, 2.0) try { skillNodeGraphic = LK.getAsset(iconAssetId, { anchorX: 0.5, // Set anchor in asset definition or here if needed anchorY: 0.5 // Don't set scale here if using .scale.set() below }); } catch (e) { // Fallback if the specific asset failed to load console.log("Error loading asset '" + iconAssetId + "', using placeholder. Error: " + e); iconAssetId = 'skill_node_placeholder'; // Ensure we use placeholder name skillNodeGraphic = LK.getAsset(iconAssetId, { anchorX: 0.5, anchorY: 0.5 }); } // --- Apply Scaling --- if (skillNodeGraphic.scale && skillNodeGraphic.scale.set) { // Check if scale property exists skillNodeGraphic.scale.set(nodeScale); // Set both X and Y scale } else { // Fallback if .scale.set isn't available (less common) skillNodeGraphic.scaleX = nodeScale; skillNodeGraphic.scaleY = nodeScale; } skillNodeGraphic.x = position.x; skillNodeGraphic.y = position.y; skillNodeGraphic.interactive = true; // --- Points Text --- var points = skillData.points; var maxPoints = skillData.max; // Maybe increase font size slightly too? e.g., size: 50 var pointsText = new Text2(points + '/' + maxPoints, { size: 45, fill: 0xFFFFFF, fontWeight: "bold", // Increased size slightly stroke: 0x000000, strokeThickness: 3 }); pointsText.x = position.x; // Adjust Y offset based on the NEW scaled icon size // If original height was 80 and scale is 1.5, new height is 120. Offset should be > half of that. var iconScaledHeight = (skillNodeGraphic.height || 80) * nodeScale; // Estimate scaled height pointsText.y = position.y + iconScaledHeight / 2 + 10; // Position below the scaled icon + small gap pointsText.anchor.set(0.5, 0); // Anchor horizontal center, vertical top pointsText.name = skillName + "_pointsText"; // Unique name for updates // --- Click Handler --- skillNodeGraphic.down = function () { displaySkillDetailPopup(skillName); // Call the single generic display function // Visual feedback (Flash) - Optional var originalTint = skillNodeGraphic.tint !== undefined ? skillNodeGraphic.tint : null; skillNodeGraphic.tint = 0xFFFF00; // Yellow flash LK.setTimeout(function () { if (skillNodeGraphic.tint !== undefined) { skillNodeGraphic.tint = originalTint; } }, 150); }; parentPopup.addChild(skillNodeGraphic); parentPopup.addChild(pointsText); } // Opens the specific skill tree view popup function openSkillTreePopup(skillTreeName) { menuContainer.removeChild(skillTreePopup); // Remove main selection screen var layoutData = skillTreeLayouts[skillTreeName]; if (!layoutData) { menuContainer.addChild(skillTreePopup); // Go back to main selection return; } var newPopup = new Container(); newPopup.width = roomWidth; newPopup.height = roomHeight; newPopup.interactive = true; // Block clicks through newPopup.name = skillTreeName + "_popup"; // Name the popup // Background var newPopupBackground = LK.getAsset(layoutData.backgroundAsset, { anchorX: 0.5, anchorY: 0.5 }); newPopupBackground.x = roomWidth / 2; newPopupBackground.y = roomHeight / 2; newPopup.addChild(newPopupBackground); // Back Button (Use the class) var backButton = new BackButton(); newPopup.addChild(backButton); // Title (Modified for better centering and wrapping) var skillTreeTitle = new Text2(skillTreeName, { size: 100, fill: 0xFFFFFF, fontWeight: "bold", fontFamily: "Techno, sans-serif", letterSpacing: 2, stroke: 0x00FFCC, strokeThickness: 3, align: 'center', // Explicitly center align the text wordWrap: true, // Enable word wrap wordWrapWidth: roomWidth * 0.8 // Wrap if title exceeds 80% of screen width }); skillTreeTitle.x = roomWidth / 2; // Center horizontally skillTreeTitle.y = 100; // Position near top skillTreeTitle.anchor.set(0.5, 0); // Anchor at horizontal center, vertical top newPopup.addChild(skillTreeTitle); // Create Nodes layoutData.nodes.forEach(function (skillName) { var position = layoutData.positions[skillName]; if (position) { createSkillTreeNode(skillName, position, newPopup); } else {} }); // Skill Points Display at Bottom (use global reference) skillTreePointsText = new Text2('Skill Points: ' + heroSkillPoints, { size: 70, fill: 0xFFFFFF, fontWeight: "bold" }); skillTreePointsText.x = roomWidth / 2; skillTreePointsText.y = roomHeight - 100; // bottom of the screen skillTreePointsText.anchor.set(0.5, 1); // Anchor bottom center newPopup.addChild(skillTreePointsText); refreshTreePointsText(); // Update the text content menuContainer.addChild(newPopup); isPopupActive = true; // This tree view is a major popup isSkillDetailPopupActive = false; // Detail view is not open yet } // Refreshes the skill points text on visible UI elements function refreshSkillPointsText() { // Update main skill tree selection screen text (if visible) if (skillPointsDisplay && skillPointsDisplay.parent) { skillPointsDisplay.setText('Skill Points: ' + heroSkillPoints); } // Update individual skill tree screen text (if visible) if (skillTreePointsText && skillTreePointsText.parent) { skillTreePointsText.setText('Skill Points: ' + heroSkillPoints); } // Update skill detail popup text (if visible and active) if (isSkillDetailPopupActive && menuContainer.children.length > 0) { var currentPopup = menuContainer.children[menuContainer.children.length - 1]; // Check if it's likely the detail popup (has the specific text element) var popupPointsText = currentPopup.getChildByName ? currentPopup.getChildByName("popupSkillPointsText") : null; if (popupPointsText) { popupPointsText.setText('Skill Points: ' + heroSkillPoints); } } } // Updates the 'X/Y' text on the nodes within an active skill tree popup function updateSkillNodePointDisplays() { var activeSkillTreePopup = null; // Find the currently displayed skill tree popup by its name convention for (var i = menuContainer.children.length - 1; i >= 0; i--) { var child = menuContainer.children[i]; // Check if child has a name and it ends with '_popup' (set in openSkillTreePopup) if (child && child.name && child.name.endsWith("_popup")) { activeSkillTreePopup = child; break; // Found the active tree popup } } // Only proceed if we found an active skill tree popup if (activeSkillTreePopup && activeSkillTreePopup.children) { // Check children array exists // Loop through all skills defined in our central structure for (var skillName in skillDefinitions) { if (skillDefinitions.hasOwnProperty(skillName)) { var skillData = skillDefinitions[skillName]; var targetNodeName = skillName + "_pointsText"; // The name of the text node we are looking for // Iterate through the children of the active skill tree popup to find the node var foundNode = false; // Flag to stop searching once found for (var k = 0; k < activeSkillTreePopup.children.length; k++) { var nodeChild = activeSkillTreePopup.children[k]; // Check if this child is the points text node we want by name if (nodeChild && nodeChild.name === targetNodeName) { // Found it! Update the text. // Make sure it's a text object with setText if (nodeChild.setText) { nodeChild.setText(skillData.points + '/' + skillData.max); } else {} foundNode = true; // Mark as found break; // Stop searching children for this specific skillName } } // End inner loop (children iteration) } } // End outer loop (skillDefinitions iteration) } else { // This case means no skill tree popup is currently active, which is normal // when the player is in the game or on the main skill selection screen. } } // Refreshes the skill points text specifically on the individual tree view (if open) function refreshTreePointsText() { if (skillTreePointsText && skillTreePointsText.parent) { // Check if it exists and is on stage skillTreePointsText.setText('Skill Points: ' + heroSkillPoints); } } function spendSkillPoint(skillName) { // Example refactor needed inside spend button's down handler var skillData = skillDefinitions[skillName]; if (heroSkillPoints > 0 && skillData.points < skillData.max) { heroSkillPoints--; skillData.points++; refreshSkillPointsText(); // --- TODO: Update the skill detail popup UI --- // updateSkillPopupContent(skillName); // You'd need this helper updateSkillNodePointDisplays(); hero.recalculateStats(); // <<< RECALCULATE STATS AFTER SPENDING } } // --- Room and Game Flow Functions --- function addDirectionalArrows() { // Remove existing arrows first to prevent duplicates gameContainer.children.forEach(function (child) { if (child.isDirectionalArrow) { // Add a flag to identify these arrows gameContainer.removeChild(child); } }); var arrowDirections = ['top', 'bottom', 'left', 'right']; arrowDirections.forEach(function (direction) { var arrow = LK.getAsset('directionalArrow', { anchorX: 0.5, anchorY: 0.5 }); arrow.isDirectionalArrow = true; // Add flag switch (direction) { case 'top': arrow.x = roomWidth / 2; arrow.y = wallThickness + 150; arrow.rotation = 0; break; case 'bottom': arrow.x = roomWidth / 2; arrow.y = roomHeight - wallThickness - 150; arrow.rotation = Math.PI; break; case 'left': arrow.x = wallThickness + 150; arrow.y = roomHeight / 2; arrow.rotation = -Math.PI / 2; break; case 'right': arrow.x = roomWidth - wallThickness - 150; arrow.y = roomHeight / 2; arrow.rotation = Math.PI / 2; break; } gameContainer.addChild(arrow); // Add arrows to game container, not UI LK.effects.flashObject(arrow, 0xffffff, 1000); // Flash effect }); } function removeDirectionalArrows() { gameContainer.children.slice().forEach(function (child) { // Iterate over a copy if (child.isDirectionalArrow) { gameContainer.removeChild(child); } }); } function updateRoomDisplay() { if (!roomDisplay || !currentRoom || !hero) { return; } // Ensure UI elements and game state exist var enemiesLeft = currentRoom.killGoal - currentRoom.enemiesKilled; roomDisplay.setText('Room: ' + currentRoom.number + ' | Enemies Left: ' + Math.max(enemiesLeft, 0)); if (healthText && hero && hero.active) { // Check hero exists and is active var currentHealth = Math.max(Math.round(hero.health), 0); var maxHealth = Math.round(hero.getStat('maxHealth')); healthText.setText('Health: ' + currentHealth + ' / ' + maxHealth); // Update color based on health percentage var healthPercent = maxHealth > 0 ? currentHealth / maxHealth : 0; // ***** CORRECTION HERE ***** if (healthPercent > 0.6) { healthText.fill = 0x00FF00; } // Green - Use healthText.fill else if (healthPercent > 0.3) { healthText.fill = 0xFFFF00; } // Yellow - Use healthText.fill else { healthText.fill = 0xFF0000; } // Red - Use healthText.fill // ***** END CORRECTION ***** } else if (healthText && (!hero || !hero.active)) { // Show 0 health if dead or hero doesn't exist yet var displayMaxHealth = hero ? Math.round(hero.getStat('maxHealth')) : '---'; healthText.setText('Health: 0 / ' + displayMaxHealth); // ***** CORRECTION HERE ***** healthText.fill = 0xFF0000; // Red - Use healthText.fill // ***** END CORRECTION ***** } } function stopRoomMusic() { LK.stopMusic(); // Use engine's function to stop all music currentRoomMusic = null; } function playRoomMusic(roomNumber) { stopRoomMusic(); // Stop previous music first var musicAsset = 'room' + roomNumber + '_music'; // Check if asset exists? LK might handle errors gracefully or not. try { currentRoomMusic = LK.playMusic(musicAsset, { loop: true }); // Play new music } catch (e) {} } function checkRoomCleared() { if (!currentRoom || currentRoom.isCleared) { return; } if (currentRoom.enemiesKilled >= currentRoom.killGoal) { currentRoom.isCleared = true; hero.canExitRoom = true; // Allow hero to leave // Play room cleared sound var soundAsset = 'room' + currentRoom.number + '_cleared'; try { var roomClearedSound = LK.getSound(soundAsset); // Get sound object if (roomClearedSound && roomClearedSound.play) { roomClearedSound.play(); } } catch (e) {} stopRoomMusic(); // Stop the room's music updateRoomDisplay(); addDirectionalArrows(); // Show exits } } function transitionToNextRoom() { // --- Clean up current room --- if (game.background) { game.removeChild(game.background); game.background = null; } // Clear remaining enemies explicitly currentRoom.enemies.slice().forEach(function (enemy) { // Iterate over a copy if (enemy.parent) { enemy.parent.removeChild(enemy); } // if(enemy.destroy) enemy.destroy(); // Use engine destroy if available }); currentRoom.enemies = []; removeDirectionalArrows(); // Remove exit indicators // --- Choose next room --- if (!visitedRooms.includes(currentRoom.number)) { visitedRooms.push(currentRoom.number); } if (visitedRooms.length >= totalRooms) { visitedRooms = []; // Reset if all rooms visited // Optionally exclude the immediate previous room if (currentRoom) { visitedRooms.push(currentRoom.number); } } var nextRoomNumber; do { nextRoomNumber = Math.floor(Math.random() * totalRooms) + 1; } while (visitedRooms.includes(nextRoomNumber)); // Ensure it's not in the visited list for this cycle // --- Initialize the new room --- currentRoom = new Room(nextRoomNumber); // Create new Room object // Set up background var bgImageIndex = (currentRoom.number - 1) % roomBackgroundImages.length; var bgImage = LK.getAsset(roomBackgroundImages[bgImageIndex], { anchorX: 0.5, anchorY: 0.5 }); bgImage.x = roomWidth / 2; bgImage.y = roomHeight / 2; game.addChildAt(bgImage, 0); // Add background behind other game elements game.background = bgImage; // Update UI and state hero.canExitRoom = false; updateRoomDisplay(); playRoomMusic(currentRoom.number); currentRoom.startSpawning(); // Start spawning enemies for the new room } // Called by Hero when entering an exit zone function transitionToNewRoom(entrySide) { // Position hero based on where they will ENTER the NEW room if (entrySide === 'top') { hero.y = wallThickness + hero.height / 2 + 10; hero.x = roomWidth / 2; } else if (entrySide === 'bottom') { hero.y = roomHeight - wallThickness - hero.height / 2 - 10; hero.x = roomWidth / 2; } else if (entrySide === 'left') { hero.x = wallThickness + hero.width / 2 + 10; hero.y = roomHeight / 2; } else if (entrySide === 'right') { hero.x = roomWidth - wallThickness - hero.width / 2 - 10; hero.y = roomHeight / 2; } stopRoomMusic(); // Stop music before transition log transitionToNextRoom(); // Sets up the new room, plays music, etc. } // --- Room Class Definition --- var Room = function Room(number) { this.number = number; this.enemies = []; this.isCleared = false; // Adjust spawn counts/kill goals as needed this.spawnLimit = this.number === 1 ? 15 : 10 + 5 * this.number; // Example scaling this.killGoal = this.spawnLimit; this.enemiesSpawned = 0; this.enemiesKilled = 0; this.enemyCounter = 0; // Count of currently active enemies this.spawnIntervalId = null; // To store the interval ID this.startSpawning = function () { var self = this; // Reference to 'this' room instance var spawnDelay = 1500; // ms between spawns this.spawnIntervalId = LK.setInterval(function () { // Check conditions *inside* the interval function if (self.enemiesSpawned < self.spawnLimit && !self.isCleared && !isPopupActive && !isSkillDetailPopupActive) { self.spawnEnemy(); } else if (self.enemiesSpawned >= self.spawnLimit || self.isCleared) { // Stop spawning if limit reached or room cleared LK.clearInterval(self.spawnIntervalId); self.spawnIntervalId = null; } // If popups are active, the interval continues but doesn't spawn }, spawnDelay); }; this.spawnEnemy = function () { // Double check conditions just before spawning if (this.enemiesSpawned >= this.spawnLimit || this.isCleared || isPopupActive || isSkillDetailPopupActive) { return; } var spawnKeys = Object.keys(spawnPoints); var spawnIndex = Math.floor(Math.random() * spawnKeys.length); var spawnPoint = spawnPoints[spawnKeys[spawnIndex]]; var enemy = null; var enemyTypeRand = Math.random(); // Define spawn probabilities based on room number if (this.number <= 1) { // Room 1: Mostly basic, few tanks if (enemyTypeRand < 0.15) { enemy = new TankEnemy(); } // 15% Tank else { enemy = new Enemy(); } // 85% Basic } else if (this.number <= 3) { // Rooms 2-3: Introduce ranged if (enemyTypeRand < 0.15) { enemy = new TankEnemy(); } // 15% Tank else if (enemyTypeRand < 0.40) { enemy = new RangedEnemy(); } // 25% Ranged else { enemy = new Enemy(); } // 60% Basic } else { // Rooms 4+: More tough enemies if (enemyTypeRand < 0.25) { enemy = new TankEnemy(); } // 25% Tank else if (enemyTypeRand < 0.60) { enemy = new RangedEnemy(); } // 35% Ranged else { enemy = new Enemy(); } // 40% Basic } if (enemy) { enemy.x = spawnPoint.x; enemy.y = spawnPoint.y; // enemy.active = true; // Set in BaseEnemy constructor/init // enemy.health = ???; // Set in specific enemy class constructor enemy.visible = true; this.enemies.push(enemy); gameContainer.addChild(enemy); // Add to the game world container this.enemiesSpawned++; this.enemyCounter++; // Increment active enemy count // Track the first melee enemy spawned for debugging } }; }; // Call startSpawning when the room is created and ready // this.startSpawning(); // Moved initialization to transitionToNextRoom /**** * Game Setup and Initialization ****/ function initializeMainMenu() { // Create the main skill tree selection popup container skillTreePopup = new Container(); skillTreePopup.width = roomWidth; skillTreePopup.height = roomHeight; skillTreePopup.interactive = true; var skillTreeBackground = LK.getAsset('skillTree_Background', { anchorX: 0.5, anchorY: 0.5 }); skillTreeBackground.x = roomWidth / 2; skillTreeBackground.y = roomHeight / 2; skillTreePopup.addChild(skillTreeBackground); // Skill Points Display for main menu skillPointsDisplay = new Text2('Skill Points: ' + heroSkillPoints, { size: 70, fill: 0xFFFFFF, fontWeight: "bold" }); skillPointsDisplay.x = roomWidth / 2; skillPointsDisplay.y = roomHeight - 100; // Near bottom skillPointsDisplay.anchor.set(0.5, 1); // Anchor bottom center skillTreePopup.addChild(skillPointsDisplay); // Define positions for the 6 main tree icons/titles var treeIconPositions = [{ x: roomWidth / 2 - 450, y: roomHeight / 2 - 550 }, { x: roomWidth / 2 + 450, y: roomHeight / 2 - 550 }, { x: roomWidth / 2 - 450, y: roomHeight / 2 }, { x: roomWidth / 2 + 450, y: roomHeight / 2 }, { x: roomWidth / 2 - 450, y: roomHeight / 2 + 550 }, { x: roomWidth / 2 + 450, y: roomHeight / 2 + 550 }]; var skillTreeAssetMap = { 'The Circuit of Ascension': 'circuitofascension', 'The Echoes of Ancestry': 'echoesofancestry', 'The Forge of Possibilities': 'forgeofpossibilities', 'The Prism of Potential': 'prismofpotential', 'The Nexus of Chaos': 'nexusofchaos', 'The Symphony of Shadows': 'symphonyofshadows' }; var skillTreeTitles = Object.keys(skillTreeAssetMap); skillTreeTitles.forEach(function (title, index) { if (index >= treeIconPositions.length) { return; } // Safety check var position = treeIconPositions[index]; var assetId = skillTreeAssetMap[title]; // Title Text var titleText = new Text2(title, { size: 40, fill: 0xFFFFFF, fontWeight: "bold", // Smaller title fontFamily: "Techno, sans-serif", letterSpacing: 1, stroke: 0x00FFCC, strokeThickness: 2, align: 'center', wordWrap: true, wordWrapWidth: 300 }); titleText.x = position.x; titleText.y = position.y + 200; // Below icon titleText.anchor.set(0.5, 0); // Icon Asset var skillTreeAsset = LK.getAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); skillTreeAsset.x = position.x; skillTreeAsset.y = position.y; skillTreeAsset.interactive = true; // Click handler for icon AND title var clickHandler = function clickHandler() { openSkillTreePopup(title); }; skillTreeAsset.down = clickHandler; titleText.interactive = true; titleText.down = clickHandler; skillTreePopup.addChild(skillTreeAsset); skillTreePopup.addChild(titleText); }); // Start Game Button var startGameButton = new StartGameButton(); skillTreePopup.addChild(startGameButton); // Add the main menu to the stage menuContainer.addChild(skillTreePopup); isPopupActive = true; // Start with the menu active } // --- initializeGame (Modified) --- function initializeGame() { if (!hero) { hero = gameContainer.addChild(new Hero()); hero.x = roomWidth / 2; hero.y = roomHeight / 2; var joystick = uiContainer.addChild(new Joystick()); hero.joystick = joystick; var aimingJoystick = uiContainer.addChild(new AimingJoystick()); hero.aimingJoystick = aimingJoystick; // ---> CALCULATE Skill Button Positions <--- var joyX = aimingJoystick.x; var joyY = aimingJoystick.y; var joyRadius = 150; // Approximate radius outside joystick graphic var buttonOffset = 100; // Distance outwards from joyRadius var totalButtonRadius = joyRadius + buttonOffset; var buttonSize = 150; // New, larger button size (adjust as needed) skillButtonPositions = [ // Slot 0: Left of joystick { x: joyX - totalButtonRadius, y: joyY }, // Slot 1: Upper-Left of joystick { x: joyX - totalButtonRadius * Math.cos(Math.PI / 4), y: joyY - totalButtonRadius * Math.sin(Math.PI / 4) }, // Slot 2: Above-Left of joystick (more North) { x: joyX - totalButtonRadius * Math.cos(Math.PI / 2.5), y: joyY - totalButtonRadius * Math.sin(Math.PI / 2.5) }, // Adjusted angle slightly more North // Slot 3: Directly Above joystick { x: joyX, y: joyY - totalButtonRadius }]; // ---> END Calculate Positions <--- } else { // Reset hero state if restarting hero.active = true; // Ensure hero is active hero.health = hero.getStat('maxHealth'); // Heal to full calculated max health hero.x = roomWidth / 2; hero.y = roomHeight / 2; hero.canExitRoom = false; hero.activeModifiers = []; // Clear buffs/debuffs // Reset any active effects or cooldowns if needed } isGameOver = false; // --- Apply initial stats from skills --- hero.recalculateStats(); // Calculate stats based on any initially allocated points (if loaded from save later) hero.health = hero.getStat('maxHealth'); // Ensure health starts at max after calculation // Set up initial room currentRoom = new Room(1); visitedRooms = [1]; var initialBgImage = LK.getAsset(roomBackgroundImages[0], { anchorX: 0.5, anchorY: 0.5 }); initialBgImage.x = roomWidth / 2; initialBgImage.y = roomHeight / 2; game.addChildAt(initialBgImage, 0); game.background = initialBgImage; // Setup UI Bars/Text var statsBar = LK.getAsset('box', { anchorX: 0.0, anchorY: 0.0, width: roomWidth, height: 100, x: 0, y: 0, color: 0x111111, alpha: 0.7 }); uiContainer.addChild(statsBar); roomDisplay = new Text2('Room: 1 | Enemies Left: 0', { size: 60, fill: 0xFFFFFF, fontWeight: "bold", wordWrap: false }); roomDisplay.x = roomWidth / 2; roomDisplay.y = 60; roomDisplay.anchor.set(0.5, 0.5); uiContainer.addChild(roomDisplay); healthText = new Text2('Health: ' + Math.round(hero.health) + ' / ' + Math.round(hero.getStat('maxHealth')), { size: 60, fill: 0x00FF00, fontWeight: "bold", wordWrap: false }); healthText.x = 250; healthText.y = 60; healthText.anchor.set(0, 0.5); uiContainer.addChild(healthText); updateRoomDisplay(); // Initial UI text update playRoomMusic(currentRoom.number); currentRoom.startSpawning(); } // --- Global UI Buttons --- // Kill All Button (Debug?) var killAllButton = LK.getAsset('killAllButton', { anchorX: 0.5, anchorY: 0.5 }); killAllButton.x = 250; killAllButton.y = roomHeight - 500; // Position lower left uiContainer.addChild(killAllButton); killAllButton.down = function (x, y, obj) { if (isPopupActive || isSkillDetailPopupActive || !currentRoom) { return; } // Don't activate if popups open or no room currentRoom.enemies.slice().forEach(function (enemy) { // Iterate copy if (enemy.die) { enemy.die(); } // Use the die method which handles cleanup else if (enemy.parent) { enemy.parent.removeChild(enemy); } // Fallback }); currentRoom.enemies = []; // Clear array just in case currentRoom.enemyCounter = 0; currentRoom.enemiesKilled = currentRoom.killGoal; // Mark as cleared for kill count updateRoomDisplay(); checkRoomCleared(); // Trigger cleared state }; // Add new switchWeaponZone creation at center right var switchWeaponZone = LK.getAsset('switchWeaponZone', { anchorX: 0.5, anchorY: 0.5 }); switchWeaponZone.x = roomWidth - 150; // Position near center right edge switchWeaponZone.y = roomHeight / 2; // Center vertically uiContainer.addChild(switchWeaponZone); switchWeaponZone.interactive = true; switchWeaponZone.down = function (x, y, obj) { if (isPopupActive || isSkillDetailPopupActive || !hero) { return; } hero.switchWeapon(); }; // ---> END MOVE Weapon Switch <--- // --- Initial Setup Call --- initializeMainMenu(); // Start by showing the main menu /**** * Game Update Loop ****/ game.update = function () { if (isGameOver) { // Optionally add logic here for game over screen animations? return; // Stop all game updates if game is over } // If any popup is active, skip core game updates if (isPopupActive || isSkillDetailPopupActive) { // Potentially update UI animations even when paused return; } // --- Core Gameplay Update --- if (!hero || !currentRoom || !hero.active) { // Ensure hero is active return; } // Update Hero (handles movement, attacks, modifiers, collision checks) hero.update(); // --- Update Arrows (Projectiles fired by Hero) --- for (var j = arrows.length - 1; j >= 0; j--) { var arrow = arrows[j]; // Basic validity check if (!arrow) { arrows.splice(j, 1); // Remove null/undefined entries if they occur continue; } // Update arrow position (move method handles removal if out of bounds) arrow.update(); // Check collision with enemies AFTER moving for (var i = currentRoom.enemies.length - 1; i >= 0; i--) { var enemy = currentRoom.enemies[i]; // Validity checks for enemy if (!enemy || !enemy.active) { continue; } // Check if arrow still exists (might have been destroyed by out of bounds check) // and check if it's still in the gameContainer (might have been removed elsewhere) if (!arrow || !arrow.parent) { // If arrow was destroyed (e.g., hit bounds), stop checking it against enemies break; // Exit the inner enemy loop for this (now destroyed) arrow } // Calculate distance squared for efficiency var hitDx = enemy.x - arrow.x; var hitDy = enemy.y - arrow.y; var hitDistSqr = hitDx * hitDx + hitDy * hitDy; // Corrected distance squared calculation // Calculate collision radii var enemyRadius = (enemy.width || 50) / 2; // Use default if width undefined var arrowRadius = (arrow.width || 10) / 2; // Use default if width undefined var collisionDist = enemyRadius + arrowRadius; var collisionDistSqr = collisionDist * collisionDist; // Use squared distance // --- Check for Collision --- if (hitDistSqr < collisionDistSqr) { // Apply Damage if enemy can take it if (enemy.takeDamage) { enemy.takeDamage(arrow.damage, arrow.damageType, arrow.source); // Pass type and source } // Apply Lifesteal and Status Effects ONLY if the arrow came from the hero if (arrow.source === hero) { // Lifesteal var lifeStealAmount = arrow.damage * hero.getStat('lifeSteal'); if (lifeStealAmount > 0) { hero.takeDamage(lifeStealAmount, DamageType.HEALING); } // --- APPLY STATUS EFFECTS / PROCS (Corrected Structure) --- // Cascade of Disorder Check if (Math.random() < hero.cascadeProcChance) { var points_CoD_Arr = skillDefinitions['Cascade of Disorder'].points; // Unique var names var calc_CoD_Arr = skillDefinitions['Cascade of Disorder'].calcData; if (points_CoD_Arr > 0 && calc_CoD_Arr) { var dmgPercent_CoD_Arr = (calc_CoD_Arr.baseDmg + (points_CoD_Arr - 1) * calc_CoD_Arr.dmgPerRank) / 100; var radiusUnits_CoD_Arr = calc_CoD_Arr.baseRadius + (points_CoD_Arr - 1) * calc_CoD_Arr.radiusPerRank; var radiusPixels_CoD_Arr = radiusUnits_CoD_Arr * 50; // ADJUST unit-to-pixel conversion factor executeCascadeExplosion(enemy.x, enemy.y, dmgPercent_CoD_Arr, radiusPixels_CoD_Arr, hero); } } // Elemental Infusion Check if (Math.random() < hero.elementalInfusionChance) { var randomEffectDataEI_Arr = getRandomElementalStatusData(hero); if (randomEffectDataEI_Arr) { // Optional: randomEffectDataEI_Arr.duration = 3; enemy.applyStatusEffect(randomEffectDataEI_Arr); } } // Ignite Check if (Math.random() < hero.igniteChanceOnHit) { enemy.applyStatusEffect({ type: StatusEffectType.IGNITE, source: hero, duration: 5, magnitude: hero.igniteDamagePerSec, tickInterval: 1, stackingType: 'Intensity' }); } // Chaotic Spark Check if (Math.random() < hero.chaoticSparkChance) { var randomEffectDataCS_Arr = getRandomElementalStatusData(hero); if (randomEffectDataCS_Arr) { randomEffectDataCS_Arr.duration = 2; // Override duration enemy.applyStatusEffect(randomEffectDataCS_Arr); } } // Bleed Check (Corrected Duration to 5s) if (Math.random() < hero.bleedChanceOnHit) { enemy.applyStatusEffect({ type: StatusEffectType.BLEED, source: hero, duration: 5, // Use 5 seconds magnitude: hero.bleedDamagePerSec, tickInterval: 1, stackingType: 'Intensity' }); } // Unstable Rift Check if (Math.random() < hero.unstableRiftChance) { var randomDebuffData_Arr = getRandomDebuffData(hero); if (randomDebuffData_Arr) { randomDebuffData_Arr.duration = 4; // Set duration enemy.applyStatusEffect(randomDebuffData_Arr); } } // Umbral Echoes Check if (Math.random() < hero.umbralEchoesChance) { // Use hero's chance var points_UE_Arr = skillDefinitions['Umbral Echoes'].points; var calc_UE_Arr = skillDefinitions['Umbral Echoes'].calcData; if (points_UE_Arr > 0 && calc_UE_Arr) { var confusionDur_Arr = calc_UE_Arr.confusionDurs[points_UE_Arr - 1]; // Spawn echo at the ENEMY's location spawnUmbralEcho(enemy.x, enemy.y, confusionDur_Arr, hero); } } // --- Add other on-hit procs here --- // --- REMOVED Redundant Combined Check --- } // End if (arrow.source === hero) // --- Destroy Arrow After Hit --- // Make sure arrow destroy is called AFTER all effects are applied arrow.destroy(); // Destroys arrow and removes from array break; // Arrow hits one enemy and is destroyed, exit inner enemy loop } // End if (collision detected) } // End FOR loop (enemies) } // End FOR loop (arrows) // --- End Arrow Update Block --- // Update Enemies var enemiesToUpdate = currentRoom.enemies.slice(); // Iterate copy for (var k = 0; k < enemiesToUpdate.length; k++) { var enemy = enemiesToUpdate[k]; // Check if enemy still exists and is active (might have been killed by hero/arrow this frame) // And check if it has an update method if (enemy && enemy.active && enemy.update) { // Also check if the enemy is still in the original array (could be removed by another enemy's effect?) if (currentRoom.enemies.includes(enemy)) { enemy.update(); } } } // ---> ADDED: Update Active Familiar <--- if (hero && hero.activeFamiliar && hero.activeFamiliar.active && hero.activeFamiliar.update) { hero.activeFamiliar.update(); } // ---> END Update Active Familiar <--- // ---> ADDED: Update Decoys <--- for (var d = activeDecoys.length - 1; d >= 0; d--) { var decoy = activeDecoys[d]; if (!decoy) { // Safety check activeDecoys.splice(d, 1); continue; } decoy.remainingDuration--; // Decrement duration if (decoy.remainingDuration <= 0) { // Duration expired, remove visual and data if (decoy.visual && decoy.visual.parent) { decoy.visual.parent.removeChild(decoy.visual); // Remove from game container } activeDecoys.splice(d, 1); // Remove data object from array } } // ---> END Update Decoys <--- // ---> ADDED: Update Active Skill Button UI <--- if (hero && uiContainer) { uiContainer.children.forEach(function (child) { // Check if this child is one of our identified skill buttons if (child.skillName && child.label && child.label.setText) { // Ensure label exists and has setText var skillName = child.skillName; var cooldownRemainingFrames = hero.activeSkillCooldowns[skillName] || 0; if (cooldownRemainingFrames > 0) { var cooldownRemainingSec = (cooldownRemainingFrames / 60).toFixed(1); child.label.setText(cooldownRemainingSec); // Show seconds remaining child.alpha = 0.5; // Dim the button (or change color/tint) } else { // Reset to default appearance when cooldown is 0 var defaultLabel = ""; // Determine default label text switch (skillName) { case 'Power Surge': defaultLabel = "PwrSrg"; break; case 'Legendary Ancestor': defaultLabel = "LegAnc"; break; case 'Nightfall Veil': defaultLabel = "NgtVl"; break; // --- ADDED CASES --- case 'Overload Blast': defaultLabel = "OvrlBlst"; break; case 'Masterwork Creation': defaultLabel = "Mstrwrk"; break; case 'Prismatic Ascendance': defaultLabel = "PrsmAsc"; break; case 'Shadowstep': defaultLabel = "ShdwStp"; break; // --- END ADDED --- // Add other skills if more buttons exist } child.label.setText(defaultLabel); // Reset label text // Reset alpha (ensure the color is correct from initializeGame) // ---> ADDED COLORS for new buttons <--- var baseAlpha = 0.7; // Default base alpha if (skillName === 'Legendary Ancestor' || skillName === 'Masterwork Creation' || skillName === 'Prismatic Ascendance') { baseAlpha = 0.7; // Or adjust if needed for light colors } child.alpha = baseAlpha; // Note: Simplified alpha reset, assuming most are 0.7 } } }); } // ---> END Update Active Skill Button UI <--- // Find closest melee enemy in range and display its cooldown var closestEnemyDist = Infinity; var closestEnemyCD = '---'; if (currentRoom && currentRoom.enemies && hero) { // Check existence for (var i = 0; i < currentRoom.enemies.length; i++) { var enemy = currentRoom.enemies[i]; if (enemy && enemy.active && (enemy instanceof Enemy || enemy instanceof TankEnemy)) { // Check if it's a melee type var dx = enemy.x - hero.x; var dy = enemy.y - hero.y; var distSqr = dx * dx + dy * dy; var minDist = enemy instanceof TankEnemy ? 80 : 70; // Get appropriate range var minDistSqr = minDist * minDist; if (distSqr < minDistSqr) { // If enemy is in range if (distSqr < closestEnemyDist) { // Check if it's the closest one so far closestEnemyDist = distSqr; closestEnemyCD = Math.round(enemy.damageCooldown); // Get its current cooldown } } } } } // Update Enemy Projectiles (assuming they are added directly to gameContainer, not a specific array) // We need to iterate gameContainer children or create a separate enemyProjectiles array // Let's assume for now Projectile class handles its own hero collision check within its update gameContainer.children.forEach(function (child) { // This is inefficient - better to have specific arrays if (child instanceof Projectile && child.update) { // Projectile.update already handles collision with hero // If it didn't, we'd check here: // if (hero && hero.active && child.intersects && child.intersects(hero)) { ... } } }); // checkRoomCleared(); // Now called reliably within enemy.die() }; // End Game Update
===================================================================
--- original.js
+++ change.js
@@ -219,8 +219,9 @@
self.facingDirection = 0;
self.canExitRoom = false;
// ***** ADD ACTIVE SKILL COOLDOWN TRACKING *****
self.activeSkillCooldowns = {}; // Stores { skillName: framesRemaining }
+ self.activeFamiliar = null;
// ***** END ADDED TRACKING *****
// Recalculate Stats (Now implemented)
self.recalculateStats = function () {
// 1. Reset stats to base values
@@ -471,8 +472,11 @@
// Use value at current rank from calcData: procValues: [10, 13, 16, 19, 22]
skillBonusUmbralEchoesProc = calc.procValues[points - 1] / 100;
}
break;
+ case 'Shadow Familiar':
+ success = executeShadowFamiliar(skillData);
+ break;
// Others are mostly procs/actives/summons
}
}
}
@@ -1497,76 +1501,103 @@
self.damageCooldown--;
}
// 3. Update Animation
self.updateAnimation();
- var target = null; // Start with no target
- // --- TARGETING LOGIC ---
- // 1. Prioritize Decoys
- var closestDecoy = null;
- var minDistSqToDecoy = Infinity;
- if (activeDecoys.length > 0) {
- for (var i = 0; i < activeDecoys.length; i++) {
- var decoy = activeDecoys[i];
- var dx = decoy.x - self.x;
- var dy = decoy.y - self.y;
- var distSq = dx * dx + dy * dy;
- if (distSq < minDistSqToDecoy) {
- minDistSqToDecoy = distSq;
- closestDecoy = decoy;
+ // --- Check Status Effects (AFTER duration processing) ---
+ var isConfused = self.activeStatusEffects.some(function (eff) {
+ return eff && eff.type === StatusEffectType.CONFUSE;
+ });
+ // --- Check Status Effects (AFTER duration processing) ---
+ var isConfused = self.activeStatusEffects.some(function (eff) {
+ return eff && eff.type === StatusEffectType.CONFUSE;
+ });
+ // --- AI Behavior ---
+ if (isConfused) {
+ // ---> COPY Confused Behavior from TankEnemy <---
+ if (!self.wanderTarget || Math.random() < 0.02) {
+ // Pick new wander target occasionally
+ self.wanderTarget = {
+ x: self.x + (Math.random() - 0.5) * 200,
+ y: self.y + (Math.random() - 0.5) * 200
+ };
+ // Clamp wander target within walls
+ if (self.wanderTarget.x < wallThickness) {
+ self.wanderTarget.x = wallThickness;
}
+ if (self.wanderTarget.x > roomWidth - wallThickness) {
+ self.wanderTarget.x = roomWidth - wallThickness;
+ }
+ if (self.wanderTarget.y < wallThickness) {
+ self.wanderTarget.y = wallThickness;
+ }
+ if (self.wanderTarget.y > roomHeight - wallThickness) {
+ self.wanderTarget.y = roomHeight - wallThickness;
+ }
}
- target = closestDecoy;
- }
- // 2. If no decoy targeted, check for VISIBLE hero
- if (!target && hero && hero.active) {
- var isHeroCloaked = hero.activeStatusEffects.some(function (effect) {
- return effect.type === StatusEffectType.CLOAKED;
- });
- if (!isHeroCloaked) {
+ var currentMoveSpeedConfused = self.getStat('movementSpeed');
+ if (currentMoveSpeedConfused > 0 && self.wanderTarget) {
+ self.moveTowards(self.wanderTarget.x, self.wanderTarget.y, currentMoveSpeedConfused * 0.6); // Move slower maybe?
+ }
+ self.rotation += 0.1; // Spin slowly? Indicate confusion visually
+ // ---> END Confused Behavior <---
+ } else {
+ // --- Normal (Non-Confused) Behavior ---
+ var target = null;
+ // 1. Prioritize Decoys
+ var closestDecoy = null;
+ var minDistSqToDecoy = Infinity;
+ if (activeDecoys && activeDecoys.length > 0) {
+ for (var i = 0; i < activeDecoys.length; i++) {/* ... find closest decoy ... */}
+ if (closestDecoy) {
+ target = closestDecoy;
+ }
+ }
+ // 2. Target Hero if no decoy and hero visible
+ if (!target && hero && hero.active && !hero.activeStatusEffects.some(function (eff) {
+ return eff && eff.type === StatusEffectType.CLOAKED;
+ })) {
target = hero;
}
- }
- // --- END TARGETING LOGIC ---
- if (target) {
- // Have a target (decoy or hero)
- var targetX = target.x + (Math.random() - 0.5) * 20; // Add jitter
- var targetY = target.y + (Math.random() - 0.5) * 20;
- var currentMoveSpeed = self.getStat('movementSpeed');
- var distance = self.moveTowards(targetX, targetY, currentMoveSpeed); // Move towards target
- var minDistance = 70; // Contact distance
- // --- ATTACK & PUSHBACK ---
- if (distance <= minDistance) {
- // --- Attack Logic (Only attack HERO, not decoy) ---
- if (target === hero) {
- // Check if target IS hero
- if (self.damageCooldown <= 0) {
- self.damageCooldown = self.damageCooldownTime;
- var outgoingDamageInfo = self.calculateOutgoingDamage(self.damageAmount, DamageType.PHYSICAL);
- if (hero.takeDamage) {
- // Target is hero
- hero.takeDamage(outgoingDamageInfo.damage, DamageType.PHYSICAL, self);
+ if (target) {
+ // --- Normal Move/Attack/Pushback Logic ---
+ var targetX = target.x + (Math.random() - 0.5) * 20; // Add jitter
+ var targetY = target.y + (Math.random() - 0.5) * 20;
+ var currentMoveSpeedNormal = self.getStat('movementSpeed');
+ var distance = self.moveTowards(targetX, targetY, currentMoveSpeedNormal); // Move towards target
+ var minDistance = 70;
+ if (distance <= minDistance) {
+ // If in range
+ if (target === hero) {
+ // Only attack/pushback HERO
+ if (self.damageCooldown <= 0) {
+ // Attack if cooldown ready
+ self.damageCooldown = self.damageCooldownTime;
+ var outgoingDamageInfo = self.calculateOutgoingDamage(self.damageAmount, DamageType.PHYSICAL);
+ if (hero.takeDamage) {
+ hero.takeDamage(outgoingDamageInfo.damage, DamageType.PHYSICAL, self);
+ }
}
+ // Pushback logic
+ var pushDx = self.x - target.x;
+ var pushDy = self.y - target.y;
+ var pushDist = Math.sqrt(pushDx * pushDx + pushDy * pushDy);
+ if (pushDist > 0) {
+ var pushAmount = (minDistance - distance) * 0.5;
+ self.x += pushDx / pushDist * pushAmount;
+ self.y += pushDy / pushDist * pushAmount;
+ }
}
- // Pushback logic (Only pushback from hero)
- var pushDx = self.x - target.x; // Use target.x (hero's x)
- var pushDy = self.y - target.y; // Use target.y (hero's y)
- var pushDist = Math.sqrt(pushDx * pushDx + pushDy * pushDy);
- if (pushDist > 0) {
- var pushAmount = (minDistance - distance) * 0.5;
- self.x += pushDx / pushDist * pushAmount;
- self.y += pushDy / pushDist * pushAmount;
- }
- } else {
- // If target is decoy, stop moving towards it when close, don't attack/pushback
+ // If target is decoy, just stop near it.
}
- // --- End Attack Logic ---
+ // --- End Normal Move/Attack/Pushback ---
+ } else {
+ // No target - Stand still
}
- // --- END ATTACK & PUSHBACK ---
- } else {
- // No target - Stand still or wander
+ // --- End Normal Behavior ---
}
- self.handleWallCollision();
- };
+ // --- Final Updates ---
+ self.handleWallCollision(); // Always apply wall collision
+ }; // --- End self.update for Enemy (Melee) ---
return self;
});
// Joystick class
var Joystick = Container.expand(function () {
@@ -1668,8 +1699,111 @@
}
};
return self;
});
+// --- Shadow Familiar Class ---
+var ShadowFamiliar = Container.expand(function (sourceHero, durationSeconds, slowPercent) {
+ var self = Container.call(this);
+ // Basic container, doesn't need full BaseEntity stats (unless it can be attacked?)
+ // --- Properties ---
+ self.sourceHero = sourceHero;
+ self.durationMillis = durationSeconds * 1000;
+ self.slowMagnitude = slowPercent / 100; // Convert percent to decimal for status effect
+ self.auraRadius = 150; // How close enemies need to be to get slowed (adjust)
+ self.auraTickInterval = 30; // How often to check for enemies in aura (frames, e.g., twice per second)
+ self.auraTickCounter = 0;
+ self.active = true; // Mark as active
+ // --- Visuals ---
+ // Use the ShadowFamiliar icon? Or another graphic? Let's use the icon.
+ var familiarVisual = self.attachAsset('ShadowFamiliar_icon', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ alpha: 0.8,
+ // Slightly transparent
+ scaleX: 1.5,
+ scaleY: 1.5 // Make it a bit bigger maybe
+ });
+ // Make it float near the hero? Or stay in one place? Let's have it follow.
+ self.followOffset = {
+ x: -60,
+ y: -60
+ }; // Example offset from hero
+ // --- Timeout for Despawn ---
+ var despawnTimeoutId = LK.setTimeout(function () {
+ self.destroy(); // Call destroy when duration ends
+ }, self.durationMillis);
+ // --- Update Method ---
+ self.update = function () {
+ if (!self.active || !self.sourceHero || !self.sourceHero.active) {
+ // If familiar is inactive or hero disappears, destroy self
+ self.destroy();
+ return;
+ }
+ // Follow the hero
+ self.x = self.sourceHero.x + self.followOffset.x;
+ self.y = self.sourceHero.y + self.followOffset.y;
+ // Bobbing/floating animation? (Optional visual flair)
+ self.y += Math.sin(game.frame * 0.05) * 3; // Simple vertical bob
+ // Aura Check Timer
+ self.auraTickCounter--;
+ if (self.auraTickCounter <= 0) {
+ self.auraTickCounter = self.auraTickInterval; // Reset timer
+ self.applySlowAura(); // Apply slow to nearby enemies
+ }
+ };
+ // --- Helper to Apply Slow Aura ---
+ self.applySlowAura = function () {
+ if (!currentRoom || !currentRoom.enemies) {
+ return;
+ }
+ currentRoom.enemies.forEach(function (enemy) {
+ if (enemy && enemy.active) {
+ var dx = enemy.x - self.x; // Distance from familiar
+ var dy = enemy.y - self.y;
+ var distSq = dx * dx + dy * dy;
+ if (distSq < self.auraRadius * self.auraRadius) {
+ // Enemy is in range, apply SLOW effect
+ enemy.applyStatusEffect({
+ type: StatusEffectType.SLOW,
+ source: self.sourceHero,
+ // Attributed to the hero
+ duration: 2,
+ // Short duration, reapplied frequently by aura
+ magnitude: {
+ speed: self.slowMagnitude,
+ attack: self.slowMagnitude * 0.5
+ },
+ // Slow movement more than attack? Example values
+ stackingType: 'Duration' // Refresh duration
+ });
+ }
+ }
+ });
+ };
+ // --- Destroy Method ---
+ var originalDestroy = self.destroy; // Store original if exists
+ self.destroy = function () {
+ self.active = false;
+ // Clear the despawn timeout if destroyed manually/early
+ if (despawnTimeoutId) {
+ LK.clearTimeout(despawnTimeoutId);
+ despawnTimeoutId = null;
+ }
+ // ---> ADD THIS CHECK <---
+ if (self.sourceHero && self.sourceHero.activeFamiliar === self) {
+ self.sourceHero.activeFamiliar = null; // Clear reference on hero
+ }
+ // ---> END ADDITION <---
+ // Remove from parent container
+ if (self.parent) {
+ self.parent.removeChild(self);
+ }
+ // Call original destroy if needed (though Container might not have one)
+ // if (originalDestroy) { originalDestroy.call(self); }
+ // Remove from any global tracking arrays if necessary (we aren't using one here)
+ };
+ return self;
+});
// StartGameButton class
var StartGameButton = Container.expand(function () {
var self = Container.call(this);
self.attachAsset('startGameButton', {
@@ -1729,8 +1863,16 @@
/****
* Global Constants
****/
// Define Damage Types
+function _typeof(o) {
+ "@babel/helpers - typeof";
+ return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
+ return typeof o;
+ } : function (o) {
+ return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
+ }, _typeof(o);
+}
var DamageType = {
PHYSICAL: 'physical',
FIRE: 'fire',
ICE: 'ice',
@@ -2041,94 +2183,172 @@
BaseEntity.prototype.removeStatusEffect = function (effectId) {
if (!effectId) {
return;
}
- var index = this.activeStatusEffects.findIndex(function (eff) {
- return eff && eff.id === effectId;
- });
+ var index = -1;
+ var timeoutIdToClear = null;
+ // Find the index AND get the timeoutId before removing
+ for (var i = 0; i < this.activeStatusEffects.length; i++) {
+ if (this.activeStatusEffects[i] && this.activeStatusEffects[i].id === effectId) {
+ index = i;
+ timeoutIdToClear = this.activeStatusEffects[i].timeoutId; // Get the stored ID
+ break;
+ }
+ }
if (index > -1) {
- this.activeStatusEffects.splice(index, 1); // Remove from effects array
+ // --- Clear the associated Timeout ---
+ if (timeoutIdToClear) {
+ LK.clearTimeout(timeoutIdToClear); // Prevent expiration callback if manually removed
+ }
+ // Remove from the main effects array
+ this.activeStatusEffects.splice(index, 1);
+ // Force visual update check
+ this.forceVisualRecalculation = true;
}
- // ---> Remove the DURATION TIMER object <---
- // Check if it's the hero and the map exists
- if (this.activeDurationTimers && this.activeDurationTimers.hasOwnProperty(effectId)) {
- delete this.activeDurationTimers[effectId];
- }
- // Force visual update check
- this.forceVisualRecalculation = true;
+ // No need to interact with activeDurationTimers anymore
};
-// This function NO LONGER handles duration decrement or expiry checks.
-// It focuses on DoTs and visual updates.
+// It focuses on DoTs and visual updates based on the current effects list.
BaseEntity.prototype.updateStatusEffects = function () {
- // Flag to trigger tint update if needed (e.g., from apply/remove)
- var applyTintUpdate = this.forceVisualRecalculation || false;
- this.forceVisualRecalculation = false; // Reset flag
- var isCloakedNow = false; // Track current cloak status
- var cloakDurationDisplay = -1; // For debug text
- // ---> Iterate effects for DoTs and determining current state <---
+ // Flag to trigger tint update if needed (set by apply/removeStatusEffect)
+ var applyVisualUpdate = this.forceVisualRecalculation || false;
+ this.forceVisualRecalculation = false; // Reset flag for next frame
+ var isCloakedNow = false; // Track current cloak status for visuals
+ // ---> Iterate effects ONLY for DoTs and determining current state <---
+ // We loop through the array as effects might be added/removed externally too
for (var i = this.activeStatusEffects.length - 1; i >= 0; i--) {
var effectData = this.activeStatusEffects[i];
if (!effectData) {
continue;
} // Safety check
- // Check for cloak presence
+ // Check for cloak presence for visual update later
if (effectData.type === StatusEffectType.CLOAKED) {
isCloakedNow = true;
- // Try reading remaining duration from the hero's timer map for debug
- if (this === hero && this.activeDurationTimers && this.activeDurationTimers[effectData.id]) {
- cloakDurationDisplay = this.activeDurationTimers[effectData.id].framesRemaining;
- }
}
- // --- Handle DoT Ticking (Logic remains) ---
+ // --- Handle DoT Ticking ---
+ // Check if this effect is a Damage Over Time type
var isDoT = effectData.type === StatusEffectType.IGNITE || effectData.type === StatusEffectType.POISON || effectData.type === StatusEffectType.BLEED;
if (isDoT) {
- effectData.ticksRemaining = (effectData.ticksRemaining || 0) - 1;
+ // Ensure ticksRemaining is initialized if needed
+ effectData.ticksRemaining = effectData.ticksRemaining || 0;
+ // Decrement tick timer
+ effectData.ticksRemaining--;
+ // Apply damage if tick timer reaches zero
if (effectData.ticksRemaining <= 0) {
- var dotDamage = effectData.magnitude && effectData.magnitude.dot !== undefined ? effectData.magnitude.dot : effectData.magnitude || 0;
+ var dotDamage = 0;
+ // Safely access magnitude, checking for 'dot' property if it's an object
+ if (effectData.magnitude) {
+ if (_typeof(effectData.magnitude) === 'object' && effectData.magnitude.dot !== undefined) {
+ dotDamage = effectData.magnitude.dot;
+ } else if (typeof effectData.magnitude === 'number') {
+ dotDamage = effectData.magnitude; // Use magnitude directly if it's just a number
+ }
+ }
if (dotDamage > 0) {
- var damageType;
+ var damageType = DamageType.TRUE; // Default damage type
+ // Determine correct damage type based on DoT effect
if (effectData.type === StatusEffectType.IGNITE) {
damageType = DamageType.FIRE;
} else if (effectData.type === StatusEffectType.POISON) {
damageType = DamageType.POISON;
} else if (effectData.type === StatusEffectType.BLEED) {
damageType = DamageType.PHYSICAL;
- } else {
- damageType = DamageType.TRUE;
}
+ // Apply the DoT damage (bypassing dodge)
this.takeDamage(dotDamage, damageType, effectData.source, true);
}
+ // Reset tick timer (use tickInterval from the effect data, default to 60 frames/1 sec)
effectData.ticksRemaining = effectData.tickInterval || 60;
}
}
// --- End DoT Ticking ---
} // End loop through activeStatusEffects
+ // ----- Update Debug Text (Shows presence, NOT duration) -----
+ // Keep this removed if you previously removed it. If kept for testing:
+ /*
+ if (statusDebugText && this === hero) {
+ var statusString = "Status: ";
+ if (this.activeStatusEffects.length > 0) {
+ statusString += this.activeStatusEffects.map(function(eff){ return eff ? eff.type : '???'; }).join(", ");
+ } else {
+ statusString += "[]";
+ }
+ if (statusString.length > 60) statusString = statusString.substring(0, 57) + "...";
+ statusDebugText.setText(statusString);
+ statusDebugText.fill = 0x00FFFF;
+ } else if (statusDebugText) {
+ statusDebugText.setText("Status: ---");
+ }
+ */
+ // ----- End Debug Text Update -----
// ----- Update Visuals (Alpha for Hero, Tint for all) -----
- // Update alpha for hero based on current cloak status found in the loop above
+ // Update alpha for hero based purely on the presence check done above
if (this === hero) {
this.alpha = isCloakedNow ? 0.3 : 1.0;
}
- // Update tint if apply/remove occurred this frame OR first update
- // (forceVisualRecalculation flag handles this)
- if (applyTintUpdate) {
- var finalTint = 0xFFFFFF;
+ // Update tint only if flagged (apply/remove happened or forced)
+ if (applyVisualUpdate) {
+ var finalTint = 0xFFFFFF; // Default White
var highestPriority = -1;
+ // Check remaining effects in activeStatusEffects array for tint calculation
this.activeStatusEffects.forEach(function (eff) {
if (!eff) {
return;
- }
+ } // Safety check
var effectTint = 0xFFFFFF;
var priority = 0;
- switch /* ... tint cases ... */ (eff.type) {}
+ // Assign tint and priority based on effect type
+ switch (eff.type) {
+ case StatusEffectType.FREEZE:
+ effectTint = 0x00AAFF;
+ priority = 5;
+ break;
+ case StatusEffectType.STUN:
+ effectTint = 0xAAAA00;
+ priority = 5;
+ break;
+ case StatusEffectType.IGNITE:
+ effectTint = 0xFF8800;
+ priority = 4;
+ break;
+ case StatusEffectType.BLEED:
+ effectTint = 0xCC0000;
+ priority = 4;
+ break;
+ case StatusEffectType.POISON:
+ effectTint = 0x00CC00;
+ priority = 3;
+ break;
+ case StatusEffectType.SHOCK:
+ effectTint = 0xFFFF00;
+ priority = 3;
+ break;
+ case StatusEffectType.CHILL:
+ case StatusEffectType.SLOW:
+ effectTint = 0x00AAFF;
+ priority = 2;
+ break;
+ case StatusEffectType.CHARM:
+ effectTint = 0xFF88CC;
+ priority = 1;
+ break;
+ case StatusEffectType.CONFUSE:
+ effectTint = 0xAA00AA;
+ priority = 1;
+ break;
+ // Added confuse tint back
+ // CLOAKED has no tint, handled by alpha
+ }
+ // Update finalTint if current effect has higher priority
if (priority > highestPriority) {
highestPriority = priority;
finalTint = effectTint;
}
});
+ // Apply the calculated final tint
this.tint = finalTint;
}
// ----- End Visual Update -----
-}; // End NEW updateStatusEffects
+}; // End NEW BaseEntity.prototype.updateStatusEffects
// Damage Calculation Logic
BaseEntity.prototype.calculateDamageTaken = function (amount, damageType, attackerStats) {
if (damageType === DamageType.TRUE) {
return amount;
@@ -2316,8 +2536,10 @@
if (!this.active) {
return;
} // Use 'this' inside prototype methods
this.updateModifiers(); // Update durations and remove expired buffs/debuffs
+ // --- Update Status Effects (Still needed for DoTs/Visuals) ---
+ this.updateStatusEffects();
};
// Define the die method on the PROTOTYPE
BaseEntity.prototype.die = function (killer) {
if (!this.active) {
@@ -3933,8 +4155,14 @@
var skillTreePointsText = null; // Text on individual tree screens
var roomDisplay = null;
var healthText = null;
var isGameOver = false;
+// ---> ADDED: Skill Button Management <---
+var MAX_ACTIVE_SKILL_SLOTS = 4;
+var activeSkillSlots = []; // Array to hold skill names assigned to slots [slot0, slot1, slot2, slot3]
+var skillButtonReferences = {}; // Map: { skillName: buttonContainerObject }
+var skillButtonPositions = []; // Array to store calculated positions [{x, y}, {x, y}, ...]
+// ---> END Skill Button Management <---
game.addChild(gameContainer); // For gameplay elements (hero, enemies, projectiles)
game.addChild(uiContainer); // For UI elements (joysticks, buttons not part of popups)
game.addChild(menuContainer); // For popups (skill tree, skill details)
/****
@@ -4125,8 +4353,10 @@
// 2. Check for fixed cooldown property
} else if (calcData.cooldown !== undefined) {
// Check if the property exists
baseCooldownSeconds = calcData.cooldown;
+ } else if (skillName === 'Shadow Familiar') {
+ baseCooldownSeconds = 30;
}
// 3. If neither specific array nor fixed property found, use the default 60s
}
// --- End Fetch Cooldown ---
@@ -4278,8 +4508,46 @@
} // Revert tint if needed
}, durationSeconds * 1000);
return true; // Skill executed
}
+function executeShadowFamiliar(skillData) {
+ if (!hero) {
+ return false;
+ }
+ var points = skillData.points;
+ var calc = skillData.calcData;
+ if (!calc || points <= 0) {
+ return false;
+ }
+ // --- Check if a familiar already exists? Prevent multiple summons? ---
+ // We need a way to track the active familiar if only one is allowed.
+ // Let's add a reference to the hero object.
+ if (hero.activeFamiliar && hero.activeFamiliar.active) {
+ // Familiar already active, maybe refresh duration? Or do nothing?
+ // For now, let's prevent summoning a new one.
+ return false; // Indicate skill didn't "execute" fully (no cooldown trigger)
+ }
+ // --- End Check ---
+ var duration = calc.durations[points - 1];
+ var slowPercent = calc.slowValues[points - 1];
+ // Create the familiar instance
+ var familiar = new ShadowFamiliar(hero, duration, slowPercent);
+ // Add to game container
+ gameContainer.addChild(familiar);
+ // Store reference on hero to prevent duplicates and allow easy access/removal
+ hero.activeFamiliar = familiar;
+ // Add a listener to remove the reference when it's destroyed
+ // Requires an 'ondestroy' event or similar. If LK doesn't have one,
+ // we need to manually clear hero.activeFamiliar in ShadowFamiliar.destroy
+ // Let's modify ShadowFamiliar.destroy:
+ /* Add this inside ShadowFamiliar.destroy, before removing from parent:
+ if (self.sourceHero && self.sourceHero.activeFamiliar === self) {
+ self.sourceHero.activeFamiliar = null; // Clear reference on hero
+ }
+ */
+ // TODO: Add Familiar Summon SFX
+ return true; // Skill executed successfully
+}
function executeShadowstep(skillData) {
if (!hero || !hero.joystick) {
return false;
} // Need joystick for direction
@@ -4656,17 +4924,22 @@
if (!skillData) {
return;
}
if (heroSkillPoints > 0 && skillData.points < skillData.max) {
+ var isFirstPoint = skillData.points === 0; // Check if this is the FIRST point allocated
heroSkillPoints--;
- skillData.points++; // Update points in central definition
- // ***** CALL RECALCULATE STATS *****
+ skillData.points++; // Allocate point
+ // ---> ADD Button Creation/Assignment Logic <---
+ if (isFirstPoint && isSkillActive(skillTitle)) {
+ // Check if it's an active skill using a helper
+ assignSkillToNextSlot(skillTitle); // Assign skill and potentially create button
+ }
+ // ---> END Button Logic <---
+ // Recalculate Stats AFTER allocating point
if (hero && hero.recalculateStats) {
hero.recalculateStats();
- // Optionally, immediately update health display if max health changed
updateRoomDisplay();
}
- // ***** END CALL *****
// Refresh global skill point displays
refreshSkillPointsText(); // Updates display on popup and underlying screens
// --- Update EXISTING popup content using direct variables ---
// ** No need for getChildByName **
@@ -5330,8 +5603,38 @@
var joystick = uiContainer.addChild(new Joystick());
hero.joystick = joystick;
var aimingJoystick = uiContainer.addChild(new AimingJoystick());
hero.aimingJoystick = aimingJoystick;
+ // ---> CALCULATE Skill Button Positions <---
+ var joyX = aimingJoystick.x;
+ var joyY = aimingJoystick.y;
+ var joyRadius = 150; // Approximate radius outside joystick graphic
+ var buttonOffset = 100; // Distance outwards from joyRadius
+ var totalButtonRadius = joyRadius + buttonOffset;
+ var buttonSize = 150; // New, larger button size (adjust as needed)
+ skillButtonPositions = [
+ // Slot 0: Left of joystick
+ {
+ x: joyX - totalButtonRadius,
+ y: joyY
+ },
+ // Slot 1: Upper-Left of joystick
+ {
+ x: joyX - totalButtonRadius * Math.cos(Math.PI / 4),
+ y: joyY - totalButtonRadius * Math.sin(Math.PI / 4)
+ },
+ // Slot 2: Above-Left of joystick (more North)
+ {
+ x: joyX - totalButtonRadius * Math.cos(Math.PI / 2.5),
+ y: joyY - totalButtonRadius * Math.sin(Math.PI / 2.5)
+ },
+ // Adjusted angle slightly more North
+ // Slot 3: Directly Above joystick
+ {
+ x: joyX,
+ y: joyY - totalButtonRadius
+ }];
+ // ---> END Calculate Positions <---
} else {
// Reset hero state if restarting
hero.active = true; // Ensure hero is active
hero.health = hero.getStat('maxHealth'); // Heal to full calculated max health
@@ -5387,227 +5690,8 @@
healthText.x = 250;
healthText.y = 60;
healthText.anchor.set(0, 0.5);
uiContainer.addChild(healthText);
- // ***** ADD PLACEHOLDER ACTIVE SKILL BUTTONS *****
- var skillButtonSize = 120;
- var skillButtonPadding = 30;
- var startButtonX = roomWidth - skillButtonPadding - skillButtonSize / 2; // Right side
- var startButtonY = 200; // Example Y position
- // Button 1: Power Surge (Red Box)
- var powerSurgeButton = LK.getAsset('box', {
- width: skillButtonSize,
- height: skillButtonSize,
- color: 0xFF0000,
- alpha: 0.7,
- anchorX: 0.5,
- anchorY: 0.5,
- x: startButtonX,
- y: startButtonY
- });
- powerSurgeButton.interactive = true;
- powerSurgeButton.down = function () {
- activateSkill('Power Surge');
- }; // Link to activation function
- uiContainer.addChild(powerSurgeButton);
- var powerSurgeButton = uiContainer.children.find(function (child) {
- return child.down && child.color === 0xFF0000;
- }); // Find the button (adjust if needed)
- if (powerSurgeButton) {
- powerSurgeButton.skillName = 'Power Surge';
- var psLabel = uiContainer.children.find(function (child) {
- return child instanceof Text2 && child.text === "PwrSrg";
- });
- if (psLabel) {
- powerSurgeButton.label = psLabel;
- }
- }
- // Add Text label
- var psLabel = new Text2("PwrSrg", {
- size: 30,
- fill: 0xffffff
- });
- psLabel.anchor.set(0.5);
- psLabel.x = powerSurgeButton.x;
- psLabel.y = powerSurgeButton.y;
- uiContainer.addChild(psLabel);
- // Button 2: Legendary Ancestor (Yellow Box)
- var legendaryAncestorButton = LK.getAsset('box', {
- width: skillButtonSize,
- height: skillButtonSize,
- color: 0xFFFF00,
- alpha: 0.7,
- anchorX: 0.5,
- anchorY: 0.5,
- x: startButtonX,
- y: startButtonY + skillButtonSize + skillButtonPadding
- });
- legendaryAncestorButton.interactive = true;
- legendaryAncestorButton.down = function () {
- activateSkill('Legendary Ancestor');
- };
- uiContainer.addChild(legendaryAncestorButton);
- var legendaryAncestorButton = uiContainer.children.find(function (child) {
- return child.down && child.color === 0xFFFF00;
- }); // Find the button (adjust if needed)
- if (legendaryAncestorButton) {
- legendaryAncestorButton.skillName = 'Legendary Ancestor';
- var laLabel = uiContainer.children.find(function (child) {
- return child instanceof Text2 && child.text === "LegAnc";
- });
- if (laLabel) {
- legendaryAncestorButton.label = laLabel;
- }
- }
- // Add Text label
- var laLabel = new Text2("LegAnc", {
- size: 30,
- fill: 0x000000
- });
- laLabel.anchor.set(0.5);
- laLabel.x = legendaryAncestorButton.x;
- laLabel.y = legendaryAncestorButton.y;
- uiContainer.addChild(laLabel);
- // Button 3: Nightfall Veil (Purple Box)
- var nightfallVeilButton = LK.getAsset('box', {
- width: skillButtonSize,
- height: skillButtonSize,
- color: 0x8800FF,
- alpha: 0.7,
- anchorX: 0.5,
- anchorY: 0.5,
- x: startButtonX,
- y: startButtonY + 2 * (skillButtonSize + skillButtonPadding)
- });
- nightfallVeilButton.interactive = true;
- nightfallVeilButton.down = function () {
- activateSkill('Nightfall Veil');
- };
- // ---> Add Skill Name Identifier <---
- nightfallVeilButton.skillName = 'Nightfall Veil';
- uiContainer.addChild(nightfallVeilButton);
- // Add Text label AND store reference on button
- var nvLabel = new Text2("NgtVl", {
- size: 30,
- fill: 0xffffff
- });
- nvLabel.anchor.set(0.5);
- nvLabel.x = nightfallVeilButton.x;
- nvLabel.y = nightfallVeilButton.y;
- // ---> Store Label Reference on Button <---
- nightfallVeilButton.label = nvLabel;
- uiContainer.addChild(nvLabel);
- // ---> ADDED BUTTONS FOR NEW SKILLS <---
- var buttonYOffset = 3 * (skillButtonSize + skillButtonPadding); // Start below Nightfall Veil
- // Button 4: Overload Blast (Orange Box)
- var overloadBlastButton = LK.getAsset('box', {
- width: skillButtonSize,
- height: skillButtonSize,
- color: 0xFF8C00,
- alpha: 0.7,
- anchorX: 0.5,
- anchorY: 0.5,
- x: startButtonX,
- y: startButtonY + buttonYOffset
- });
- overloadBlastButton.interactive = true;
- overloadBlastButton.down = function () {
- activateSkill('Overload Blast');
- };
- overloadBlastButton.skillName = 'Overload Blast';
- uiContainer.addChild(overloadBlastButton);
- var obLabel = new Text2("OvrlBlst", {
- size: 30,
- fill: 0xffffff
- });
- obLabel.anchor.set(0.5);
- obLabel.x = overloadBlastButton.x;
- obLabel.y = overloadBlastButton.y;
- overloadBlastButton.label = obLabel;
- uiContainer.addChild(obLabel);
- buttonYOffset += skillButtonSize + skillButtonPadding; // Increment Y offset
- // Button 5: Masterwork Creation (Silver Box)
- var masterworkButton = LK.getAsset('box', {
- width: skillButtonSize,
- height: skillButtonSize,
- color: 0xC0C0C0,
- alpha: 0.7,
- anchorX: 0.5,
- anchorY: 0.5,
- x: startButtonX,
- y: startButtonY + buttonYOffset
- });
- masterworkButton.interactive = true;
- masterworkButton.down = function () {
- activateSkill('Masterwork Creation');
- };
- masterworkButton.skillName = 'Masterwork Creation';
- uiContainer.addChild(masterworkButton);
- var mwLabel = new Text2("Mstrwrk", {
- size: 30,
- fill: 0x000000
- });
- mwLabel.anchor.set(0.5);
- mwLabel.x = masterworkButton.x;
- mwLabel.y = masterworkButton.y;
- masterworkButton.label = mwLabel;
- uiContainer.addChild(mwLabel);
- buttonYOffset += skillButtonSize + skillButtonPadding;
- // Button 6: Prismatic Ascendance (White Box)
- var prismaticButton = LK.getAsset('box', {
- width: skillButtonSize,
- height: skillButtonSize,
- color: 0xFFFFFF,
- alpha: 0.7,
- anchorX: 0.5,
- anchorY: 0.5,
- x: startButtonX,
- y: startButtonY + buttonYOffset
- });
- prismaticButton.interactive = true;
- prismaticButton.down = function () {
- activateSkill('Prismatic Ascendance');
- };
- prismaticButton.skillName = 'Prismatic Ascendance';
- uiContainer.addChild(prismaticButton);
- var paLabel = new Text2("PrsmAsc", {
- size: 30,
- fill: 0x000000
- });
- paLabel.anchor.set(0.5);
- paLabel.x = prismaticButton.x;
- paLabel.y = prismaticButton.y;
- prismaticButton.label = paLabel;
- uiContainer.addChild(paLabel);
- buttonYOffset += skillButtonSize + skillButtonPadding;
- // Button 7: Shadowstep (Dark Grey Box)
- var shadowstepButton = LK.getAsset('box', {
- width: skillButtonSize,
- height: skillButtonSize,
- color: 0x555555,
- alpha: 0.7,
- anchorX: 0.5,
- anchorY: 0.5,
- x: startButtonX,
- y: startButtonY + buttonYOffset
- });
- shadowstepButton.interactive = true;
- shadowstepButton.down = function () {
- activateSkill('Shadowstep');
- };
- shadowstepButton.skillName = 'Shadowstep';
- uiContainer.addChild(shadowstepButton);
- var ssLabel = new Text2("ShdwStp", {
- size: 30,
- fill: 0xffffff
- });
- ssLabel.anchor.set(0.5);
- ssLabel.x = shadowstepButton.x;
- ssLabel.y = shadowstepButton.y;
- shadowstepButton.label = ssLabel;
- uiContainer.addChild(ssLabel);
- // buttonYOffset += skillButtonSize + skillButtonPadding; // Increment if adding more
- // ***** END PLACEHOLDER BUTTONS ***** // Moved this comment down
updateRoomDisplay(); // Initial UI text update
playRoomMusic(currentRoom.number);
currentRoom.startSpawning();
}
@@ -5638,23 +5722,24 @@
currentRoom.enemiesKilled = currentRoom.killGoal; // Mark as cleared for kill count
updateRoomDisplay();
checkRoomCleared(); // Trigger cleared state
};
-// Switch Weapon Zone/Button
+// Add new switchWeaponZone creation at center right
var switchWeaponZone = LK.getAsset('switchWeaponZone', {
anchorX: 0.5,
anchorY: 0.5
});
-switchWeaponZone.x = roomWidth - 250; // Position lower right
-switchWeaponZone.y = roomHeight - 500;
+switchWeaponZone.x = roomWidth - 150; // Position near center right edge
+switchWeaponZone.y = roomHeight / 2; // Center vertically
uiContainer.addChild(switchWeaponZone);
-switchWeaponZone.interactive = true; // Make the zone itself clickable
+switchWeaponZone.interactive = true;
switchWeaponZone.down = function (x, y, obj) {
if (isPopupActive || isSkillDetailPopupActive || !hero) {
return;
}
hero.switchWeapon();
};
+// ---> END MOVE Weapon Switch <---
// --- Initial Setup Call ---
initializeMainMenu(); // Start by showing the main menu
/****
* Game Update Loop
@@ -5814,8 +5899,13 @@
enemy.update();
}
}
}
+ // ---> ADDED: Update Active Familiar <---
+ if (hero && hero.activeFamiliar && hero.activeFamiliar.active && hero.activeFamiliar.update) {
+ hero.activeFamiliar.update();
+ }
+ // ---> END Update Active Familiar <---
// ---> ADDED: Update Decoys <---
for (var d = activeDecoys.length - 1; d >= 0; d--) {
var decoy = activeDecoys[d];
if (!decoy) {
A round button with icons of a sword and bow crossed over a shield, hinting at weapon switching.. Game interface icon. Medieval theme with crossed weapons on a shield. High contrast and intuitive design.
A rugged medieval bow with a wooden frame and slightly frayed string, perfect for a fantasy setting.. Game asset. Rustic and worn. Medieval fantasy style. High detail with visible wood grain.
Remove the joystick stick
A dark, stone-walled dungeon chamber viewed directly from above. The floor is uneven with scattered bones and chains. Each wall has an entrance centered in the middle, like arched doorways, positioned on the top, bottom, left, and right sides. The room fills the entire frame, with torch-lit ambiance.. Full-frame, top-down view of a stone-walled dungeon chamber. Uneven floor, bones, chains, torch lighting. Open, arched entrances centered on each wall: top, bottom, left, and right. No 3D perspective, even lighting.
A high-tech command center with a glowing grid floor and sleek metallic walls. The room is viewed from directly above and has open entrances centered in the middle of each wall (top, bottom, left, and right) for easy transitions. Neon lights and holographic screens line the walls, casting a blue glow.. Full-frame, top-down view of a futuristic command center. Glowing grid floor, metallic walls, neon lights. Open entrances centered on each wall: top, bottom, left, and right. Blue glow, no perspective distortion.
A top-down view of jungle ruins with moss-covered stone walls and floors. The floor is scattered with vines and broken pillars. Each wall has an entrance centered in the middle, resembling natural archways positioned on the top, bottom, left, and right. Sunlight filters through, illuminating the room softly.. Full-frame, top-down view of jungle ruins. Moss-covered stone walls and floors, vines, broken pillars. Open natural archways centered on each wall: top, bottom, left, and right. Soft sunlight, no perspective distortion.
A pixelated skull with green digital "code streams" dripping down, symbolizing a destructive digital attack. Neon green and dark gray.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.
A circular emblem with a tree at its center, its branches intertwining with a glowing red lineage symbol.. Colors: Deep red, gold, and subtle white highlights.
Elemental Gear Icon: A gear made of multiple materials (fire, ice, lightning, and shadow) fused together, symbolizing crafting hybrid powers.. Colors: Vibrant orange, blue, yellow, and dark purple.
Shattered Prism Icon: A cracked prism emitting chaotic light beams, symbolizing untapped magical potential.. Colors: Neon purple and silver with multicolored light fragments.
Fractured Sphere Icon: A glowing orb breaking apart into jagged, floating shards, with chaotic energy swirling around it.. Colors: Neon purple, black, and electric green.
Phantom Mask Icon: A mysterious, floating mask with glowing eyes and tendrils of shadow curling around it, symbolizing illusions and deception.. Colors: White mask with glowing blue accents and black shadows.
Backdrop: An ancient, mystical forest with glowing runes etched into massive tree trunks. Colors: Earthy greens and browns with soft golden accents. Details: Misty ambiance with faint ethereal figures in the background.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.
Backdrop: A grand forge surrounded by molten lava and glowing hammers mid-swing. Colors: Fiery reds and oranges with metallic silver and gray. Details: Sparks flying and glowing weapon fragments scattered around.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.
Backdrop: A crystal cavern with refracted light beams splitting into vibrant colors. Colors: Radiant rainbow hues with a soft, dark background. Details: Floating crystals and magical glowing particles.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.
Backdrop: A warped reality scene with twisting, fragmented terrain and a swirling vortex in the background. Colors: Deep purples, neon pinks, and electric greens. Details: Fractured floating rocks and glitch-like patterns.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.
Backdrop: A dark, shadowy realm with faint glowing outlines of jagged structures and flowing mist. Colors: Black, deep purples, and faint blue highlights. Details: Shadows shifting and subtle glowing runes.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.
Weapon switch icon. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.
Start game button. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.
Big red kill button. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows.
Top-down view of a floating mechanical robot with a circular body. Thin, robotic arms extend outward, metallic and glowing. The head is small with glowing eyes. Strictly top-down view, no perspective or angle. Clean and detailed for 2D gameplay. Single Game Texture. In-Game asset. Top-down view. No shadows. 2D style. High contrast. Blank background.
A futuristic, top-down 2D game room featuring a minimalist industrial design. The room should have four distinct doorways at cardinal directions (up, down, left, and right). Each doorway should blend seamlessly into the room's aesthetic, with metallic frames and subtle glowing edges to indicate navigability. The room maintains its clean, tiled walls and floor, accented with industrial details like exposed pipes, vents, and panels. Lighting is ambient, with a mix of warm tones near the top and cooler tones along the walls. The overall theme is a high-tech but slightly weathered environment, ready for player navigation. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
A Dagger Icon with 1's and 0's dripping off of it like blood. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A stylized, sleek cybernetic boot or leg silhouette with clear motion lines trailing behind it (like speed lines). Alternatively, three chevrons (>>>) pointing forward, glowing with blue energy, suggesting rapid advancement.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
Two stylized head silhouettes (one representing the hero, one the enemy) connected by arcing lines of digital energy or circuit patterns. Color could be a mix of blue (control) and maybe red/purple (target).. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A shield shape formed from a 1's and 0's matrix pattern (like circuitry). Some lines of the grid could be missing or 'glitching' out, suggesting attacks passing through harmlessly. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
Concentric circles or energy waves expanding outwards from a central point. The waves could be depicted as sharp lines of light blue or white energy. Could also incorporate small lightning-like sparks within the surge.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A more intense version of Power Surge. A fractured or exploding core shape at the center, emitting powerful, jagged energy waves (possibly in yellow or orange on top of blue). Could incorporate classic explosion symbol elements but rendered in the cybernetic style. Should look significantly more powerful than Power Surge.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
a cracked stone tablet or ancient scroll depicting elemental symbols with a subtle glow.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A hand silhouette centrally placed, with symbolic representations of different elements (fire, ice/water, lightning/wind) swirling around it or emanating from the fingertips. Could also be intersecting elemental runes.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A primal stone axe or spearhead with stylized speed lines or a spectral blue aura indicating swift movement.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A glowing paw print (wolf, bear, or cat-like) leaving a faint spectral trail.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A bundle of glowing green herbs tied together, or a single stylized leaf with potent green light radiating from its veins.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
a translucent, ghostly shield or a faint outline of a guardian spirit figure.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
An imposing, ornate tribal mask or helmet, perhaps with glowing eyes or runic carvings.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A stylized hammer striking an anvil, creating fiery sparks or engulfing the hammer head in flames.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A potion bottle or flask swirling with multiple distinct colors (red, blue, green).. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A arrow and sword splitting into a double arrow and double sword signifying a extra shot/sword.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
Could also be a stylized recycling symbol combined with an upward arrow or a plus sign.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A symbol merging a sword and an arrow/bow, perhaps crossing each other with energy flowing between them.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A stylized metallic bracer or weapon hilt showing empty sockets being filled by small, glowing runes or gems.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A glowing, ornate hammer imbued with power, or a weapon silhouette undergoing a visible transformation with radiating light and complex runic patterns.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A sharp, faceted red crystal or gem shard glowing hotly.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
An angular, crystalline shield shimmering with blue light.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
Could also be a green crystal pulsing with soft light.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A sharp, angular gust of wind symbol in bright yellow, or a stylized yellow feather.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A sharply focused eye symbol in deep indigo color. Could also be a fractured mirror shard reflecting light, or an indigo crystal with internal sparks/light flashes.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
Two different colored streams of energy (e.g., red and blue) flowing and swirling together in the center. Could also be a crystal icon split into two distinct colors.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A classic prism shape refracting a beam of white light into a rainbow spectrum. Could also be a figure surrounded by a swirling aura containing all the skill colors.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A crackling spark of energy rapidly shifting between multiple colors (purple, green, orange). Could also be a die symbol with elemental icons instead of pips, or a weapon impact with a question mark.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A shield shape that looks warped, dissolving at the edges, or made of static/glitches. Could show an arrow bouncing off at a weird angle or fizzling into nothing.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A jagged tear or swirling vortex in space, leaking multi-colored, chaotic energy. Could incorporate shifting, abstract symbols or question marks within the rift.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A silhouette of the hero glitching or flickering between different colors or slightly different forms. Could also be an upward arrow surrounded by swirling question marks or dice symbols.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A branching pattern like lightning or cracks, but made of chaotic, multi-colored energy. Could also be a visual of one chaotic explosion triggering others nearby.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A clock face or hourglass that is visibly cracked or shattering. Could also be a die symbol mid-roll or showing multiple faces at once.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
An imploding geometric shape or structure crumbling inwards into a chaotic void/vortex. Could be an intense version of the Unstable Rift, looking more menacing and powerful.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A boot icon dissolving into smoke or shadow at the heel. Sound wave symbol with a line striking through it, indicating silence.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A stylized cloak hood casting a deep shadow, with only faint eyes or nothing visible within. Could also be a figure splitting into a solid version and a shadowy decoy.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A sharp, wicked-looking dagger or blade edge dripping with black, shadowy substance.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A silhouette of a small, mischievous shadow creature (imp, tendril beast, raven?). Could also be a pair of glowing eyes peering out from darkness.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A dagger or fist icon striking, but with sound waves around it being cancelled or muffled (e.g., crossed out or dissolving).. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
A central figure with several fading, translucent shadow copies trailing behind or positioned nearby. Could also be a weapon swing that leaves dark afterimages.. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows