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
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot set properties of undefined (setting 'fill')' in or related to this line: 'healthText.style.fill = 0x00FF00;' Line Number: 3209
Code edit (11 edits merged)
Please save this source code
User prompt
The enemy animations are flashing while rotating through the images, fix this so it is smooth and doesn't flash
Code edit (1 edits merged)
Please save this source code
User prompt
Change all of these "shapes" to image assets please: LK.init.shape('AdaptiveConstruction_icon', {width:80, height:80, color:0xabfa55, shape:'box'}) LK.init.shape('AncientWisdom_icon', {width:80, height:80, color:0x4bc7a6, shape:'box'}) LK.init.shape('AnomalyShift_icon', {width:80, height:80, color:0x36dda5, shape:'box'}) LK.init.shape('BeastTamer_icon', {width:80, height:80, color:0xb28b49, shape:'box'}) LK.init.shape('BlueBulwark_icon', {width:80, height:80, color:0xf5322c, shape:'box'}) LK.init.shape('CascadeofDisorder_icon', {width:80, height:80, color:0x6ba212, shape:'box'}) LK.init.shape('ChaoticSpark_icon', {width:80, height:80, color:0xb3aa7a, shape:'box'}) LK.init.shape('ChromaticFusion_icon', {width:80, height:80, color:0x316dbd, shape:'box'}) LK.init.shape('DodgeMatrix_icon', {width:80, height:80, color:0x5060d3, shape:'box'}) LK.init.shape('ElementalInfusion_icon', {width:80, height:80, color:0x4d931b, shape:'box'}) LK.init.shape('ElementalMastery_icon', {width:80, height:80, color:0x91620d, shape:'box'}) LK.init.shape('EntropyShield_icon', {width:80, height:80, color:0xf0c6c5, shape:'box'}) LK.init.shape('FracturedFate_icon', {width:80, height:80, color:0x766e87, shape:'box'}) LK.init.shape('GreenGrowth_icon', {width:80, height:80, color:0x13c21f, shape:'box'}) LK.init.shape('HerbalRemedies_icon', {width:80, height:80, color:0xbcc511, shape:'box'}) LK.init.shape('HybridCrafting_icon', {width:80, height:80, color:0x329fad, shape:'box'}) LK.init.shape('IndigoInsight_icon', {width:80, height:80, color:0xbaa469, shape:'box'}) LK.init.shape('LegendaryAncestor_icon', {width:80, height:80, color:0x2b7ff1, shape:'box'}) LK.init.shape('MasterworkCreation_icon', {width:80, height:80, color:0x16b9dc, shape:'box'}) LK.init.shape('MidnightCrescendo_icon', {width:80, height:80, color:0x3b6ecc, shape:'box'}) LK.init.shape('ModularEnhancement_icon', {width:80, height:80, color:0xd1035f, shape:'box'}) LK.init.shape('MuffledStrikes_icon', {width:80, height:80, color:0xe23831, shape:'box'}) LK.init.shape('NeuralHijack_icon', {width:80, height:80, color:0x1bee31, shape:'box'}) LK.init.shape('NightfallVeil_icon', {width:80, height:80, color:0x07f897, shape:'box'}) LK.init.shape('OverclockCore_icon', {width:80, height:80, color:0x42be3e, shape:'box'}) LK.init.shape('OverloadBlast_icon', {width:80, height:80, color:0xfae44d, shape:'box'}) LK.init.shape('PowerSurge_icon', {width:80, height:80, color:0x86d5d7, shape:'box'}) LK.init.shape('PrismaticAscendance_icon', {width:80, height:80, color:0x4d2b3b, shape:'box'}) LK.init.shape('RealityCollapse_icon', {width:80, height:80, color:0xf9e5c0, shape:'box'}) LK.init.shape('RedRadiance_icon', {width:80, height:80, color:0x146a37, shape:'box'}) LK.init.shape('ReflexAccelerator_icon', {width:80, height:80, color:0x8eaec7, shape:'box'}) LK.init.shape('ResourcefulSalvage_icon', {width:80, height:80, color:0x2d0cd9, shape:'box'}) LK.init.shape('RunicSlots_icon', {width:80, height:80, color:0x005446, shape:'box'}) LK.init.shape('SerratedShade_icon', {width:80, height:80, color:0x008cdc, shape:'box'}) LK.init.shape('ShadowFamiliar_icon', {width:80, height:80, color:0xe735e2, shape:'box'}) LK.init.shape('SpiritGuidance_icon', {width:80, height:80, color:0x6d76de, shape:'box'}) LK.init.shape('UmbralEchoes_icon', {width:80, height:80, color:0x8d305c, shape:'box'}) LK.init.shape('UnstableRift_icon', {width:80, height:80, color:0x67cffb, shape:'box'}) LK.init.shape('WarriorSpirit_icon', {width:80, height:80, color:0x17566d, shape:'box'}) LK.init.shape('WhisperedSteps_icon', {width:80, height:80, color:0x7a976b, shape:'box'}) LK.init.shape('YellowZephyr_icon', {width:80, height:80, color:0x4f78ed, shape:'box'})
Code edit (1 edits merged)
Please save this source code
Code edit (2 edits merged)
Please save this source code
User prompt
Please fix the bug: 'Uncaught TypeError: activeSkillTreePopup.getChildByName is not a function' in or related to this line: 'var pointsTextNode = activeSkillTreePopup.getChildByName(skillName + "_pointsText");' Line Number: 2031
Code edit (1 edits merged)
Please save this source code
User prompt
Please fix the bug: 'Uncaught TypeError: skillPopup.getChildByName is not a function' in or related to this line: 'var descTextElement = skillPopup.getChildByName("skillDescriptionText");' Line Number: 1804
Code edit (1 edits merged)
Please save this source code
User prompt
The skill descriptions are not updating like they should be for the forge of possibilities skill tree skills
User prompt
History Code Assets Play Publish ↕️Scroll up to show more VA Great, now for the next ranks, take out the words "Each rank" so they start with Increases. ✅ Update Data Spike description to remove 'Each rank' from the text ⏪Revert to here VA Great, now when I am "spending" my skill points into the skill node of choice, I want the 0/5 number to increase each spend i.e. 1/5, 2/5, and so on. Do not allow anymore then the max number. ✅ Update createSkillPopup to handle skill point allocation display ✅ Update openSkillTreePopup to track skill points for Circuit of Ascension skills ✅ Define variables to track all skill points allocations ✅ Update skill popup creation to handle skill point allocation ✅ Update Circuit of Ascension skill tree node display ✅ Update Echoes of Ancestry skill tree node display ✅ Update Forge of Possibilities skill tree node display ✅ Update Prism of Potential skill tree node display ✅ Update Nexus of Chaos skill tree node display ✅ Update Symphony of Shadows skill tree node display ✅ Update Data Spike popup to handle the diminishing returns description ⏪Revert to here VA The number beneath the skill node does not update dynamically from 0/5 to 1/5 when I spend a skill point into the skill node like it should ✅ Fix spendButton click handler to properly update skill point allocation display ✅ Modify createSkillPopup to add skill points allocation text for skill nodes ⏪Revert to here VA Please fix the bug: 'Uncaught TypeError: Cannot read properties of undefined (reading 'includes')' in or related to this line: 'return child instanceof Text2 && child.text.includes('/') && child.y > skillPopup.height / 2;' Line Number: 1460 ✅ Fix TypeError by ensuring child has text property before calling includes ⏪Revert to here VA Hmmm, I now have a number updating correctly when points are spent, inside of the skill description. This is fine, except that the numbers under each skill node do not change also. Maybe attach those numbers to eachother to both update? ✅ Update spendButton click handler to update both popup and skill tree node allocation displays ✅ Add function to update all skill node point displays in skill tree screens ✅ Modify spendButton click handler to update skill node point displays in the skill tree screens ⏪Revert to here VA Great, the numbers are both updating, but the number under the skill tree node is not updating until I leave the current screen by pressing the back button and then re-open it. ✅ Fix skill node points display not updating immediately ⏪Revert to here VA the numbers are both updating, but the number under the skill tree node is not updating until I leave the current screen by pressing the back button and then re-open it. Is there anyway to get that to work? ✅ Fix skill node point display not updating immediately by checking for Text2 elements in createSkillPopup function ✅ Fix updateSkillNodePointDisplays function to properly identify and update skill point displays ⏪Revert to here VA Make this the case for this skill: Here is the template to follow: Reflex Accelerator: Boosts movement speed—+10 %, +8 %, +6 %, +4 %, then +2 % for a total of +30 % at rank 5. Make sure the description updates each time a skill point is spent to represent this. For example: Boosts movement speed by 10% for the first point. Then, after a skill point is placed into it and the allocation text changes to 1/5, the description changes to "Boosts movement speed by 8% (Currently 10%)". This is how Each point should increase it. ✅ Fix Reflex Accelerator skill description to update dynamically with diminishing returns ✅ Update spendButton click handler to handle Reflex Accelerator's specific description update ⏪Revert to here VA Make this the case for this skill: Overclock Core: Increases attack speed and energy regeneration—+2.5 % & +2.0 %, +2.0 % & +1.6 %, +1.5 % & +1.2 %, +1.0 % & +0.8 %, then +0.5 % & +0.4 % for a total of +7.5 % attack speed and +6.0 % energy regen at rank 5. Make sure the description updates to reflect both stats. For example: "Increases attack speed by 2.5% and energy regeneration by 2.0%." Then: "Increases attack speed by 2.0% and energy regeneration by 1.6% (Currently 2.5% / 2.0%)." ✅ Implement Overclock Core skill with proper description and updating mechanism ⏪Revert to here VA The overclock core deacription is not updating dynamically like I just asked you to implement ✅ Fix Overclock Core skill description to dynamically update with diminishing returns ✅ Update spendButton click handler to properly handle Overclock Core's dynamic description ⏪Revert to here VA Make this the case for this skill: Neural Hijack: Converts an enemy to fight for you and reduces cooldown with each rank—3 s/30 s, 4 s/27 s, 5 s/24 s, 6 s/21 s, and then 8 s/ 15 s. Make sure the description updates accordingly. For example: "Temporarily converts an enemy to fight for you for 3 seconds (Cooldown: 30s)." Then: "Converts an enemy for 4 seconds (Cooldown: 27s) (Currently 3s / 30s)." ✅ Create displayNeuralHijackPopup function with diminishing returns ⏪Revert to here VA It is not updating dynamically like it should ✅ Fix Neural Hijack skill description to dynamically update with diminishing returns ✅ Fix spendButton click handler to properly update Neural Hijack description dynamically ✅ Fix createSkillPopup function to properly find and update skill point displays ⏪Revert to here VA Now make this the case: Dodge Matrix: Grants a chance to evade attacks—+10 %, +8 %, +6 %, +4 %, then +2 % for a total of +30 % at rank 5. Make sure the description updates each time. For example: "Grants a 10% chance to dodge enemy attacks." Then: "Grants an 8% chance to dodge enemy attacks (Currently 10%)." ✅ Implement Dodge Matrix skill with dynamic description updates that show diminishing returns ✅ Update spendButton click handler to properly update Dodge Matrix description ⏪Revert to here VA Power Surge: Releases an energy burst that increases in damage and radius—×1.0 & 2.0 units, ×1.25 & 2.5 units, ×1.5 & 3.0 units, then ×1.75 & 3.5 units. Make sure the description updates each time. For example: "Releases an energy burst dealing 100% magic damage in a 2.0-unit radius." Then: "Deals 125% magic damage in a 2.5-unit radius (Currently 100% / 2.0)." ✅ Implement Power Surge skill with dynamic description updates for damage and radius ⏪Revert to here VA It's not updating dynamically like it should ✅ Fix Power Surge skill description to dynamically update with damage and radius values ✅ Update spendButton click handler to properly handle Power Surge skill description updates ✅ Fix createSkillPopup function to properly update skill point displays ⏪Revert to here VA Now for Overload Blast: Unleashes a massive fir explosion—300 % damage to all enemies in a 5-unit radius with a 60 s cooldown. This is a single-rank ultimate, so it doesn't scale. Description: "Unleashes a massive explosion, dealing 300% fire damage to all enemies in the room (Cooldown: 60s)." Also, change the skill point allocation max to 1 for this skill. ✅ Add Overload Blast skill as a single-rank ultimate with correct description and max points ✅ Add Overload Blast to skillPointAllocations with max 1 point ⏪Revert to here VA The overload blast skill node skill allocation text underneath it should say 0/1, but it currently says 0/5. ✅ Fix Overload Blast skill node allocation text to display 0/1 ⏪Revert to here VA What is the text underneath each skill node called? The text underneath each skill node is called **pointsText**. This text displays the current and maximum skill point allocation for each skill node, formatted as "current/max". VA Great, for overload blast skill node **pointsText** change the max from 0/5 to 0/1 ✅ Fix Overload Blast skill node pointsText to display 0/1 ⏪Revert to here VA What is the points text in the description called? The text underneath each skill node that displays the current and maximum skill point allocation is called **pointsText**. This text is formatted as "current/max" to indicate how many points have been allocated to that particular skill node out of the maximum possible points. VA What about in the skill description popup? In the skill description popup, the text that displays the current and maximum skill point allocation is referred to as **allocationText**. This text is formatted as "current/max" to indicate how many points have been allocated to that particular skill out of the maximum possible points. VA Great, what line of code is pointstext for overload blast? The `pointsText` for the Overload Blast skill node is defined in the section of the code where skill nodes are created and displayed within the skill tree popups. Specifically, it is created when iterating over the skill nodes to display their current and maximum skill point allocations. The `pointsText` is set to display "0/1" for the Overload Blast skill node, reflecting its maximum allocation of 1 point. This is done by accessing the `skillPointAllocations` object to retrieve the current points and maximum points for each skill node, and then creating a `Text2` object with this information to display beneath the skill node icon. VA Please save this source code ✅ Code successfully saved ⏪Revert to here VA It looks like the Overload Blast skill node is duplicated with the "Ultimate Skill" node within the code and it is causing issues. Can you change those instances to Overload Blast only please ✅ Remove 'Ultimate Skill' node and use 'Overload Blast' everywhere in skill tree and allocation ✅ Replace 'Ultimate Skill' node with 'Overload Blast' in Circuit of Ascension skillNodes ✅ Update skillNode.down handler to use 'Overload Blast' instead of 'Ultimate Skill' ⏪Revert to here VA Explain all of the Echoes of ancestry skills to me Absolutely! Here’s a clear, human-friendly explanation of all the skills in the **Echoes of Ancestry** skill tree, based on your game’s design: --- ## Echoes of Ancestry Skill Explanations ### 1. **Ancient Wisdom** *Tap into the deep knowledge of your forebears. Increases your critical hit chance by 5% per point spent, up to a maximum. This skill makes your attacks more likely to deal extra damage.* --- ### 2. **Elemental Mastery** *Channel elemental forces passed down through the ages. Each point increases the elemental damage of your attacks, making both your sword and bow strikes more powerful with fire, ice, or other magical effects.* --- ### 3. **Warrior Spirit** *Draw upon the martial prowess of ancestral warriors. Each point increases your melee attack speed by 10%, allowing you to swing your sword faster and deal more damage in close combat.* --- ### 4. **Beast Tamer** *Ancestral bonds with nature grant you the ability to briefly call forth a spectral beast. At higher ranks, you can summon a spirit animal that attacks nearby enemies for a short time before vanishing.* --- ### 5. **Herbal Remedies** *Old herbal secrets provide gentle healing over time. With each point, you gradually restore a portion of your health while in battle, helping you survive longer.* --- ### 6. **Spirit Guidance** *A guiding ancestral spirit offers insight in combat. Each point reduces incoming damage by 5%, making you more resilient against enemy attacks.* --- ### 7. **Legendary Ancestor** *Invoke the essence of a mighty champion from your bloodline. For a short duration, you gain a significant boost to both your damage and defense, turning the tide of battle in your favor.* --- **In summary:** The Echoes of Ancestry tree is all about drawing on the power, wisdom, and protection of your ancestors. It offers a mix of offensive boosts (like critical chance and elemental damage), defensive perks (damage reduction and healing), and unique abilities (summoning a beast or temporarily becoming a legendary hero). Let me know if you’d like a breakdown of another skill tree or more details on any specific skill! VA Ancient Wisdom: Increases elemental critical hit chance—+5 %, +4 %, +3 %, +2 %, then +1 % for a total of +15 % at rank 5. Make sure the description updates each time a skill point is spent to represent this. For example: "Increases elemental critical hit chance by 5%." Then: "Increases elemental critical hit chance by 4% (Currently 5%)." Elemental Mastery: Increases elemental damage—+10 %, +8 %, +6 %, +4 %, then +2 % for a total of +30 % at rank 5. Make sure the description updates each time. For example: "Increases elemental damage by 10%." Then: "Increases elemental damage by 8% (Currently 10%)." Warrior Spirit: Increases melee attack speed—+10 %, +8 %, +6 %, +4 %, then +2 % for a total of +30 % at rank 5. Make sure the description updates each time. For example: "Increases melee attack speed by 10%." Then: "Increases melee attack speed by 8% (Currently 10%)." Beast Tamer: Summons a spirit animal ally—2 s duration / 20 s cooldown, 3 s / 18 s, then 4 s / 16 s. Make sure the description updates with duration and cooldown scaling. For example: "Summons a spirit beast for 2 seconds (Cooldown: 20s)." Then: "Summons a spirit beast for 3 seconds (Cooldown: 18s) (Currently 2s / 20s)." Herbal Remedies: Grants passive healing over time—0.5 % max HP/sec, 0.6 %, 0.7 %, 0.8 %, then 1.0 % at rank 5. Make sure the description updates each time. For example: "Heals 0.5% of max HP per second during combat." Then: "Heals 0.6% per second (Currently 0.5%)." Spirit Guidance: Reduces incoming damage—5 %, 4 %, 3 %, 2 %, then 1 % for a total of 15 % at rank 5. Make sure the description updates each time. For example: "Reduces incoming damage by 5%." Then: "Reduces incoming damage by 4% (Currently 5%)." Legendary Ancestor: Temporarily boosts damage and defense—+100 % damage & +50 % defense for 6 s (Cooldown: 60 s). This is a single-rank ultimate. Description: "Temporarily increases your damage by 100% and defense by 50% for 6 seconds (Cooldown: 60s)." FAILURE: Unexpected error VA Now do the same as we did with circuit of ascension skill tree with the Echoes of Ancestry skill tree skills. Here is the template for them all: Ancient Wisdom: Increases elemental critical hit chance—+5 %, +4 %, +3 %, +2 %, then +1 % for a total of +15 % at rank 5. Make sure the description updates each time a skill point is spent to represent this. For example: "Increases elemental critical hit chance by 5%." Then: "Increases elemental critical hit chance by 4% (Currently 5%)." Elemental Mastery: Increases elemental damage—+10 %, +8 %, +6 %, +4 %, then +2 % for a total of +30 % at rank 5. Make sure the description updates each time. For example: "Increases elemental damage by 10%." Then: "Increases elemental damage by 8% (Currently 10%)." Warrior Spirit: Increases melee attack speed—+10 %, +8 %, +6 %, +4 %, then +2 % for a total of +30 % at rank 5. Make sure the description updates each time. For example: "Increases melee attack speed by 10%." Then: "Increases melee attack speed by 8% (Currently 10%)." Beast Tamer: Summons a spirit animal ally—2 s duration / 20 s cooldown, 3 s / 18 s, then 4 s / 16 s. Make sure the description updates with duration and cooldown scaling. For example: "Summons a spirit beast for 2 seconds (Cooldown: 20s)." Then: "Summons a spirit beast for 3 seconds (Cooldown: 18s) (Currently 2s / 20s)." Herbal Remedies: Grants passive healing over time—0.5 % max HP/sec, 0.6 %, 0.7 %, 0.8 %, then 1.0 % at rank 5. Make sure the description updates each time. For example: "Heals 0.5% of max HP per second during combat." Then: "Heals 0.6% per second (Currently 0.5%)." Spirit Guidance: Reduces incoming damage—5 %, 4 %, 3 %, 2 %, then 1 % for a total of 15 % at rank 5. Make sure the description updates each time. For example: "Reduces incoming damage by 5%." Then: "Reduces incoming damage by 4% (Currently 5%)." Legendary Ancestor: Temporarily boosts damage and defense—+100 % damage & +50 % defense for 6 s (Cooldown: 60 s). This is a single-rank ultimate. Description: "Temporarily increases your damage by 100% and defense by 50% for 6 seconds (Cooldown: 60s)." ✅ Implement dynamic diminishing returns popup for Ancient Wisdom skill ✅ Implement dynamic diminishing returns popup for Elemental Mastery skill ✅ Implement dynamic diminishing returns popup for Warrior Spirit skill ✅ Implement dynamic duration/cooldown popup for Beast Tamer skill ✅ Implement dynamic diminishing returns popup for Herbal Remedies skill ✅ Implement dynamic diminishing returns popup for Spirit Guidance skill ✅ Implement static ultimate popup for Legendary Ancestor skill ⏪Revert to here VA The descriptions are not changing dynamically when skill points are added to the skill node. ✅ Make skill popup descriptions for Echoes of Ancestry update dynamically after spending points ⏪Revert to here VA For legendary ancester set the max points allocatable to 1 ✅ Set Legendary Ancestor max points allocatable to 1 ⏪Revert to here VA What are the skills and their descriptions in the forge of possibilities skill tree? Of course! Here are the skills and their descriptions for the **Forge of Possibilities** skill tree, written in clear, human-friendly language: --- ## Forge of Possibilities — Skill Explanations ### 1. **Adaptive Construction** *Learn to rapidly modify your weapons on the fly. Temporarily increase your sword or bow damage by 10% depending on which you wield most recently.* --- ### 2. **Elemental Infusion** *Forge special arrowheads or blade edges with elemental cores. Each successful hit has a small chance to apply a random elemental effect to enemies.* --- ### 3. **Modular Enhancement** *Attach versatile modules to your equipment. Gain a slight increase in attack speed and movement speed, allowing you to reposition more effectively during combat.* --- ### 4. **Resourceful Salvage** *Disassemble enemy gear after defeats. Occasionally gain a temporary buff that slightly increases your skill point gain or reduces skill cooldowns.* --- ### 5. **Hybrid Crafting** *Blend melee and ranged weapon traits. Your arrows gain a minor cleave effect at short range, and your sword attacks have a small chance to unleash a short-ranged projectile.* --- ### 6. **Runic Slots** *Add runic slots to your weapons. When you acquire elemental shards from fallen foes, socketing them grants short-term buffs like increased critical chance, armor, or life steal.* --- ### 7. **Masterwork Creation** *Temporarily upgrade your weapon into a masterwork artifact. Significantly boosts both melee and ranged damage, and improves your elemental infusions for a short duration, turning you into a forging prodigy on the battlefield.* --- Let me know if you’d like a breakdown of another skill tree or want to see how these skills scale with points! VA Change the Forge of possibilities skill tree skills descriptions to these and have them update dynamically when a skill point is allocated to them. Firebrand Enchantment: Imbue your weapon with searing flame—each rank adds +5 % fire damage and +2 % chance to ignite on hit. Make sure the description updates each time a skill point is spent to represent this. For example: “Imbue your weapon with searing flame, adding 5% fire damage and 2% chance to ignite on hit.” Then: “Imbue your weapon with searing flame, adding 10% fire damage and 4% chance to ignite on hit (Currently 5% fire damage / 2% ignite).” Elemental Tempo: Every swing or shot has a passive chance to inflict a random elemental effect—proc chance scales +10 %, +8 %, +6 %, +4 %, then +2 % (max 30 % at rank 5). Make sure the description updates each time a skill point is spent. For example: “Every attack has a 10% chance to inflict a random elemental effect.” Then: “Every attack has an 18% chance to inflict a random elemental effect (Currently 10%).” Mechanical Reflex: Bolted springs release on hit, giving you a chance to instantly ready your weapon for an extra strike—proc chance scales +3 %, +6 %, +9 %, +12 %, then +15 % (max 15 % at rank 5). Make sure the description updates each time. For example: “Each hit has a 3% chance to instantly ready your weapon for an extra strike.” Then: “Each hit has a 6% chance to instantly ready your weapon for an extra strike (Currently 3%).” Resourceful Salvage: Disassemble enemy gear on kill for a small chance to siphon extra skill‐point gain—proc chance scales 2 %→6 % and buff strength/duration scales +10 % for 5 s up to +20 % for 15 s. Make sure the description updates each time. For example: “Killing an enemy has a 2% chance to grant +10% skill‐point gain for 5 seconds.” Then: “Killing an enemy has a 3% chance to grant +12% skill‐point gain for 7 seconds (Currently 2% / +10% / 5s).” Hybrid Crafting (1/1): 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. Make sure this description appears correctly when the point is spent. Gem Harvester: Kills can drop volatile runic gems that grant a random buff—buff potency and duration scale from +5 % for 3 s up to +15 % for 8 s, and drop chance scales 5 %→20 %. Make sure the description updates each time a skill point is spent. For example: “Killing an enemy has a 5% chance to drop a runic gem granting a random +5% buff for 3 seconds.” Then: “Killing an enemy has an 8% chance to drop a runic gem granting a random +7% buff for 4 seconds (Currently 5% / +5% / 3s).” Masterwork Creation: Temporarily transform your weapon into a masterwork artifact—damage bonus, arrow‐cleave count, and shockwave radius all increase with each of 3 ranks. Make sure the description updates each time a point is spent. For example (Rank 1): “Transforms your weapon for 6s, granting +30% damage, arrows cleave 1 extra target, and melee triggers a 1‐unit shockwave (Cooldown: 60s).” Then (Rank 2): “Transforms your weapon for 6s, granting +60% damage, arrows cleave 2 extra targets, and melee triggers a 2‐unit shockwave (Currently +30% / 1 cleave / 1‐unit shockwave).”
User prompt
For legendary ancester set the max points allocatable to 1
User prompt
The descriptions are not changing dynamically when skill points are added to the skill node.
User prompt
Now do the same as we did with circuit of ascension skill tree with the Echoes of Ancestry skill tree skills. Here is the template for them all: Ancient Wisdom: Increases elemental critical hit chance—+5 %, +4 %, +3 %, +2 %, then +1 % for a total of +15 % at rank 5. Make sure the description updates each time a skill point is spent to represent this. For example: "Increases elemental critical hit chance by 5%." Then: "Increases elemental critical hit chance by 4% (Currently 5%)." Elemental Mastery: Increases elemental damage—+10 %, +8 %, +6 %, +4 %, then +2 % for a total of +30 % at rank 5. Make sure the description updates each time. For example: "Increases elemental damage by 10%." Then: "Increases elemental damage by 8% (Currently 10%)." Warrior Spirit: Increases melee attack speed—+10 %, +8 %, +6 %, +4 %, then +2 % for a total of +30 % at rank 5. Make sure the description updates each time. For example: "Increases melee attack speed by 10%." Then: "Increases melee attack speed by 8% (Currently 10%)." Beast Tamer: Summons a spirit animal ally—2 s duration / 20 s cooldown, 3 s / 18 s, then 4 s / 16 s. Make sure the description updates with duration and cooldown scaling. For example: "Summons a spirit beast for 2 seconds (Cooldown: 20s)." Then: "Summons a spirit beast for 3 seconds (Cooldown: 18s) (Currently 2s / 20s)." Herbal Remedies: Grants passive healing over time—0.5 % max HP/sec, 0.6 %, 0.7 %, 0.8 %, then 1.0 % at rank 5. Make sure the description updates each time. For example: "Heals 0.5% of max HP per second during combat." Then: "Heals 0.6% per second (Currently 0.5%)." Spirit Guidance: Reduces incoming damage—5 %, 4 %, 3 %, 2 %, then 1 % for a total of 15 % at rank 5. Make sure the description updates each time. For example: "Reduces incoming damage by 5%." Then: "Reduces incoming damage by 4% (Currently 5%)." Legendary Ancestor: Temporarily boosts damage and defense—+100 % damage & +50 % defense for 6 s (Cooldown: 60 s). This is a single-rank ultimate. Description: "Temporarily increases your damage by 100% and defense by 50% for 6 seconds (Cooldown: 60s)."
User prompt
It looks like the Overload Blast skill node is duplicated with the "Ultimate Skill" node within the code and it is causing issues. Can you change those instances to Overload Blast only please
User prompt
For some reason, Overload Blast is also Ultimate skill in the code.
Code edit (1 edits merged)
Please save this source code
User prompt
Great, for overload blast skill node **pointsText** change the max from 0/5 to 0/1
/**** * 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 } 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 self.health = 100; self.maxHealth = 100; self.energy = 50; self.maxEnergy = 50; self.energyRegen = 1.0; self.armor = 5; self.magicResist = 5; self.movementSpeed = 3.0; self.attackSpeed = 1.0; self.damageBonus = 0.0; self.critChance = 0.05; self.critDamage = 1.5; self.dodgeChance = 0.0; self.lifeSteal = 0.0; self.thorns = 0; self.skillCooldownReduction = 0.0; self.fireResist = 0.0; self.iceResist = 0.0; self.lightningResist = 0.0; self.poisonResist = 0.0; self.shadowResist = 0.0; self.chaosResist = 0.0; // 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; // Recalculate Stats self.recalculateStats = function () { console.log("Recalculating Hero Stats (Not Implemented Yet)"); }; // 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) // Check if Stunned/Frozen var canAct = true; self.activeStatusEffects.forEach(function (effect) { if (effect.type === StatusEffectType.STUN || effect.type === StatusEffectType.FREEZE) { canAct = false; } }); // Movement var moveDirection = { x: 0, y: 0 }; if (canAct && self.joystick) { moveDirection = self.joystick.getDirection(); } var currentMoveSpeed = self.getStat('movementSpeed'); var nextX = self.x + moveDirection.x * currentMoveSpeed; var nextY = self.y + moveDirection.y * currentMoveSpeed; // 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 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; } if (self.weaponType === 1) { self.swordGraphics.rotation = 0; } else { self.bowGraphics.rotation = 0; } // Attack Logic 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 = []; for (var i = currentRoom.enemies.length - 1; i >= 0; i--) { var enemy = currentRoom.enemies[i]; if (!enemy || !enemy.active || hitEnemiesThisCheck.includes(enemy)) { continue; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distSqr = dx * dx + dy * dy; var rangeSqr = self.swordRange * self.swordRange; if (distSqr < rangeSqr) { var angleToEnemy = Math.atan2(dy, dx); var angleDifference = Math.atan2(Math.sin(angleToEnemy - attackAngle), Math.cos(angleToEnemy - attackAngle)); if (Math.abs(angleDifference) < Math.PI / 3) { if (enemy.takeDamage) { enemy.takeDamage(swordDamageInfo.damage, DamageType.PHYSICAL, self); hitEnemiesThisCheck.push(enemy); var lifeStealAmount = swordDamageInfo.damage * self.getStat('lifeSteal'); if (lifeStealAmount > 0) { self.takeDamage(lifeStealAmount, DamageType.HEALING); } } } } } }; 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 () { if (!self.active || !hero) { return; } BaseEntity.prototype.update.call(self); // Runs BaseEntity update (modifiers, status effects) if (self.damageCooldown > 0) { self.damageCooldown--; } var currentMoveSpeed = self.getStat('movementSpeed'); var offsetX = (Math.random() - 0.5) * 20; var offsetY = (Math.random() - 0.5) * 20; if (hero) { self.moveTowards(hero.x + offsetX, hero.y + offsetY, currentMoveSpeed); } self.handleWallCollision(); }; // 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 () { if (!self.active || !hero) { return; } BaseEntity.prototype.update.call(self); // Run base updates (status effects, modifiers) if (self.damageCooldown > 0) { self.damageCooldown--; } // Explicitly decrement cooldown self.updateAnimation(); var targetX = hero.x; var targetY = hero.y; var distance = Math.sqrt(Math.pow(targetX - self.x, 2) + Math.pow(targetY - self.y, 2)); var currentMoveSpeed = self.getStat('movementSpeed'); if (distance > self.minDistance) { self.moveTowards(targetX, targetY, currentMoveSpeed); } else { if (self.damageCooldown <= 0) { self.damageCooldown = self.damageCooldownTime; // Reset CD FIRST var outgoingDamageInfo = self.calculateOutgoingDamage(self.damageAmount, DamageType.PHYSICAL); if (hero.takeDamage) { hero.takeDamage(outgoingDamageInfo.damage, DamageType.PHYSICAL, self); } } self.rotation = Math.atan2(targetY - self.y, targetX - self.x); } self.handleWallCollision(); }; 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 () { if (!self.active || !hero) { return; } BaseEntity.prototype.update.call(self); // Run base updates (status effects, modifiers) self.updateAnimation(); var targetX = hero.x; var targetY = hero.y; var dx = targetX - self.x; var dy = targetY - self.y; var distance = Math.sqrt(dx * dx + dy * dy); var currentMoveSpeed = self.getStat('movementSpeed'); if (distance < self.desiredDistance - 10) { self.moveTowards(targetX, targetY, -currentMoveSpeed); } else if (distance > self.desiredDistance + 10) { self.moveTowards(targetX, targetY, currentMoveSpeed); } else { self.rotation = Math.atan2(dy, dx); } self.handleWallCollision(); if (self.projectileCooldown <= 0) { self.shootProjectile(targetX, targetY); var attackSpeedMult = self.getStat('attackSpeed'); self.projectileCooldown = self.projectileCooldownTime / attackSpeedMult; } else { self.projectileCooldown--; } }; 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) --- var Enemy = BaseEnemy.expand(function () { var self = BaseEnemy.call(this); // 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; self.setupAnimation(['enemy_run1', 'enemy_run2', 'enemy_run3', 'enemy_run4']); // Update self.update = function () { if (!self.active || !hero) { return; } BaseEntity.prototype.update.call(self); // Run base updates (status effects, modifiers) if (self.damageCooldown > 0) { self.damageCooldown--; } // Explicitly decrement cooldown self.updateAnimation(); var targetX = hero.x + (Math.random() - 0.5) * 20; var targetY = hero.y + (Math.random() - 0.5) * 20; var currentMoveSpeed = self.getStat('movementSpeed'); var distance = self.moveTowards(targetX, targetY, currentMoveSpeed); var minDistance = 70; if (distance <= minDistance) { if (self.damageCooldown <= 0) { self.damageCooldown = self.damageCooldownTime; // Reset CD FIRST var outgoingDamageInfo = self.calculateOutgoingDamage(self.damageAmount, DamageType.PHYSICAL); if (hero.takeDamage) { hero.takeDamage(outgoingDamageInfo.damage, DamageType.PHYSICAL, self); } } // Pushback logic... var pushDx = self.x - targetX; var pushDy = self.y - targetY; 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; } } self.handleWallCollision(); }; 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); } if (self.parent) { self.parent.removeChild(self); } } }; 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 ****/ // Define Damage Types /**** * Global Constants ****/ // Define Damage Types /**** * Global Constants ****/ // LK.init.image('hero_topdown', {width:100, height:100, id:'YOUR_NEW_HERO_ASSET_ID'}) // Placeholder for new hero asset // KEEPING THIS FOR NOW - REPLACE WITH 'hero_topdown' LATER // -- Symphony of Shadows Icons -- // -- Nexus of Chaos Icons -- // -- Prism of Potential Icons -- // -- Forge of Possibilities Icons -- // -- Echoes of Ancestry Icons -- // -- Circuit of Ascension Icons -- // -- Symphony of Shadows Icons -- // -- Nexus of Chaos Icons -- // -- Prism of Potential Icons -- // -- Forge of Possibilities Icons -- // -- Echoes of Ancestry Icons -- // -- Circuit of Ascension Icons -- // Made it grey // Add containers to the game stage // Adjusted default size /**** * Game Configuration & Data ****/ // --- BaseEntity Prototype Methods (CORRECTED PLACEMENT) --- 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 CHARM: 'charm', DISORIENT: 'disorient', VULNERABLE: 'vulnerable' // Add more as needed }; // --- BaseEntity Prototype Methods --- // Update (Handles Modifiers and Status Effects) BaseEntity.prototype.update = function () { if (!this.active) { return; } this.updateModifiers(); // Update stat buff/debuff durations using 'this' this.updateStatusEffects(); // Update status effect durations & apply DoTs using 'this' }; // 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 if (statName === 'movementSpeed' || statName === 'attackSpeed') { if (effect.type === StatusEffectType.CHILL) { var _effect$magnitude; speedMultiplier *= 1.0 - (((_effect$magnitude = effect.magnitude) === null || _effect$magnitude === void 0 ? void 0 : _effect$magnitude.speed) || 0.3); } // Example: 30% slow if (effect.type === StatusEffectType.SLOW) { var _effect$magnitude2; speedMultiplier *= 1.0 - (((_effect$magnitude2 = effect.magnitude) === null || _effect$magnitude2 === void 0 ? void 0 : _effect$magnitude2.speed) || 0.5); } // Example: 50% slow } // Action prevention checks if (effect.type === StatusEffectType.STUN || effect.type === StatusEffectType.FREEZE) { canActMultiplier = 0; } }); finalValue *= speedMultiplier; // If checking speed/attack speed, also apply stun/freeze 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) { return; } // Use 'this' var effectType = effectData.type; var source = effectData.source; var durationFrames = Math.round((effectData.duration || 1) * 60); var magnitude = effectData.magnitude; var tickInterval = Math.round((effectData.tickInterval || 1) * 60); var stackingType = effectData.stackingType || 'Duration'; var existingEffect = null; var existingEffectIndex = -1; for (var i = 0; i < this.activeStatusEffects.length; i++) { // Use 'this' if (this.activeStatusEffects[i].type === effectType && this.activeStatusEffects[i].source === source) { existingEffect = this.activeStatusEffects[i]; existingEffectIndex = i; break; } } if (existingEffect) { // Apply stacking rules... (logic unchanged, just uses existingEffect variable) if (stackingType === 'Duration') { existingEffect.duration = Math.max(existingEffect.duration, durationFrames); existingEffect.magnitude = magnitude; } else if (stackingType === 'Intensity') { existingEffect.duration = Math.max(existingEffect.duration, durationFrames); existingEffect.magnitude = magnitude; /* Add proper intensity stacking if needed */ } else { return; } // 'None' } else { var newEffect = { id: Date.now().toString(36) + Math.random().toString(36).substr(2), type: effectType, source: source, duration: durationFrames, magnitude: magnitude, ticksRemaining: 0, tickInterval: tickInterval, stackingType: stackingType, data: {} }; this.activeStatusEffects.push(newEffect); // Use 'this' // --- TODO: Add visual/audio cue start --- } }; BaseEntity.prototype.removeStatusEffect = function (effectId) { var index = this.activeStatusEffects.findIndex(function (eff) { return eff.id === effectId; }); if (index > -1) { // --- TODO: Add visual/audio cue end --- this.activeStatusEffects.splice(index, 1); // Use 'this' } }; BaseEntity.prototype.updateStatusEffects = function () { for (var i = this.activeStatusEffects.length - 1; i >= 0; i--) { var effect = this.activeStatusEffects[i]; effect.duration--; var isDoT = effect.type === StatusEffectType.IGNITE || effect.type === StatusEffectType.POISON || effect.type === StatusEffectType.BLEED; if (isDoT) { effect.ticksRemaining--; if (effect.ticksRemaining <= 0) { var dotDamage = effect.magnitude || 0; var damageType; if (effect.type === StatusEffectType.IGNITE) { damageType = DamageType.FIRE; } else if (effect.type === StatusEffectType.POISON) { damageType = DamageType.POISON; } else if (effect.type === StatusEffectType.BLEED) { damageType = DamageType.PHYSICAL; } else { damageType = DamageType.TRUE; } this.takeDamage(dotDamage, damageType, effect.source, true); // Use 'this', bypass dodge effect.ticksRemaining = effect.tickInterval; } } if (effect.duration <= 0) { this.removeStatusEffect(effect.id); } // Use 'this' } }; // 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; } }); 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' var critChance = this.getStat('critChance'); // Use 'this' if (Math.random() < critChance) { isCrit = true; var critMultiplier = this.getStat('critDamage'); // Use 'this' 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; } // Use 'this' var healingMultiplier = 1.0; if (damageType === DamageType.HEALING) { this.activeStatusEffects.forEach(function (effect) { // Use 'this' if (effect.type === StatusEffectType.POISON) { var _effect$magnitude3; healingMultiplier *= 1.0 - (((_effect$magnitude3 = effect.magnitude) === null || _effect$magnitude3 === void 0 ? void 0 : _effect$magnitude3.healReduction) || 0.5); } }); amount *= healingMultiplier; if (amount <= 0) { return; } } 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); // Use 'this' this.health -= effectiveAmount; // Use 'this' // --- TODOs unchanged --- 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 }; // 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: { damageMultipliers: [1.0, 1.25, 1.5, 1.75, 2.0], radiusSizes: [2.0, 2.5, 3.0, 3.5, 4.0] }, 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.'; } // Corrected max description based on arrays } }, 'Overload Blast': { points: 0, max: 1, tree: 'The Circuit of Ascension', 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', 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': { // NOTE: Original code had max 5, description implies max 3. Assuming max 3. points: 0, max: 3, tree: 'The Forge of Possibilities', calcData: { dmgBonuses: [30, 60, 90], cleaveCounts: [1, 2, 3], shockwaveRadii: [1, 2, 3] }, 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: { // Values *at* each rank (1 to 5) based on user description potencyValues: [12, 10, 8, 6, 4], // % Potency durationValues: [6, 8, 10, 12, 15] // seconds Duration }, 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 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(); // Holds main skill tree, specific trees, and detail popups // 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; 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 ****/ // --- 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) { heroSkillPoints--; skillData.points++; // Update points in central definition // 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(); } else { // Add visual feedback? (e.g., shake button, change color briefly) } }; 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; } 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); enemyDebugText = new Text2('Enemy CD: ---', { size: 40, fill: 0xFFFF00, // Yellow color for debug fontWeight: "bold", wordWrap: false }); enemyDebugText.x = roomWidth - 250; // Position top-right corner enemyDebugText.y = 120; // Below the main stats bar enemyDebugText.anchor.set(1, 0); // Anchor top-right 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 }; // Switch Weapon Zone/Button var switchWeaponZone = LK.getAsset('switchWeaponZone', { anchorX: 0.5, anchorY: 0.5 }); switchWeaponZone.x = roomWidth - 250; // Position lower right switchWeaponZone.y = roomHeight - 500; uiContainer.addChild(switchWeaponZone); switchWeaponZone.interactive = true; // Make the zone itself clickable switchWeaponZone.down = function (x, y, obj) { if (isPopupActive || isSkillDetailPopupActive || !hero) { return; } hero.switchWeapon(); }; // --- 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]; if (!arrow) { arrows.splice(j, 1); continue; } arrow.update(); // Move arrow // Check collision with enemies AFTER moving for (var i = currentRoom.enemies.length - 1; i >= 0; i--) { var enemy = currentRoom.enemies[i]; if (!enemy || !enemy.active) { continue; } // Simple distance check (improve with AABB or Circle if needed) var hitDx = enemy.x - arrow.x; var hitDy = enemy.y - arrow.y; var hitDistSqr = hitDx * hitDx + hitDy * hitDy; var enemyRadius = (enemy.width || 50) / 2; var arrowRadius = (arrow.width || 10) / 2; var collisionDist = enemyRadius + arrowRadius; if (hitDistSqr < collisionDist * collisionDist) { // console.log("Arrow Hit Enemy! Damage:", arrow.damage, "Type:", arrow.damageType); // Debug if (enemy.takeDamage) { enemy.takeDamage(arrow.damage, arrow.damageType, arrow.source); // Pass type and source } // Apply LifeSteal on hit (if the arrow came from the hero) if (arrow.source === hero) { var lifeStealAmount = arrow.damage * hero.getStat('lifeSteal'); if (lifeStealAmount > 0) { hero.takeDamage(lifeStealAmount, DamageType.HEALING); } } // --- TODO: Add Arrow Hit VFX/SFX --- // --- TODO: Add arrow on-hit proc checks (e.g., for Elemental Infusion, Chaotic Spark from Hero) --- arrow.destroy(); // Destroys arrow and removes from array break; // Arrow hits one enemy and is destroyed } } } // End arrow update loop // 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(); } } } // 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
@@ -119,359 +119,144 @@
isSkillDetailPopupActive = false; // Ensure detail flag is off
};
return self;
});
-// --- Base Entity Class (NEW - Parent for Hero and BaseEnemy) ---
// 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; // Optional resource
+ self.energy = 50;
self.maxEnergy = 50;
- self.energyRegen = 0.5; // Per second? Per frame? TBD
- self.armor = 10; // Reduces Physical Damage
- self.magicResist = 5; // Reduces non-physical Damage
- self.movementSpeed = 3; // Base speed multiplier
- self.attackSpeed = 1.0; // Multiplier (1.0 = 100%)
- self.damageBonus = 0; // Percentage bonus (e.g., 0.1 for +10%)
- self.critChance = 0.05; // 5% base crit chance
- self.critDamage = 1.5; // 150% damage on crit
- self.dodgeChance = 0.0; // 0% base dodge
+ 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; // Flat damage reflected
- self.skillCooldownReduction = 0.0; // Percentage (0.1 for 10%)
- // --- Resistances (as percentages, 0.1 = 10%) ---
+ 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;
- // --- Modifiers (Buffs/Debuffs) ---
- self.activeModifiers = []; // Stores { stat, value, duration, isPercentage, source }
- // --- Stat Calculation Helpers ---
- self.getStat = function (statName) {
- var baseValue = self[statName] || 0;
- var additiveBonus = 0;
- var multiplicativeBonus = 1.0;
- // Apply active modifiers
- self.activeModifiers.forEach(function (mod) {
- if (mod.stat === statName) {
- if (mod.isPercentage) {
- multiplicativeBonus += mod.value;
- } else {
- additiveBonus += mod.value;
- }
- }
- });
- // Calculate final value: (Base + Flat Bonuses) * Multiplicative Bonuses
- var finalValue = (baseValue + additiveBonus) * multiplicativeBonus;
- // --- Clamping specific stats ---
- if (statName === 'critChance' || statName === 'dodgeChance' || statName === 'skillCooldownReduction' || statName.includes('Resist')) {
- finalValue = Math.max(0, Math.min(finalValue, 1.0)); // Clamp percentages between 0% and 100% (or higher if needed for resists?)
- }
- if (statName === 'attackSpeed') {
- finalValue = Math.max(0.1, finalValue); // Ensure attack speed doesn't go below a certain threshold
- }
- if (statName === 'movementSpeed') {
- finalValue = Math.max(0.5, finalValue); // Ensure move speed doesn't go too low
- }
- return finalValue;
- };
- // --- Modifier Management ---
- self.applyStatModifier = function (stat, value, duration, isPercentage, source) {
- var modifier = {
- id: LK.Utils.uuid(),
- // Assuming LK engine has a UUID generator, otherwise use timestamp or simple counter
- stat: stat,
- value: value,
- duration: duration,
- // In frames (assuming 60fps)
- isPercentage: isPercentage,
- source: source || 'unknown'
- };
- self.activeModifiers.push(modifier);
- // Optionally add visual effect start here
- };
- self.removeStatModifier = function (modifierId) {
- var index = self.activeModifiers.findIndex(function (mod) {
- return mod.id === modifierId;
- });
- if (index > -1) {
- // Optionally add visual effect end here
- self.activeModifiers.splice(index, 1);
- }
- };
- self.updateModifiers = function () {
- for (var i = self.activeModifiers.length - 1; i >= 0; i--) {
- var mod = self.activeModifiers[i];
- mod.duration--;
- if (mod.duration <= 0) {
- self.removeStatModifier(mod.id);
- }
- }
- };
- // --- Damage Calculation Logic ---
- /**
- * Calculates the actual damage taken after considering defenses.
- * @param {number} amount - The raw incoming damage amount.
- * @param {string} damageType - The type of damage (from DamageType enum).
- * @param {object} attackerStats - Optional stats of the attacker (for effects like penetration).
- * @returns {number} The effective damage amount.
- */
- self.calculateDamageTaken = function (amount, damageType, attackerStats) {
- if (damageType === DamageType.TRUE) {
- return amount;
- }
- if (damageType === DamageType.HEALING) {
- return -amount; // Represent healing as negative damage
- }
- var effectiveDamage = amount;
- var resistance = 0;
- var defense = 0; // For armor/magic resist calculation
- switch (damageType) {
- case DamageType.PHYSICAL:
- // Armor calculation (example: reduces damage by a percentage based on armor value)
- // Formula needs tuning: e.g., Damage * (1 - Armor / (Armor + 100))
- defense = self.getStat('armor');
- effectiveDamage *= 1 - defense / (defense + 100); // Example formula
- resistance = self.getStat('physicalResist') || 0; // Though often just Armor
- break;
- case DamageType.FIRE:
- resistance = self.getStat('fireResist');
- defense = self.getStat('magicResist');
- break;
- case DamageType.ICE:
- resistance = self.getStat('iceResist');
- defense = self.getStat('magicResist');
- break;
- case DamageType.LIGHTNING:
- resistance = self.getStat('lightningResist');
- defense = self.getStat('magicResist');
- break;
- case DamageType.POISON:
- resistance = self.getStat('poisonResist');
- defense = self.getStat('magicResist');
- break;
- case DamageType.SHADOW:
- resistance = self.getStat('shadowResist');
- defense = self.getStat('magicResist');
- break;
- case DamageType.CHAOS:
- resistance = self.getStat('chaosResist');
- defense = self.getStat('magicResist'); // Chaos typically uses magic resist
- break;
- default:
- // Unknown type, assume magic resist applies
- resistance = 0;
- defense = self.getStat('magicResist');
- break;
- }
- // Apply Magic Resist if applicable (non-physical, non-true, non-healing)
- if (damageType !== DamageType.PHYSICAL && defense > 0) {
- effectiveDamage *= 1 - defense / (defense + 100); // Same formula as armor, adjust as needed
- }
- // Apply specific elemental/type resistance (percentage reduction)
- effectiveDamage *= 1 - resistance;
- // --- TODO: Implement attacker penetration stats if needed ---
- // Example: effectiveArmor = Math.max(0, defense - (attackerStats?.armorPenetration || 0));
- // Example: effectiveResistance = Math.max(0, resistance - (attackerStats?.resistancePenetration || 0));
- return Math.max(0, effectiveDamage); // Damage cannot be negative (unless healing)
- };
- /**
- * Calculates the final outgoing damage of an attack.
- * @param {number} baseAmount - The base damage of the weapon/skill.
- * @param {string} damageType - The type of damage.
- * @returns {object} { damage: number, isCrit: boolean }
- */
- self.calculateOutgoingDamage = function (baseAmount, damageType) {
- var finalDamage = baseAmount;
- var isCrit = false;
- // Apply general damage bonus
- finalDamage *= 1 + self.getStat('damageBonus');
- // --- TODO: Apply specific damage type bonuses (e.g., +Fire Damage %) ---
- // Example: if(damageType === DamageType.FIRE) finalDamage *= (1 + self.getStat('fireDamageBonus'));
- // Check for critical hit
- var critChance = self.getStat('critChance');
- if (Math.random() < critChance) {
- isCrit = true;
- var critMultiplier = self.getStat('critDamage');
- finalDamage *= critMultiplier;
- // console.log("CRIT!"); // Debug
- }
- return {
- damage: Math.max(0, finalDamage),
- isCrit: isCrit
- };
- };
- // --- Take Damage Method ---
- /**
- * Applies damage to the entity after calculating defenses.
- * @param {number} amount - The raw incoming damage amount.
- * @param {string} damageType - The type of damage.
- * @param {object} source - Optional: The entity that dealt the damage (for thorns, logs, etc.).
- */
- self.takeDamage = function (amount, damageType, source) {
- if (!self.active) {
- return;
- } // Don't take damage if inactive/dead
- // Dodge Check (only against damaging types)
- if (damageType !== DamageType.HEALING && damageType !== DamageType.TRUE) {
- var dodgeChance = self.getStat('dodgeChance');
- if (Math.random() < dodgeChance) {
- // console.log("DODGED!"); // Debug
- // --- TODO: Add dodge visual/sound effect ---
- return; // Take no damage
- }
- }
- var effectiveDamage = self.calculateDamageTaken(amount, damageType, source === null || source === void 0 ? void 0 : source.stats); // Pass source stats if available
- // Apply Healing (negative effective damage) or Damage
- self.health -= effectiveDamage;
- // console.log(self.constructor.name + " took " + effectiveDamage.toFixed(2) + " " + damageType + " damage. HP left: " + self.health.toFixed(2)); // Debug
- // Clamp health
- self.health = Math.min(self.health, self.getStat('maxHealth')); // Cannot heal above max health
- if (self.health <= 0) {
- self.health = 0;
- self.die(source); // Pass source to die method if needed
- }
- // --- TODO: Add Thorns logic here ---
- // if (damageType !== DamageType.HEALING && damageType !== DamageType.TRUE && source && source.takeDamage) {
- // var thornDamage = self.getStat('thorns');
- // if (thornDamage > 0) {
- // source.takeDamage(thornDamage, DamageType.PHYSICAL, self); // Thorns usually physical
- // }
- // }
- // --- TODO: Add visual feedback (hit flash, damage numbers) ---
- // Update UI (if this is the hero) - This should be handled externally ideally
- if (self === hero) {
- updateRoomDisplay();
- }
- };
- // --- Die Method (placeholder, to be implemented by children) ---
- self.die = function (killer) {
- console.log(self.constructor.name + " died.");
- self.active = false;
- // Base cleanup, specific logic in Hero/BaseEnemy
- if (self.parent) {
- self.parent.removeChild(self);
- }
- };
- // --- Base Update (Handles Modifier Timers) ---
- var originalUpdate = self.update; // Store potential existing update
- self.update = function () {
- if (!self.active) {
- return;
- }
- self.updateModifiers(); // Update durations and remove expired buffs/debuffs
- // Call original update logic if it existed
- if (originalUpdate) {
- originalUpdate.call(self);
- }
- };
+ // --- State Tracking ---
+ self.activeModifiers = []; // Stores { id, stat, value, duration, isPercentage, source }
+ self.activeStatusEffects = []; // Stores { id, type, source, duration, magnitude, ticksRemaining, tickInterval, stackingType, data }
+ self.active = true; // Entities start active
return self;
});
-// --- Hero Class (Modified - Inherits from BaseEntity) ---
+// --- BaseEntity Prototype Methods ---
+// Update (Handles Modifiers and Status Effects)
+// --- Hero Class ---
var Hero = BaseEntity.expand(function () {
var self = BaseEntity.call(this);
- // <<< Inherit from BaseEntity
- // --- Graphics Setup ---
+ // Graphics Setup
var heroGraphics = self.attachAsset('hero', {
- // CHANGE TO 'hero_topdown' when asset is ready
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; // Adjust positioning if needed
+ 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; // Adjust position as needed
+ self.bowGraphics.x = heroGraphics.width / 2;
self.bowGraphics.visible = false;
- // --- Hero Specific Stats & Properties ---
- self.health = 100; // Starting health
+ // Hero Specific Stats & Properties
+ self.health = 100;
self.maxHealth = 100;
- self.energy = 50; // Starting energy
+ self.energy = 50;
self.maxEnergy = 50;
- self.energyRegen = 1.0; // Per second? Needs timing implementation
+ self.energyRegen = 1.0;
self.armor = 5;
self.magicResist = 5;
- self.movementSpeed = 3.0; // Base speed
- self.attackSpeed = 1.0; // Base attack speed multiplier
- self.damageBonus = 0.0; // Base damage bonus %
- self.critChance = 0.05; // Base crit chance
- self.critDamage = 1.5; // Base crit damage multiplier
- self.dodgeChance = 0.0; // Base dodge chance
+ self.movementSpeed = 3.0;
+ self.attackSpeed = 1.0;
+ self.damageBonus = 0.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 (start at 0)
self.fireResist = 0.0;
self.iceResist = 0.0;
self.lightningResist = 0.0;
self.poisonResist = 0.0;
self.shadowResist = 0.0;
self.chaosResist = 0.0;
- // --- Combat State ---
+ // Combat State
self.attackCooldown = 0;
- self.isAttacking = false; // Specifically for sword swing state
- self.attackDuration = 20; // Duration of sword swing animation (frames)
+ self.isAttacking = false;
+ self.attackDuration = 20;
self.attackDurationCounter = 0;
- self.weaponType = 1; // 1: Sword, 2: Bow
- // Base weapon stats (can be overridden by equipped weapon later)
+ self.weaponType = 1;
self.swordBaseDamage = 10;
- self.swordRange = 75; // Increased range slightly
- self.swordAttackSpeedFrames = 45; // Base cooldown frames
+ self.swordRange = 75;
+ self.swordAttackSpeedFrames = 45;
self.bowBaseDamage = 8;
- self.bowAttackSpeedFrames = 30; // Base cooldown frames
- // --- References & Flags ---
+ self.bowAttackSpeedFrames = 30;
+ // References & Flags
self.joystick = null;
self.aimingJoystick = null;
self.facingDirection = 0;
self.canExitRoom = false;
- self.active = true; // Hero is active by default
- // --- Recalculate Stats (Call this after spending skill points) ---
+ // Recalculate Stats
self.recalculateStats = function () {
- // This function will be expanded later to apply skill bonuses
- // For now, it doesn't do much but is a placeholder for applying passive skills
console.log("Recalculating Hero Stats (Not Implemented Yet)");
- // Example: self.maxHealth = BASE_MAX_HEALTH + getSkillBonus('BonusHealth');
- // Example: self.critChance = BASE_CRIT_CHANCE + getSkillBonus('DataSpike');
};
- // --- takeDamage is now inherited from BaseEntity ---
- // self.takeDamage = function (amount) { ... }; // Removed, uses BaseEntity's now
- // --- Die Method (Overrides BaseEntity's) ---
+ // Die Method
self.die = function (killer) {
- if (self.active) {
- console.log("HERO DIED");
- self.active = false; // Mark as inactive
- BaseEntity.prototype.die.call(self, killer); // Call parent for basic cleanup (removes from parent)
- LK.showGameOver(); // Handle death
+ if (!self.active) {
+ return;
}
+ self.active = false;
+ isGameOver = true;
+ LK.showGameOver();
+ stopRoomMusic();
};
- // --- Update Method (Overrides BaseEntity Update) ---
+ // Update Method
self.update = function () {
- BaseEntity.prototype.update.call(self); // <<< CALL PARENT UPDATE FIRST (for modifiers)
if (!self.active) {
return;
- } // Use self.active flag
- // --- Movement ---
- var moveDirection = self.joystick ? self.joystick.getDirection() : {
+ }
+ BaseEntity.prototype.update.call(self); // Run base updates (status effects, modifiers)
+ // Check if Stunned/Frozen
+ var canAct = true;
+ self.activeStatusEffects.forEach(function (effect) {
+ if (effect.type === StatusEffectType.STUN || effect.type === StatusEffectType.FREEZE) {
+ canAct = false;
+ }
+ });
+ // Movement
+ var moveDirection = {
x: 0,
y: 0
};
- var currentMoveSpeed = self.getStat('movementSpeed'); // Get calculated speed
+ if (canAct && self.joystick) {
+ moveDirection = self.joystick.getDirection();
+ }
+ var currentMoveSpeed = self.getStat('movementSpeed');
var nextX = self.x + moveDirection.x * currentMoveSpeed;
var nextY = self.y + moveDirection.y * currentMoveSpeed;
- // --- Wall Collision & Room Transition Check ---
+ // Wall Collision & Room Transition
var onEntrance = false;
- // --- [Wall collision logic remains the same] ---
if (nextX < wallThickness) {
if (nextY >= entrances.left.yStart && nextY <= entrances.left.yEnd) {
onEntrance = true;
} else {
@@ -498,140 +283,115 @@
}
}
self.x = nextX;
self.y = nextY;
- // Handle Room Transition
if (self.canExitRoom && onEntrance) {
- // --- [Transition logic remains the same] ---
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) {
- var entrySide = '';
- if (exitSide === 'top') {
- entrySide = 'bottom';
- } else if (exitSide === 'bottom') {
- entrySide = 'top';
- } else if (exitSide === 'left') {
- entrySide = 'right';
- } else if (exitSide === 'right') {
- entrySide = 'left';
- }
transitionToNewRoom(entrySide);
return;
}
}
- // --- Rotation & Aiming ---
- // --- [Rotation logic remains the same] ---
+ // Rotation & Aiming
var isMoving = moveDirection.x !== 0 || moveDirection.y !== 0;
var isAiming = self.aimingJoystick && self.aimingJoystick.hasInput();
var aimAngle = isAiming ? self.aimingJoystick.getRotation() : null;
- if (isMoving) {
+ if (isMoving && canAct) {
self.facingDirection = Math.atan2(moveDirection.y, moveDirection.x);
}
- if (isAiming && aimAngle !== null) {
+ if (isAiming && aimAngle !== null && canAct) {
self.rotation = aimAngle;
- } else if (isMoving) {
+ } else if (isMoving && canAct) {
self.rotation = self.facingDirection;
}
- // Update weapon graphic positions relative to hero rotation
if (self.weaponType === 1) {
self.swordGraphics.rotation = 0;
} else {
self.bowGraphics.rotation = 0;
}
- // --- Attack Logic ---
- if (!isPopupActive && !isSkillDetailPopupActive) {
+ // Attack Logic
+ if (canAct && !isPopupActive && !isSkillDetailPopupActive) {
if (self.attackCooldown > 0) {
self.attackCooldown--;
}
var attackAngle = isAiming && aimAngle !== null ? aimAngle : self.rotation;
- var currentAttackSpeedMult = self.getStat('attackSpeed'); // Get calculated attack speed
- // Sword Attack
+ var currentAttackSpeedMult = self.getStat('attackSpeed');
+ // Sword
if (self.weaponType === 1) {
- var swordCooldown = Math.round(self.swordAttackSpeedFrames / currentAttackSpeedMult); // Apply attack speed
+ 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; // Simple swing arc
- // Check for hits during the swing (e.g., mid-swing) - Check only once per swing? Add hit tracking.
- // For now, check multiple times:
+ self.swordGraphics.rotation = Math.sin(swingProgress * Math.PI) * 1.2;
if (swingProgress > 0.3 && swingProgress < 0.7 && self.attackDurationCounter % 5 === 0) {
- // Check every 5 frames during swing
self.checkSwordHits(attackAngle);
}
} else {
self.isAttacking = false;
self.swordGraphics.rotation = 0;
- self.attackCooldown = swordCooldown; // Set cooldown AFTER swing
+ self.attackCooldown = swordCooldown;
}
- } else if (self.attackCooldown <= 0 && (isAiming || isMoving)) {
- // Require aiming or moving to attack? Or just aiming joystick input? Let's require input.
- if (isAiming) {
- // Start swing only if aiming stick has input
- self.isAttacking = true;
- self.attackDurationCounter = self.attackDuration;
- // Play swing sound? LK.playSound('sword_swing');
- }
+ } else if (self.attackCooldown <= 0 && isAiming) {
+ self.isAttacking = true;
+ self.attackDurationCounter = self.attackDuration;
}
}
- // Bow Attack
+ // Bow
else if (self.weaponType === 2) {
- var bowCooldown = Math.round(self.bowAttackSpeedFrames / currentAttackSpeedMult); // Apply attack speed
+ var bowCooldown = Math.round(self.bowAttackSpeedFrames / currentAttackSpeedMult);
if (self.attackCooldown <= 0 && isAiming) {
- // Require aim input to fire bow
self.fireArrow(attackAngle);
self.attackCooldown = bowCooldown;
- // Play shoot sound? LK.playSound('bow_shoot');
}
}
} else {
- // Reset attack state if popup is open
+ // 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 ---
+ // Helper Methods
self.checkSwordHits = function (attackAngle) {
var swordDamageInfo = self.calculateOutgoingDamage(self.swordBaseDamage, DamageType.PHYSICAL);
- var hitEnemiesThisSwing = []; // Track enemies hit in this specific check
+ var hitEnemiesThisCheck = [];
for (var i = currentRoom.enemies.length - 1; i >= 0; i--) {
var enemy = currentRoom.enemies[i];
- if (!enemy || !enemy.active || hitEnemiesThisSwing.includes(enemy)) {
- // Skip inactive or already hit
+ if (!enemy || !enemy.active || hitEnemiesThisCheck.includes(enemy)) {
continue;
}
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 sword range stat
+ var rangeSqr = self.swordRange * self.swordRange;
if (distSqr < rangeSqr) {
var angleToEnemy = Math.atan2(dy, dx);
var angleDifference = Math.atan2(Math.sin(angleToEnemy - attackAngle), Math.cos(angleToEnemy - attackAngle));
if (Math.abs(angleDifference) < Math.PI / 3) {
- // Wider cone for sword (60 degrees)
if (enemy.takeDamage) {
- console.log("Sword Hit! Damage:", swordDamageInfo.damage); // Debug
- enemy.takeDamage(swordDamageInfo.damage, DamageType.PHYSICAL, self); // Pass damage type and source (self)
- hitEnemiesThisSwing.push(enemy); // Mark as hit for this check instance
- // Apply LifeSteal on hit
+ enemy.takeDamage(swordDamageInfo.damage, DamageType.PHYSICAL, self);
+ hitEnemiesThisCheck.push(enemy);
var lifeStealAmount = swordDamageInfo.damage * self.getStat('lifeSteal');
if (lifeStealAmount > 0) {
- self.takeDamage(lifeStealAmount, DamageType.HEALING); // Heal self
+ self.takeDamage(lifeStealAmount, DamageType.HEALING);
}
- // --- TODO: Add Sword Hit VFX/SFX ---
- // --- TODO: Add on-hit proc checks here (e.g., for Chaotic Spark, Serrated Shade) ---
}
}
}
}
@@ -639,24 +399,23 @@
self.fireArrow = function (fireAngle) {
if (self.weaponType !== 2) {
return;
}
- var arrow = new Arrow(); // Arrow class now has damageType
- var bowDamageInfo = self.calculateOutgoingDamage(self.bowBaseDamage, DamageType.PHYSICAL); // Arrows are physical by default
- arrow.damage = bowDamageInfo.damage; // Use calculated damage
- arrow.damageType = DamageType.PHYSICAL; // Explicitly set type
- arrow.isCrit = bowDamageInfo.isCrit; // Pass crit status if needed for visuals later
- arrow.critChance = self.getStat('critChance'); // Pass hero stats to arrow
+ 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; // Track source
- arrow.x = self.x + Math.cos(fireAngle) * 30; // Start slightly ahead
+ 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); // Add to global array
- gameContainer.addChild(arrow); // Add to scene
+ arrows.push(arrow);
+ gameContainer.addChild(arrow);
};
self.switchWeapon = function () {
- // --- [Switch weapon logic remains the same] ---
self.weaponType = self.weaponType === 1 ? 2 : 1;
self.swordGraphics.visible = self.weaponType === 1;
self.bowGraphics.visible = self.weaponType === 2;
self.isAttacking = false;
@@ -664,225 +423,306 @@
self.attackCooldown = 0;
};
return self;
});
-// --- Base Enemy Class (Modified - Inherits from BaseEntity) ---
+// --- Base Enemy Class ---
var BaseEnemy = BaseEntity.expand(function () {
var self = BaseEntity.call(this);
- // <<< Inherit from BaseEntity
- // --- Default Enemy Stats (Override BaseEntity defaults) ---
+ // Default Enemy Stats
self.health = 10;
self.maxHealth = 10;
self.armor = 5;
self.magicResist = 0;
self.movementSpeed = 1.0;
- self.damageBonus = 0; // Enemies don't typically get % damage bonuses easily
- self.critChance = 0.0; // Enemies typically don't crit unless specific type
+ self.damageBonus = 0;
+ self.critChance = 0.0;
self.critDamage = 1.5;
self.dodgeChance = 0.0;
- // --- Enemy Specific Properties ---
- self.damageAmount = 5; // Base damage dealt ON CONTACT/ATTACK
- self.damageCooldownTime = 60; // Frames between contact damage ticks
+ // Enemy Specific Properties
+ self.damageAmount = 5;
+ self.damageCooldownTime = 60;
self.damageCooldown = 0;
- self.active = true; // Flag if enemy should update
- // --- [handleWallCollision, moveTowards remain mostly the same] ---
- self.handleWallCollision = function () {/* ... unchanged ... */};
- self.moveTowards = function (targetX, targetY, moveSpeed) {/* ... unchanged ... */};
- // --- takeDamage is now inherited from BaseEntity ---
- // self.takeDamage = function (amount, damageType, source) { ... }; // Removed, uses BaseEntity's now
- // --- Die Method (Overrides BaseEntity's, adds game logic) ---
+ // 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) {
- // killer parameter is optional but good practice
- if (self.active) {
- self.active = false; // Prevent further updates/actions
- // --- TODO: Add drop logic here ---
- // Example: checkDropTable();
+ if (!self.active) {
+ return;
+ }
+ self.active = false;
+ // --- TODO: Add drop logic here ---
+ if (currentRoom) {
currentRoom.enemiesKilled++;
- currentRoom.enemyCounter--; // Decrement active enemy count
+ currentRoom.enemyCounter--;
updateRoomDisplay();
- // Remove from room's enemy list
var index = currentRoom.enemies.indexOf(self);
if (index > -1) {
currentRoom.enemies.splice(index, 1);
}
- // --- TODO: Add death VFX/SFX ---
- // Remove from scene graph (call BaseEntity's die for this)
- BaseEntity.prototype.die.call(self, killer);
- checkRoomCleared(); // Check if room is cleared after kill
+ 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 (Overrides BaseEntity's update) ---
+ // Default Update
self.update = function () {
- BaseEntity.prototype.update.call(self); // <<< CALL PARENT UPDATE FIRST (for modifiers)
if (!self.active || !hero) {
return;
}
- // Calculate actual move speed based on stats
+ BaseEntity.prototype.update.call(self); // Runs BaseEntity update (modifiers, status effects)
+ if (self.damageCooldown > 0) {
+ self.damageCooldown--;
+ }
var currentMoveSpeed = self.getStat('movementSpeed');
- // Default behavior: Move towards hero
var offsetX = (Math.random() - 0.5) * 20;
var offsetY = (Math.random() - 0.5) * 20;
- self.moveTowards(hero.x + offsetX, hero.y + offsetY, currentMoveSpeed); // Use calculated speed
+ if (hero) {
+ self.moveTowards(hero.x + offsetX, hero.y + offsetY, currentMoveSpeed);
+ }
self.handleWallCollision();
- // Decrement contact damage cooldown
- if (self.damageCooldown > 0) {
- self.damageCooldown--;
+ };
+ // 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];
+ }
};
- // --- Animation Helper ---
- self.setupAnimation = function (frameAssets) {/* ... unchanged ... */};
- self.updateAnimation = function () {/* ... unchanged ... */};
+ 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 (Modified) ---
+// --- TankEnemy Class ---
var TankEnemy = BaseEnemy.expand(function () {
var self = BaseEnemy.call(this);
- // Call base constructor
- // --- Override Default Stats ---
+ // Stat overrides
self.health = 50;
self.maxHealth = 50;
- self.armor = 20; // Higher armor
+ self.armor = 20;
self.magicResist = 10;
- self.movementSpeed = 0.8; // Slower
- self.damageAmount = 10; // Tank hits harder
+ self.movementSpeed = 0.8;
+ self.damageAmount = 10;
self.setupAnimation(['tank_run1', 'tank_run2', 'tank_run3', 'tank_run4']);
- self.minDistance = 80; // Slightly larger contact distance?
- // --- Update (Overrides BaseEnemy Update) ---
+ self.minDistance = 80;
+ // Update
self.update = function () {
- BaseEnemy.prototype.update.call(self); // <<< CALL PARENT UPDATE (Handles modifiers, base movement logic)
if (!self.active || !hero) {
return;
}
+ BaseEntity.prototype.update.call(self); // Run base updates (status effects, modifiers)
+ if (self.damageCooldown > 0) {
+ self.damageCooldown--;
+ } // Explicitly decrement cooldown
self.updateAnimation();
var targetX = hero.x;
var targetY = hero.y;
var distance = Math.sqrt(Math.pow(targetX - self.x, 2) + Math.pow(targetY - self.y, 2));
- var currentMoveSpeed = self.getStat('movementSpeed'); // Get potentially modified speed
+ var currentMoveSpeed = self.getStat('movementSpeed');
if (distance > self.minDistance) {
- // Only move if further than min distance
- self.moveTowards(targetX, targetY, currentMoveSpeed); // Use calculated speed
+ self.moveTowards(targetX, targetY, currentMoveSpeed);
} else {
- // Close enough, deal damage?
if (self.damageCooldown <= 0) {
- // Calculate final damage enemy deals (can be modified by enemy buffs/debuffs later)
+ self.damageCooldown = self.damageCooldownTime; // Reset CD FIRST
var outgoingDamageInfo = self.calculateOutgoingDamage(self.damageAmount, DamageType.PHYSICAL);
if (hero.takeDamage) {
- hero.takeDamage(outgoingDamageInfo.damage, DamageType.PHYSICAL, self); // Pass damage type and source
+ hero.takeDamage(outgoingDamageInfo.damage, DamageType.PHYSICAL, self);
}
- self.damageCooldown = self.damageCooldownTime;
- // updateRoomDisplay(); // Already called in hero.takeDamage
}
- // Tank specific: might not get pushed back, or only slightly
- self.rotation = Math.atan2(targetY - self.y, targetX - self.x); // Keep facing hero
+ self.rotation = Math.atan2(targetY - self.y, targetX - self.x);
}
- // self.handleWallCollision(); // Already handled in BaseEnemy.update
+ self.handleWallCollision();
};
return self;
});
-// --- RangedEnemy Class (Modified) ---
+// --- RangedEnemy Class ---
var RangedEnemy = BaseEnemy.expand(function () {
var self = BaseEnemy.call(this);
- // Call base constructor
- // --- Override Default Stats ---
+ // Stat overrides
self.health = 20;
self.maxHealth = 20;
- self.armor = 0; // Less armor
+ self.armor = 0;
self.magicResist = 5;
- self.movementSpeed = 1.2; // Slightly faster base speed
- self.damageAmount = 4; // Projectile base damage
+ self.movementSpeed = 1.2;
+ self.damageAmount = 4;
self.setupAnimation(['ranged_run1', 'ranged_run2', 'ranged_run3', 'ranged_run4']);
- self.projectileCooldownTime = 180; // 3 seconds at 60 FPS
- self.projectileCooldown = Math.random() * self.projectileCooldownTime; // Random initial cooldown
- self.desiredDistance = 350; // How far to stay from hero
- // --- Update (Overrides BaseEnemy Update) ---
+ self.projectileCooldownTime = 180;
+ self.projectileCooldown = Math.random() * self.projectileCooldownTime;
+ self.desiredDistance = 350;
+ // Update
self.update = function () {
- BaseEnemy.prototype.update.call(self); // <<< CALL PARENT UPDATE (Handles modifiers)
if (!self.active || !hero) {
return;
}
+ BaseEntity.prototype.update.call(self); // Run base updates (status effects, modifiers)
self.updateAnimation();
var targetX = hero.x;
var targetY = hero.y;
var dx = targetX - self.x;
var dy = targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
- var currentMoveSpeed = self.getStat('movementSpeed'); // Get potentially modified speed
- // Movement: Try to maintain desired distance
+ var currentMoveSpeed = self.getStat('movementSpeed');
if (distance < self.desiredDistance - 10) {
- self.moveTowards(targetX, targetY, -currentMoveSpeed); // Move away
+ self.moveTowards(targetX, targetY, -currentMoveSpeed);
} else if (distance > self.desiredDistance + 10) {
- self.moveTowards(targetX, targetY, currentMoveSpeed); // Move closer
+ self.moveTowards(targetX, targetY, currentMoveSpeed);
} else {
- // Close enough, just rotate to face hero
self.rotation = Math.atan2(dy, dx);
}
- // self.handleWallCollision(); // Already handled in BaseEnemy.update
- // Shooting Logic
+ self.handleWallCollision();
if (self.projectileCooldown <= 0) {
self.shootProjectile(targetX, targetY);
- // Apply attack speed modifier to cooldown?
var attackSpeedMult = self.getStat('attackSpeed');
- self.projectileCooldown = self.projectileCooldownTime / attackSpeedMult; // Faster attack speed = shorter cooldown
+ self.projectileCooldown = self.projectileCooldownTime / attackSpeedMult;
} else {
self.projectileCooldown--;
}
};
self.shootProjectile = function (targetX, targetY) {
- var projectile = new Projectile(); // Create instance
+ var projectile = new Projectile();
projectile.x = self.x;
projectile.y = self.y;
- // Calculate final projectile damage (can include enemy damageBonus etc. later)
- var outgoingDamageInfo = self.calculateOutgoingDamage(self.damageAmount, DamageType.PHYSICAL); // Assuming physical projectile
+ var outgoingDamageInfo = self.calculateOutgoingDamage(self.damageAmount, DamageType.PHYSICAL);
projectile.damage = outgoingDamageInfo.damage;
- projectile.damageType = DamageType.PHYSICAL; // Set type on projectile
- // projectile.isCrit = outgoingDamageInfo.isCrit; // Enemies don't crit by default
+ 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; // Set initial rotation
+ projectile.rotation = angle;
projectile.vx = Math.cos(angle) * projectile.speed;
projectile.vy = Math.sin(angle) * projectile.speed;
- projectile.source = self; // Track who shot it
- gameContainer.addChild(projectile); // Add to game world
+ gameContainer.addChild(projectile);
};
return self;
});
-// --- Enemy Class (Melee - Modified) ---
+// --- Enemy Class (Melee) ---
var Enemy = BaseEnemy.expand(function () {
var self = BaseEnemy.call(this);
- // Call base constructor
- // --- Override Default Stats ---
+ // Stat overrides
self.health = 10;
self.maxHealth = 10;
self.armor = 5;
self.magicResist = 0;
- self.movementSpeed = 1.5 + Math.random() * 0.3; // Base speed variation
+ self.movementSpeed = 1.5 + Math.random() * 0.3;
self.damageAmount = 5;
- self.setupAnimation(['enemy_run1', 'enemy_run2', 'enemy_run3', 'enemy_run4']); // Pass asset names
- // --- Update (Overrides BaseEnemy Update) ---
+ self.setupAnimation(['enemy_run1', 'enemy_run2', 'enemy_run3', 'enemy_run4']);
+ // Update
self.update = function () {
- BaseEnemy.prototype.update.call(self); // <<< CALL PARENT UPDATE (Handles modifiers, basic movement)
if (!self.active || !hero) {
return;
}
- self.updateAnimation(); // Update visual frame
- var targetX = hero.x + (Math.random() - 0.5) * 20; // Slight offset remains
+ BaseEntity.prototype.update.call(self); // Run base updates (status effects, modifiers)
+ if (self.damageCooldown > 0) {
+ self.damageCooldown--;
+ } // Explicitly decrement cooldown
+ self.updateAnimation();
+ var targetX = hero.x + (Math.random() - 0.5) * 20;
var targetY = hero.y + (Math.random() - 0.5) * 20;
- var currentMoveSpeed = self.getStat('movementSpeed'); // Use calculated speed
- var distance = self.moveTowards(targetX, targetY, currentMoveSpeed); // Use calculated speed
- var minDistance = 70; // Contact distance
+ var currentMoveSpeed = self.getStat('movementSpeed');
+ var distance = self.moveTowards(targetX, targetY, currentMoveSpeed);
+ var minDistance = 70;
if (distance <= minDistance) {
- // Handle being too close (damage hero)
if (self.damageCooldown <= 0) {
- // Calculate final damage enemy deals
+ self.damageCooldown = self.damageCooldownTime; // Reset CD FIRST
var outgoingDamageInfo = self.calculateOutgoingDamage(self.damageAmount, DamageType.PHYSICAL);
if (hero.takeDamage) {
- hero.takeDamage(outgoingDamageInfo.damage, DamageType.PHYSICAL, self); // Pass damage type and source
+ hero.takeDamage(outgoingDamageInfo.damage, DamageType.PHYSICAL, self);
}
- self.damageCooldown = self.damageCooldownTime;
- // updateRoomDisplay(); // Called in hero.takeDamage
}
- // Push back slightly (remains the same)
+ // Pushback logic...
var pushDx = self.x - targetX;
var pushDy = self.y - targetY;
var pushDist = Math.sqrt(pushDx * pushDx + pushDy * pushDy);
if (pushDist > 0) {
@@ -890,13 +730,13 @@
self.x += pushDx / pushDist * pushAmount;
self.y += pushDy / pushDist * pushAmount;
}
}
- // self.handleWallCollision(); // Already handled in BaseEnemy.update
+ self.handleWallCollision();
};
return self;
});
-// Joystick class (Movement)
+// Joystick class
var Joystick = Container.expand(function () {
var self = Container.call(this);
var joystickBackground = self.attachAsset('joystickBackground', {
anchorX: 0.5,
@@ -906,9 +746,9 @@
anchorX: 0.5,
anchorY: 0.5
});
self.x = joystickBackground.width / 2 + 100;
- self.y = roomHeight - joystickBackground.height / 2 - 100; // Use roomHeight
+ self.y = roomHeight - joystickBackground.height / 2 - 100;
var maxRadius = 100;
var isDragging = false;
self.down = function (x, y, obj) {
isDragging = true;
@@ -919,9 +759,8 @@
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);
@@ -948,43 +787,38 @@
};
};
return self;
});
-// --- Projectile Class (Enemy Ranged - Modified) ---
+// 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; // Base damage before source bonuses
- self.damageType = DamageType.PHYSICAL; // Default type, can be set by shooter
- self.source = null; // Reference to the enemy that shot this
+ 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; // Spin effect
- // Boundary check
+ 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;
}
- // Collision check with hero
- // Basic AABB or Circle collision (using intersects assumes engine provides it)
if (hero && hero.active && self.intersects && self.intersects(hero)) {
if (hero.takeDamage) {
- // console.log("Projectile Hit Hero! Damage:", self.damage, "Type:", self.damageType); // Debug
- hero.takeDamage(self.damage, self.damageType, self.source); // Pass damage type and source
+ hero.takeDamage(self.damage, self.damageType, self.source);
}
- // updateRoomDisplay(); // Called within hero.takeDamage
if (self.parent) {
self.parent.removeChild(self);
- } // Destroy on hit
+ }
}
};
return self;
});
@@ -1001,12 +835,12 @@
self.y = roomHeight - 400;
self.down = function () {
if (self.parent) {
menuContainer.removeChild(self.parent);
- } // Remove the popup it's on (main skill tree)
- isPopupActive = false; // No longer in menu
+ }
+ isPopupActive = false;
isSkillDetailPopupActive = false;
- initializeGame(); // Start the actual game
+ initializeGame();
};
return self;
});
@@ -1019,40 +853,390 @@
/****
* Game Code
****/
+// Define Damage Types
/****
-* Game Configuration & Data
+* Global Constants
****/
-// 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 --
+// Define Damage Types
+/****
+* Global Constants
+****/
+// LK.init.image('hero_topdown', {width:100, height:100, id:'YOUR_NEW_HERO_ASSET_ID'}) // Placeholder for new hero asset
+// KEEPING THIS FOR NOW - REPLACE WITH 'hero_topdown' LATER
// -- Symphony of Shadows Icons --
-// -- Circuit of Ascension Icons --
-// -- Echoes of Ancestry Icons --
-// -- Forge of Possibilities Icons --
-// -- Prism of Potential Icons --
// -- Nexus of Chaos Icons --
+// -- Prism of Potential Icons --
+// -- Forge of Possibilities Icons --
+// -- Echoes of Ancestry Icons --
+// -- Circuit of Ascension 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
+// -- Nexus of Chaos Icons --
+// -- Prism of Potential Icons --
+// -- Forge of Possibilities Icons --
+// -- Echoes of Ancestry Icons --
+// -- Circuit of Ascension Icons --
+// Made it grey
+// Add containers to the game stage
+// Adjusted default size
/****
-* Global Constants
+* Game Configuration & Data
****/
-// Define Damage Types
+// --- BaseEntity Prototype Methods (CORRECTED PLACEMENT) ---
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
+ CHARM: 'charm',
+ DISORIENT: 'disorient',
+ VULNERABLE: 'vulnerable'
+ // Add more as needed
+};
+// --- BaseEntity Prototype Methods ---
+// Update (Handles Modifiers and Status Effects)
+BaseEntity.prototype.update = function () {
+ if (!this.active) {
+ return;
+ }
+ this.updateModifiers(); // Update stat buff/debuff durations using 'this'
+ this.updateStatusEffects(); // Update status effect durations & apply DoTs using 'this'
+};
+// 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
+ if (statName === 'movementSpeed' || statName === 'attackSpeed') {
+ if (effect.type === StatusEffectType.CHILL) {
+ var _effect$magnitude;
+ speedMultiplier *= 1.0 - (((_effect$magnitude = effect.magnitude) === null || _effect$magnitude === void 0 ? void 0 : _effect$magnitude.speed) || 0.3);
+ } // Example: 30% slow
+ if (effect.type === StatusEffectType.SLOW) {
+ var _effect$magnitude2;
+ speedMultiplier *= 1.0 - (((_effect$magnitude2 = effect.magnitude) === null || _effect$magnitude2 === void 0 ? void 0 : _effect$magnitude2.speed) || 0.5);
+ } // Example: 50% slow
+ }
+ // Action prevention checks
+ if (effect.type === StatusEffectType.STUN || effect.type === StatusEffectType.FREEZE) {
+ canActMultiplier = 0;
+ }
+ });
+ finalValue *= speedMultiplier;
+ // If checking speed/attack speed, also apply stun/freeze 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) {
+ return;
+ } // Use 'this'
+ var effectType = effectData.type;
+ var source = effectData.source;
+ var durationFrames = Math.round((effectData.duration || 1) * 60);
+ var magnitude = effectData.magnitude;
+ var tickInterval = Math.round((effectData.tickInterval || 1) * 60);
+ var stackingType = effectData.stackingType || 'Duration';
+ var existingEffect = null;
+ var existingEffectIndex = -1;
+ for (var i = 0; i < this.activeStatusEffects.length; i++) {
+ // Use 'this'
+ if (this.activeStatusEffects[i].type === effectType && this.activeStatusEffects[i].source === source) {
+ existingEffect = this.activeStatusEffects[i];
+ existingEffectIndex = i;
+ break;
+ }
+ }
+ if (existingEffect) {
+ // Apply stacking rules... (logic unchanged, just uses existingEffect variable)
+ if (stackingType === 'Duration') {
+ existingEffect.duration = Math.max(existingEffect.duration, durationFrames);
+ existingEffect.magnitude = magnitude;
+ } else if (stackingType === 'Intensity') {
+ existingEffect.duration = Math.max(existingEffect.duration, durationFrames);
+ existingEffect.magnitude = magnitude; /* Add proper intensity stacking if needed */
+ } else {
+ return;
+ } // 'None'
+ } else {
+ var newEffect = {
+ id: Date.now().toString(36) + Math.random().toString(36).substr(2),
+ type: effectType,
+ source: source,
+ duration: durationFrames,
+ magnitude: magnitude,
+ ticksRemaining: 0,
+ tickInterval: tickInterval,
+ stackingType: stackingType,
+ data: {}
+ };
+ this.activeStatusEffects.push(newEffect); // Use 'this'
+ // --- TODO: Add visual/audio cue start ---
+ }
+};
+BaseEntity.prototype.removeStatusEffect = function (effectId) {
+ var index = this.activeStatusEffects.findIndex(function (eff) {
+ return eff.id === effectId;
+ });
+ if (index > -1) {
+ // --- TODO: Add visual/audio cue end ---
+ this.activeStatusEffects.splice(index, 1); // Use 'this'
+ }
+};
+BaseEntity.prototype.updateStatusEffects = function () {
+ for (var i = this.activeStatusEffects.length - 1; i >= 0; i--) {
+ var effect = this.activeStatusEffects[i];
+ effect.duration--;
+ var isDoT = effect.type === StatusEffectType.IGNITE || effect.type === StatusEffectType.POISON || effect.type === StatusEffectType.BLEED;
+ if (isDoT) {
+ effect.ticksRemaining--;
+ if (effect.ticksRemaining <= 0) {
+ var dotDamage = effect.magnitude || 0;
+ var damageType;
+ if (effect.type === StatusEffectType.IGNITE) {
+ damageType = DamageType.FIRE;
+ } else if (effect.type === StatusEffectType.POISON) {
+ damageType = DamageType.POISON;
+ } else if (effect.type === StatusEffectType.BLEED) {
+ damageType = DamageType.PHYSICAL;
+ } else {
+ damageType = DamageType.TRUE;
+ }
+ this.takeDamage(dotDamage, damageType, effect.source, true); // Use 'this', bypass dodge
+ effect.ticksRemaining = effect.tickInterval;
+ }
+ }
+ if (effect.duration <= 0) {
+ this.removeStatusEffect(effect.id);
+ } // Use 'this'
+ }
+};
+// 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;
+ }
+ });
+ 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'
+ var critChance = this.getStat('critChance'); // Use 'this'
+ if (Math.random() < critChance) {
+ isCrit = true;
+ var critMultiplier = this.getStat('critDamage'); // Use 'this'
+ 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;
+ } // Use 'this'
+ var healingMultiplier = 1.0;
+ if (damageType === DamageType.HEALING) {
+ this.activeStatusEffects.forEach(function (effect) {
+ // Use 'this'
+ if (effect.type === StatusEffectType.POISON) {
+ var _effect$magnitude3;
+ healingMultiplier *= 1.0 - (((_effect$magnitude3 = effect.magnitude) === null || _effect$magnitude3 === void 0 ? void 0 : _effect$magnitude3.healReduction) || 0.5);
+ }
+ });
+ amount *= healingMultiplier;
+ if (amount <= 0) {
+ return;
+ }
+ }
+ 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); // Use 'this'
+ this.health -= effectiveAmount; // Use 'this'
+ // --- TODOs unchanged ---
+ 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
+};
+// 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
@@ -2635,8 +2819,9 @@
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;
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)
/****
@@ -3090,26 +3275,36 @@
}
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) {
- // Check hero exists
- // Display current/max health
- healthText.setText('Health: ' + Math.max(Math.round(hero.health), 0) + ' / ' + Math.round(hero.getStat('maxHealth')));
- // Update color based on health percentage?
- var healthPercent = hero.health / hero.getStat('maxHealth');
+ 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.style.fill = 0x00FF00;
- } // Green
+ healthText.fill = 0x00FF00;
+ } // Green - Use healthText.fill
else if (healthPercent > 0.3) {
- healthText.style.fill = 0xFFFF00;
- } // Yellow
+ healthText.fill = 0xFFFF00;
+ } // Yellow - Use healthText.fill
else {
- healthText.style.fill = 0xFF0000;
- } // Red
+ 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
@@ -3209,9 +3404,9 @@
} else if (entrySide === 'right') {
hero.x = roomWidth - wallThickness - hero.width / 2 - 10;
hero.y = roomHeight / 2;
}
- stopRoomMusic(); // Stop music before transition logic
+ stopRoomMusic(); // Stop music before transition log
transitionToNextRoom(); // Sets up the new room, plays music, etc.
}
// --- Room Class Definition ---
var Room = function Room(number) {
@@ -3291,13 +3486,14 @@
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
};
+// Call startSpawning when the room is created and ready
+// this.startSpawning(); // Moved initialization to transitionToNextRoom
/****
* Game Setup and Initialization
****/
function initializeMainMenu() {
@@ -3419,8 +3615,9 @@
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
@@ -3465,8 +3662,18 @@
healthText.x = 250;
healthText.y = 60;
healthText.anchor.set(0, 0.5);
uiContainer.addChild(healthText);
+ enemyDebugText = new Text2('Enemy CD: ---', {
+ size: 40,
+ fill: 0xFFFF00,
+ // Yellow color for debug
+ fontWeight: "bold",
+ wordWrap: false
+ });
+ enemyDebugText.x = roomWidth - 250; // Position top-right corner
+ enemyDebugText.y = 120; // Below the main stats bar
+ enemyDebugText.anchor.set(1, 0); // Anchor top-right
updateRoomDisplay(); // Initial UI text update
playRoomMusic(currentRoom.number);
currentRoom.startSpawning();
}
@@ -3518,8 +3725,12 @@
/****
* 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;
@@ -3583,8 +3794,33 @@
enemy.update();
}
}
}
+ // 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) {
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