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
User prompt
The overload blast skill node skill allocation text underneath it should say 0/1, but it currently says 0/5.
/**** * 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 var Arrow = Container.expand(function () { var self = Container.call(this); self.attachAsset('arrow', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 15; self.damage = 5; // Base damage 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(); // Use built-in destroy if available // Remove from arrows array - IMPORTANT! var index = arrows.indexOf(self); if (index > -1) { arrows.splice(index, 1); } } }; // 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); } // Call original LK destroy else if (self.parent) { self.parent.removeChild(self); } // Manual remove if no built-in destroy }; 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; }); // Base Enemy class (with shared movement logic) var BaseEnemy = Container.expand(function () { var self = Container.call(this); self.speed = 1.0; // Default speed self.health = 10; // Default health self.damageAmount = 5; // Default damage self.damageCooldownTime = 60; // Frames self.damageCooldown = 0; self.active = true; // Flag if enemy should update // --- 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; // Return true if position was adjusted }; 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) { // Avoid division by zero and tiny movements self.x += dx / distance * moveSpeed; self.y += dy / distance * moveSpeed; self.rotation = Math.atan2(dy, dx); } return distance; }; self.takeDamage = function (amount) { self.health -= amount; if (self.health <= 0) { self.die(); } }; self.die = function () { if (self.active) { self.active = false; // Prevent further updates/actions currentRoom.enemiesKilled++; currentRoom.enemyCounter--; // Decrement active enemy count updateRoomDisplay(); // Remove from room's enemy list AND destroy the object var index = currentRoom.enemies.indexOf(self); if (index > -1) { currentRoom.enemies.splice(index, 1); } if (self.parent) { self.parent.removeChild(self); } // Remove from scene graph // LK might have a proper destroy method: if (self.destroy) self.destroy(); checkRoomCleared(); // Check if room is cleared after kill } }; // --- Default Update (can be overridden) --- self.update = function () { if (!self.active || !hero) { return; } // Basic safety checks // 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, self.speed); self.handleWallCollision(); }; // --- Animation Helper --- self.setupAnimation = function (frameAssets) { self.frames = frameAssets; self.currentFrameIndex = 0; self.frameDelay = 30; // Frames per animation frame self.frameCounter = 0; self.graphics = self.attachAsset(self.frames[0], { anchorX: 0.5, anchorY: 0.5 }); }; self.updateAnimation = function () { if (!self.frames || self.frames.length === 0) { return; } self.frameCounter++; if (self.frameCounter >= self.frameDelay) { self.frameCounter = 0; if (self.graphics) { self.removeChild(self.graphics); } // Remove old frame self.currentFrameIndex = (self.currentFrameIndex + 1) % self.frames.length; self.graphics = self.addChild(LK.getAsset(self.frames[self.currentFrameIndex], { anchorX: 0.5, anchorY: 0.5 })); // Add new frame asset } }; return self; }); // TankEnemy class var TankEnemy = BaseEnemy.expand(function () { var self = BaseEnemy.call(this); // Call base constructor self.setupAnimation(['tank_run1', 'tank_run2', 'tank_run3', 'tank_run4']); self.speed = 1.0; self.health = 50; self.damageAmount = 10; // Tank hits harder? self.minDistance = 80; // Slightly larger contact distance? self.update = function () { if (!self.active || !hero) { return; } 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)); if (distance > self.minDistance) { // Only move if further than min distance self.moveTowards(targetX, targetY, self.speed); } else { // Close enough, deal damage? if (self.damageCooldown <= 0) { if (hero.takeDamage) { hero.takeDamage(self.damageAmount); } else { hero.health -= self.damageAmount; } self.damageCooldown = self.damageCooldownTime; updateRoomDisplay(); } else { self.damageCooldown--; } // Tank might not get pushed back, or only slightly self.rotation = Math.atan2(targetY - self.y, targetX - self.x); // Keep facing hero } self.handleWallCollision(); }; return self; }); // RangedEnemy class var RangedEnemy = BaseEnemy.expand(function () { var self = BaseEnemy.call(this); // Call base constructor self.setupAnimation(['ranged_run1', 'ranged_run2', 'ranged_run3', 'ranged_run4']); self.speed = 2.0; self.health = 20; self.projectileCooldownTime = 180; // 3 seconds at 60 FPS self.projectileCooldown = self.projectileCooldownTime; // Start ready? Or random initial cooldown? self.desiredDistance = 350; // How far to stay from hero self.update = function () { if (!self.active || !hero) { return; } 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); // Movement: Try to maintain desired distance if (distance < self.desiredDistance - 10) { // Move away if too close (with buffer) self.moveTowards(targetX, targetY, -self.speed); } else if (distance > self.desiredDistance + 10) { // Move closer if too far (with buffer) self.moveTowards(targetX, targetY, self.speed); } else { // Close enough, just rotate to face hero self.rotation = Math.atan2(dy, dx); } self.handleWallCollision(); // Shooting Logic if (self.projectileCooldown <= 0) { self.shootProjectile(targetX, targetY); self.projectileCooldown = self.projectileCooldownTime; } else { self.projectileCooldown--; } }; self.shootProjectile = function (targetX, targetY) { var projectile = new Projectile(); // Create instance projectile.x = self.x; projectile.y = self.y; projectile.damage = self.damageAmount; // Use enemy's damage stat var dx = targetX - self.x; var dy = targetY - self.y; var angle = Math.atan2(dy, dx); projectile.rotation = angle; // Set initial rotation projectile.vx = Math.cos(angle) * projectile.speed; projectile.vy = Math.sin(angle) * projectile.speed; gameContainer.addChild(projectile); // Add to game world }; return self; }); // Enemy class (Melee) var Enemy = BaseEnemy.expand(function () { var self = BaseEnemy.call(this); // Call base constructor self.setupAnimation(['enemy_run1', 'enemy_run2', 'enemy_run3', 'enemy_run4']); // Pass asset names self.speed = 2 + Math.random() * 0.5; self.health = 10; self.damageAmount = 5; self.update = function () { if (!self.active || !hero) { return; } self.updateAnimation(); // Update visual frame var offsetX = (Math.random() - 0.5) * 20; var offsetY = (Math.random() - 0.5) * 20; var targetX = hero.x + offsetX; var targetY = hero.y + offsetY; var distance = self.moveTowards(targetX, targetY, self.speed); var minDistance = 70; if (distance <= minDistance) { // Handle being too close (damage hero) if (self.damageCooldown <= 0) { if (hero.takeDamage) { hero.takeDamage(self.damageAmount); } // Use hero's method if exists else { hero.health -= self.damageAmount; } // Direct modification fallback self.damageCooldown = self.damageCooldownTime; updateRoomDisplay(); // Update health display } else { self.damageCooldown--; } // Push back slightly 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; // Push back less aggressively self.x += pushDx / pushDist * pushAmount; self.y += pushDy / pushDist * pushAmount; } } self.handleWallCollision(); }; return self; }); // Hero class var Hero = Container.expand(function () { var self = Container.call(this); 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; // Adjust position as needed self.bowGraphics.visible = false; self.attackCooldown = 0; self.isAttacking = false; // Specifically for sword swing state self.health = 100; self.maxHealth = 100; // Add max health for potential healing skills self.attackDuration = 20; // Duration of sword swing animation self.attackDurationCounter = 0; self.weaponType = 1; // 1: Sword, 2: Bow self.attackRange = 60; // Sword range self.attackDamage = 10; // Base damage self.speed = 3; self.swordAttackSpeed = 45; // Cooldown frames for sword self.bowAttackSpeed = 30; // Cooldown frames for bow self.joystick = null; // Movement joystick ref self.aimingJoystick = null; // Aiming joystick ref self.facingDirection = 0; // For movement rotation when not aiming self.canExitRoom = false; // Flag set when room is cleared // Method to take damage self.takeDamage = function (amount) { self.health -= amount; if (self.health < 0) { self.health = 0; } updateRoomDisplay(); // Update health text if (self.health <= 0) { LK.showGameOver(); // Handle death } }; self.update = function () { if (self.health <= 0) { return; } // Don't update if dead // --- Movement --- var moveDirection = self.joystick ? self.joystick.getDirection() : { x: 0, y: 0 }; var nextX = self.x + moveDirection.x * self.speed; var nextY = self.y + moveDirection.y * self.speed; // --- Wall Collision & Room Transition Check --- 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; // Handle Room Transition (Moved here from game.update) if (self.canExitRoom && onEntrance) { // Determine exit side based on final position near edge var exitSide = ''; if (self.y < wallThickness + 10) { exitSide = 'top'; } // Exiting top else if (self.y > roomHeight - wallThickness - 10) { exitSide = 'bottom'; } // Exiting bottom else if (self.x < wallThickness + 10) { exitSide = 'left'; } // Exiting left else if (self.x > roomWidth - wallThickness - 10) { exitSide = 'right'; } // Exiting right if (exitSide) { // Determine entry side for the *next* room 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); // Trigger transition return; // Stop further updates this frame after transition } } // --- 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) { self.facingDirection = Math.atan2(moveDirection.y, moveDirection.x); } // Set hero rotation: Aiming takes priority if (isAiming && aimAngle !== null) { self.rotation = aimAngle; // Position weapons based on aim direction (simplified for now) if (self.weaponType === 1) { // Sword self.swordGraphics.rotation = 0; // Base rotation relative to hero } else { // Bow self.bowGraphics.rotation = 0; // Base rotation relative to hero } } else if (isMoving) { self.rotation = self.facingDirection; // Rotate based on movement if not aiming } // If not aiming and not moving, hero keeps last rotation // --- Attack Logic --- if (!isPopupActive && !isSkillDetailPopupActive) { // Check both flags if (self.attackCooldown > 0) { self.attackCooldown--; } // Determine attack target direction (aiming joystick or facing direction) var attackAngle = isAiming && aimAngle !== null ? aimAngle : self.rotation; // Sword Attack if (self.weaponType === 1) { if (self.isAttacking) { // Continue existing swing animation if (self.attackDurationCounter > 0) { self.attackDurationCounter--; var swingProgress = (self.attackDuration - self.attackDurationCounter) / self.attackDuration; // Simple swing animation self.swordGraphics.rotation = Math.sin(swingProgress * Math.PI) * 1.2; // Swing arc // Check for hits during the swing (e.g., mid-swing) if (swingProgress > 0.3 && swingProgress < 0.7) { // Check during the active part of swing self.checkSwordHits(attackAngle); } } else { // Swing finished self.isAttacking = false; self.swordGraphics.rotation = 0; self.attackCooldown = self.swordAttackSpeed; // Set cooldown AFTER swing finishes } } else if (self.attackCooldown <= 0) { // Start new swing if ready self.isAttacking = true; self.attackDurationCounter = self.attackDuration; // Play swing sound? } } // Bow Attack else if (self.weaponType === 2) { if (self.attackCooldown <= 0 && (isAiming || isMoving)) { // Require aim input or movement to fire bow? Or always fire? Assuming requires input/movement for now. self.fireArrow(attackAngle); self.attackCooldown = self.bowAttackSpeed; // Play shoot sound? } } } else { // Ensure attack state is reset if popup becomes active mid-swing if (self.isAttacking) { self.isAttacking = false; self.swordGraphics.rotation = 0; self.attackDurationCounter = 0; // Don't set cooldown here, might allow instant attack when popup closes } } }; // End of update function // --- Helper Methods --- self.checkSwordHits = function (attackAngle) { for (var i = currentRoom.enemies.length - 1; i >= 0; i--) { var enemy = currentRoom.enemies[i]; if (!enemy || !enemy.active) { continue; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distSqr = dx * dx + dy * dy; // Use squared distance for efficiency var rangeSqr = self.attackRange * self.attackRange; if (distSqr < rangeSqr) { var angleToEnemy = Math.atan2(dy, dx); // Calculate difference in angle, wrap around PI var angleDifference = Math.atan2(Math.sin(angleToEnemy - attackAngle), Math.cos(angleToEnemy - attackAngle)); if (Math.abs(angleDifference) < Math.PI / 4) { // Check if enemy is within 45-degree cone if (enemy.takeDamage) { enemy.takeDamage(self.attackDamage); } // Prevent hitting same enemy multiple times per swing - requires tracking hits // For simplicity now, we hit every frame check within the swing arc. // A better way: track which enemies were hit *this swing*. } } } }; self.fireArrow = function (fireAngle) { if (self.weaponType !== 2) { return; } var arrow = new Arrow(); arrow.x = self.x + Math.cos(fireAngle) * 30; // Start slightly ahead arrow.y = self.y + Math.sin(fireAngle) * 30; arrow.rotation = fireAngle; arrow.damage = self.attackDamage; // Use hero's damage arrows.push(arrow); // Add to global array gameContainer.addChild(arrow); // Add to scene }; 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; // Reset sword attack state self.attackDurationCounter = 0; self.attackCooldown = 0; // Allow immediate attack with new weapon? Or add small delay? }; return self; }); // Joystick class (Movement) 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; // 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 }; }; 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.vx = 0; // Velocity components self.vy = 0; self.update = function () { self.x += self.vx; self.y += self.vy; self.rotation += 0.1; // Spin effect // Boundary check if (self.x < 0 || self.x > roomWidth || self.y < 0 || self.y > roomHeight) { if (self.parent) { self.parent.removeChild(self); } // Destroy if out of bounds // No need to remove from an array if not tracked globally return; } // Collision check with hero if (hero && self.intersects && self.intersects(hero)) { // Check intersects exists if (hero.takeDamage) { hero.takeDamage(self.damage); } else { hero.health -= self.damage; } updateRoomDisplay(); if (self.parent) { self.parent.removeChild(self); } // Destroy on hit } }; 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); } // Remove the popup it's on (main skill tree) isPopupActive = false; // No longer in menu isSkillDetailPopupActive = false; initializeGame(); // Start the actual game }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 // Default background color }); /**** * Game Code ****/ /**** * Game Configuration & Data ****/ // Adjusted default size // Add containers to the game stage 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 --- (Assuming all max 5, simple descriptions for now) 'Red Radiance': { points: 0, max: 5, tree: 'The Prism of Potential', descriptionFn: function descriptionFn(p, d) { return 'Channel the crimson energy of aggression. Increases your attack damage slightly and grants a chance for attacks to ignite enemies, causing minor burn damage over time.'; } }, 'Blue Bulwark': { points: 0, max: 5, tree: 'The Prism of Potential', descriptionFn: function descriptionFn(p, d) { return 'Embrace cerulean hues of defense. Enhances your armor and magic resistance for a short duration, reducing incoming damage and occasionally shielding you with a barrier of ice.'; } }, 'Green Growth': { points: 0, max: 5, tree: 'The Prism of Potential', descriptionFn: function descriptionFn(p, d) { return 'Tap into verdant life essence. Gradually regenerates a portion of your health and grants a chance for attacks to apply a minor heal-over-time effect to yourself or nearby allies.'; } }, 'Yellow Zephyr': { points: 0, max: 5, tree: 'The Prism of Potential', descriptionFn: function descriptionFn(p, d) { return 'Harness the swiftness of golden winds. Boosts your movement speed slightly and improves your bow’s draw rate or sword swing speed, letting you dart around combat swiftly.'; } }, 'Indigo Insight': { points: 0, max: 5, tree: 'The Prism of Potential', descriptionFn: function descriptionFn(p, d) { return 'Draw from deep indigo wisdom. Grants increased critical hit chance and a small probability to reflect a portion of incoming damage back at your attacker, showcasing keen foresight.'; } }, 'Chromatic Fusion': { points: 0, max: 5, tree: 'The Prism of Potential', descriptionFn: function descriptionFn(p, d) { return 'Blend two chosen elemental colors (e.g., Red and Blue) to create a hybrid effect. Depending on the combination, gain unique bonuses, such as fiery storms or frostburn effects, strengthening both offense and defense.'; } }, 'Prismatic Ascendance': { points: 0, max: 5, tree: 'The Prism of Potential', descriptionFn: function descriptionFn(p, d) { return 'Briefly harness all hues at once, greatly enhancing elemental power. Attacks may inflict multiple ailments, and you gain increased damage, defense, healing, and speed.'; } }, // --- Nexus of Chaos --- (Assuming all max 5, simple descriptions for now) 'Chaotic Spark': { points: 0, max: 5, tree: 'The Nexus of Chaos', descriptionFn: function descriptionFn(p, d) { return 'Your attacks sometimes produce random elemental effects. Damage types and minor status effects vary unpredictably, keeping foes off-balance.'; } }, 'Entropy Shield': { points: 0, max: 5, tree: 'The Nexus of Chaos', descriptionFn: function descriptionFn(p, d) { return 'Surround yourself with shifting energy fields. Each incoming hit has a chance to fizzle harmlessly or rebound, dealing random damage back to attackers.'; } }, 'Unstable Rift': { points: 0, max: 5, tree: 'The Nexus of Chaos', descriptionFn: function descriptionFn(p, d) { return 'Occasionally summon a pocket of chaos in battle. Enemies near the rift suffer random debuffs—slowing, poisoning, burning—ever-changing, never stable.'; } }, 'Anomaly Shift': { points: 0, max: 5, tree: 'The Nexus of Chaos', descriptionFn: function descriptionFn(p, d) { return 'Briefly warp your form. Gain random beneficial buffs—from speed and regeneration to critical chance—but each application is uncertain, never guaranteed.'; } }, 'Cascade of Disorder': { points: 0, max: 5, tree: 'The Nexus of Chaos', descriptionFn: function descriptionFn(p, d) { return 'Trigger a chain reaction of volatile energies. Attacks may ignite unpredictable chain effects, spreading to multiple enemies or amplifying certain strikes.'; } }, 'Fractured Fate': { points: 0, max: 5, tree: 'The Nexus of Chaos', descriptionFn: function descriptionFn(p, d) { return 'Tap into temporal chaos. Sometimes attacks hit twice, or cooldowns shorten unexpectedly. Fate bends momentarily, granting you unpredictable bursts of advantage.'; } }, 'Reality Collapse': { points: 0, max: 5, tree: 'The Nexus of Chaos', descriptionFn: function descriptionFn(p, d) { return 'For a short time, unravel normal rules. Attacks ignore some defenses, enemies may lose control, and your random benefits intensify. Chaos reigns supreme, leaving foes disoriented and vulnerable.'; } }, // --- Symphony of Shadows --- (Assuming all max 5, simple descriptions for now) 'Whispered Steps': { points: 0, max: 5, tree: 'The Symphony of Shadows', descriptionFn: function descriptionFn(p, d) { return 'Move silently, becoming harder to detect. Occasionally evade attacks entirely, as your footsteps blend into the ambient darkness.'; } }, 'Nightfall Veil': { points: 0, max: 5, tree: 'The Symphony of Shadows', descriptionFn: function descriptionFn(p, d) { return 'Briefly cloak yourself in shadow. While cloaked, project a faint silhouette to mislead foes. Attacks aimed at you may strike only empty air.'; } }, 'Serrated Shade': { points: 0, max: 5, tree: 'The Symphony of Shadows', descriptionFn: function descriptionFn(p, d) { return 'Enhance your weapons with shadowy edges. Attacks may lacerate enemies, causing bleed-like damage over time as the darkness seeps into their wounds.'; } }, 'Shadow Familiar': { points: 0, max: 5, tree: 'The Symphony of Shadows', descriptionFn: function descriptionFn(p, d) { return 'Summon a small, dark entity that assists you. It distracts enemies, occasionally nipping at their heels and granting you openings to strike.'; } }, 'Muffled Strikes': { points: 0, max: 5, tree: 'The Symphony of Shadows', descriptionFn: function descriptionFn(p, d) { return 'Attacks produce little sound, reducing enemy awareness. Some foes fail to fully react, taking extra critical hits as they struggle to track your movements.'; } }, 'Umbral Echoes': { points: 0, max: 5, tree: 'The Symphony of Shadows', descriptionFn: function descriptionFn(p, d) { return 'Your attacks leave lingering echoes of shadow. These echoes confuse enemies, causing them to strike illusions or hesitate, giving you more control in battle.'; } }, 'Midnight Crescendo': { points: 0, max: 5, tree: 'The Symphony of Shadows', descriptionFn: function descriptionFn(p, d) { return 'For a short duration, become one with the darkness. Your attacks ignore some armor, movement speed increases, and enemies are overwhelmed by disorienting echoes. A swirling concert of shadows amplifies your lethality.'; } } }; // --- Centralized Skill Tree Layout Data --- 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: { '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: { 'Data Spike': 'data_spike_icon', ... } // Add actual icon asset IDs here if defined }, 'The Echoes of Ancestry': { backgroundAsset: 'popup_echoesofancestry', nodes: ['Ancient Wisdom', 'Elemental Mastery', 'Warrior Spirit', 'Beast Tamer', 'Herbal Remedies', 'Spirit Guidance', 'Legendary Ancestor'], positions: { '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: { ... } }, 'The Forge of Possibilities': { backgroundAsset: 'popup_forgeofpossibilities', nodes: ['Adaptive Construction', 'Elemental Infusion', 'Modular Enhancement', 'Resourceful Salvage', 'Hybrid Crafting', 'Runic Slots', 'Masterwork Creation'], positions: { '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: { ... } }, 'The Prism of Potential': { backgroundAsset: 'popup_prismofpotential', nodes: ['Red Radiance', 'Blue Bulwark', 'Green Growth', 'Yellow Zephyr', 'Indigo Insight', 'Chromatic Fusion', 'Prismatic Ascendance'], positions: { '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: { ... } }, 'The Nexus of Chaos': { backgroundAsset: 'popup_nexusofchaos', nodes: ['Chaotic Spark', 'Entropy Shield', 'Unstable Rift', 'Anomaly Shift', 'Cascade of Disorder', 'Fractured Fate', 'Reality Collapse'], positions: { '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: { ... } }, 'The Symphony of Shadows': { backgroundAsset: 'popup_symphonyofshadows', nodes: ['Whispered Steps', 'Nightfall Veil', 'Serrated Shade', 'Shadow Familiar', 'Muffled Strikes', 'Umbral Echoes', 'Midnight Crescendo'], positions: { '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 }, 'Muffled Strikes': { 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: { ... } } }; /**** * 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; 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); // 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); // 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; spendButton.down = function () { if (skillData && 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 --- var descTextElement = skillPopup.getChildByName("skillDescriptionText"); var allocTextElement = skillPopup.getChildByName("allocationText"); if (descTextElement && skillData.descriptionFn) { var newDescription = skillData.descriptionFn(skillData.points, skillData.calcData); descTextElement.setText(newDescription); // Recalculate position if height changes significantly? Might not be needed for most cases. allocTextElement.y = descTextElement.y + descTextElement.height / 2 + 100; // Adjust allocation text pos } if (allocTextElement) { allocTextElement.setText(skillData.points + '/' + skillData.max); } // Update the points on the node behind this popup immediately updateSkillNodePointDisplays(); } else { LK.log("Cannot spend point: Not enough points or max rank reached."); // 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) { LK.log('Error: Skill data not found for ' + skillName); 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 function createSkillTreeNode(skillName, position, parentPopup) { var skillData = skillDefinitions[skillName]; if (!skillData) { return; } // --- Icon Placeholder --- // var iconAssetId = skillTreeLayouts[skillData.tree]?.icons?.[skillName] || 'glowing_line_asset'; // Use specific icon if available var iconAssetId = 'glowing_line_asset'; // Placeholder var skillNodeGraphic = LK.getAsset(iconAssetId, { anchorX: 0.5, anchorY: 0.5 // scaleX: 1, scaleY: 1, // Use default scale or adjust as needed // width: 100, height: 100, // Use asset's default size or override }); // Apply color only if it's the placeholder shape if (iconAssetId === 'glowing_line_asset') { skillNodeGraphic.color = 0xc05bf1; skillNodeGraphic.width = 80; // Adjust size skillNodeGraphic.height = 80; } else { // Optional: Scale real icons if needed skillNodeGraphic.scale.set(3); } skillNodeGraphic.x = position.x; skillNodeGraphic.y = position.y; skillNodeGraphic.interactive = true; // --- Points Text --- var points = skillData.points; var maxPoints = skillData.max; var pointsText = new Text2(points + '/' + maxPoints, { size: 50, fill: 0xFFFFFF, fontWeight: "bold", stroke: 0x000000, strokeThickness: 4 }); pointsText.x = position.x; pointsText.y = position.y + 60; // Offset below node (adjust based on icon size) pointsText.anchor.set(0.5, 0); pointsText.name = skillName + "_pointsText"; // Unique name for updates // --- Click Handler --- skillNodeGraphic.down = function () { displaySkillDetailPopup(skillName); // Call the single generic display function // Visual feedback (Flash) var originalTint = skillNodeGraphic.tint; skillNodeGraphic.tint = 0xFFFF00; // Yellow flash LK.setTimeout(function () { 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) { LK.log("Error: Layout data not found for " + skillTreeName); 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 var skillTreeTitle = new Text2(skillTreeName, { size: 100, fill: 0xFFFFFF, fontWeight: "bold", // Slightly smaller title fontFamily: "Techno, sans-serif", letterSpacing: 2, stroke: 0x00FFCC, strokeThickness: 3, align: 'center' }); skillTreeTitle.x = roomWidth / 2; skillTreeTitle.y = 100; // Position near top skillTreeTitle.anchor.set(0.5, 0); newPopup.addChild(skillTreeTitle); // Create Nodes layoutData.nodes.forEach(function (skillName) { var position = layoutData.positions[skillName]; if (position) { createSkillTreeNode(skillName, position, newPopup); } else { LK.log("Warning: Position not defined for skill " + skillName); } }); // 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 for (var i = menuContainer.children.length - 1; i >= 0; i--) { var child = menuContainer.children[i]; if (child.name && child.name.endsWith("_popup")) { // Relies on name set in openSkillTreePopup activeSkillTreePopup = child; break; } } if (activeSkillTreePopup) { for (var skillName in skillDefinitions) { if (skillDefinitions.hasOwnProperty(skillName)) { var skillData = skillDefinitions[skillName]; // Find the text node by name convention var pointsTextNode = activeSkillTreePopup.getChildByName(skillName + "_pointsText"); if (pointsTextNode) { pointsTextNode.setText(skillData.points + '/' + skillData.max); } } } } } // 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); } } // --- 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) { healthText.setText('Health: ' + Math.max(hero.health, 0)); } // Show 0 if health < 0 } 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) { LK.log("Error playing music asset: " + musicAsset + " - " + 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) { LK.log("Error playing sound asset: " + soundAsset + " - " + 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 logic 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 } }; // 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 } function initializeGame() { // Initial setup when the game actually begins playing // Create Hero if not already created if (!hero) { hero = gameContainer.addChild(new Hero()); hero.x = roomWidth / 2; hero.y = roomHeight / 2; // Assign joysticks to hero 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.health = hero.maxHealth; hero.x = roomWidth / 2; hero.y = roomHeight / 2; hero.canExitRoom = false; // Reset any active effects or cooldowns if needed } // Set up initial room currentRoom = new Room(1); // Start in room 1 visitedRooms = [1]; // Mark room 1 as visited initially // Set initial background 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', { // Simple black bar background for stats anchorX: 0.0, anchorY: 0.0, width: roomWidth, height: 100, x: 0, y: 0, color: 0x111111, alpha: 0.7 }); uiContainer.addChild(statsBar); // Add to UI container roomDisplay = new Text2('Room: 1 | Enemies Left: 0', { size: 60, fill: 0xFFFFFF, fontWeight: "bold" }); roomDisplay.x = roomWidth / 2; // Center roomDisplay.y = 50; // Middle of bar roomDisplay.anchor.set(0.5, 0.5); uiContainer.addChild(roomDisplay); healthText = new Text2('Health: ' + hero.health, { size: 60, fill: 0x00FF00, fontWeight: "bold" }); healthText.x = 150; // Left side healthText.y = 50; healthText.anchor.set(0, 0.5); uiContainer.addChild(healthText); updateRoomDisplay(); // Initial UI text update playRoomMusic(currentRoom.number); // Play music for room 1 currentRoom.startSpawning(); // Start spawning enemies } // --- Global UI Buttons --- // Kill All Button (Debug?) var killAllButton = LK.getAsset('killAllButton', { anchorX: 0.5, anchorY: 0.5 }); killAllButton.x = 100; killAllButton.y = roomHeight - 200; // 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 - 150; // Position lower right switchWeaponZone.y = roomHeight - 200; 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 any popup is active, skip core game updates if (isPopupActive || isSkillDetailPopupActive) { // Can add logic here for animating menus if needed return; } // --- Core Gameplay Update --- if (!hero || !currentRoom) { return; } // Wait until game is initialized // Update Hero (handles movement, attacks, collision checks) hero.update(); // Update Arrows // Loop backwards for safe removal while iterating for (var j = arrows.length - 1; j >= 0; j--) { var arrow = arrows[j]; if (!arrow) { // Safety check if arrow was destroyed elsewhere 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 for collision var hitDx = enemy.x - arrow.x; var hitDy = enemy.y - arrow.y; var hitDistSqr = hitDx * hitDx + hitDy * hitDy; var enemyRadius = (enemy.width || 50) / 2; // Estimate radius var arrowRadius = (arrow.width || 10) / 2; var collisionDist = enemyRadius + arrowRadius; if (hitDistSqr < collisionDist * collisionDist) { // Check squared distance if (enemy.takeDamage) { enemy.takeDamage(arrow.damage); } // Damage enemy arrow.destroy(); // Destroy arrow (this should remove it from array too) // No need to splice here if arrow.destroy() handles it break; // Arrow hits one enemy and is destroyed } } } // End arrow update loop // Update Enemies (already loop backwards in takeDamage/die) // Make a copy of the array to iterate over, in case enemies are removed during update (e.g., by hero attack) var enemiesToUpdate = currentRoom.enemies.slice(); for (var k = 0; k < enemiesToUpdate.length; k++) { var enemy = enemiesToUpdate[k]; // Ensure the enemy wasn't destroyed by a previous update in the same frame if (enemy && enemy.active && enemy.update) { enemy.update(); } } // Room cleared check is now handled within enemy.die() // checkRoomCleared(); // Can be removed if die() handles it reliably }; // End Game Update
===================================================================
--- original.js
+++ change.js
@@ -1,8 +1,8 @@
/****
* Classes
****/
-// AimingJoystick class for aiming separately from movement
+// AimingJoystick class
var AimingJoystick = Container.expand(function () {
var self = Container.call(this);
var joystickBackground = self.attachAsset('joystickBackground', {
anchorX: 0.5,
@@ -11,17 +11,23 @@
var joystickHandle = self.attachAsset('joystickHandle', {
anchorX: 0.5,
anchorY: 0.5
});
- self.x = 2048 - joystickBackground.width / 2 - 100;
- self.y = 2732 - joystickBackground.height / 2 - 100;
+ 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) {
+ 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);
@@ -33,13 +39,8 @@
joystickHandle.x = dx;
joystickHandle.y = dy;
}
};
- self.up = function (x, y, obj) {
- isDragging = false;
- joystickHandle.x = 0;
- joystickHandle.y = 0;
- };
self.getDirection = function () {
var magnitude = Math.sqrt(joystickHandle.x * joystickHandle.x + joystickHandle.y * joystickHandle.y);
if (magnitude > 0) {
return {
@@ -53,35 +54,56 @@
};
};
self.getRotation = function () {
if (joystickHandle.x === 0 && joystickHandle.y === 0) {
- return null; // No direction provided
- }
+ 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 representing the arrows fired by the hero
+// Arrow class
var Arrow = Container.expand(function () {
var self = Container.call(this);
self.attachAsset('arrow', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 15;
- self.damage = 5;
+ self.damage = 5; // Base damage
self.update = function () {
self.x += Math.cos(self.rotation) * self.speed;
self.y += Math.sin(self.rotation) * self.speed;
- if (self.x < 0 || self.x > 2048 || self.y < 0 || self.y > 2732) {
- self.destroy();
+ // Check bounds
+ if (self.x < 0 || self.x > roomWidth || self.y < 0 || self.y > roomHeight) {
+ self.destroy(); // Use built-in destroy if available
+ // Remove from arrows array - IMPORTANT!
+ var index = arrows.indexOf(self);
+ if (index > -1) {
+ arrows.splice(index, 1);
+ }
}
};
+ // 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);
+ } // Call original LK destroy
+ else if (self.parent) {
+ self.parent.removeChild(self);
+ } // Manual remove if no built-in destroy
+ };
+ return self;
});
-// BackButton class representing the back button functionality
+// BackButton class (for skill tree navigation)
var BackButton = Container.expand(function () {
var self = Container.call(this);
self.attachAsset('backButton', {
anchorX: 0.5,
@@ -89,85 +111,269 @@
scaleX: 2,
scaleY: 2
});
self.x = 150;
- self.y = 350;
+ self.y = 350; // Position might need adjustment depending on layout
self.down = function () {
- menuContainer.removeChild(self.parent);
- menuContainer.addChild(skillTreePopup);
+ 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;
});
-// Enemy class representing the enemy character
-var Enemy = Container.expand(function () {
+// Base Enemy class (with shared movement logic)
+var BaseEnemy = Container.expand(function () {
var self = Container.call(this);
- var enemyGraphics = self.attachAsset('enemy_run1', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- var frames = [LK.getAsset('enemy_run1', {
- anchorX: 0.5,
- anchorY: 0.5
- }), LK.getAsset('enemy_run2', {
- anchorX: 0.5,
- anchorY: 0.5
- }), LK.getAsset('enemy_run3', {
- anchorX: 0.5,
- anchorY: 0.5
- }), LK.getAsset('enemy_run4', {
- anchorX: 0.5,
- anchorY: 0.5
- })];
- self.speed = 2 + Math.random() * 0.5;
+ self.speed = 1.0; // Default speed
+ self.health = 10; // Default health
+ self.damageAmount = 5; // Default damage
+ self.damageCooldownTime = 60; // Frames
self.damageCooldown = 0;
- var currentFrame = 0;
- var frameDelay = 30;
- var frameCounter = 0;
- self.update = function () {
- frameCounter++;
- if (frameCounter >= frameDelay) {
- frameCounter = 0;
- self.removeChild(enemyGraphics);
- currentFrame = (currentFrame + 1) % frames.length;
- enemyGraphics = self.addChild(frames[currentFrame]);
+ self.active = true; // Flag if enemy should update
+ // --- Shared Methods ---
+ self.handleWallCollision = function () {
+ var changed = false;
+ if (self.x < wallThickness) {
+ self.x = wallThickness;
+ changed = true;
}
- var offsetX = (Math.random() - 0.5) * 20;
- var offsetY = (Math.random() - 0.5) * 20;
- var dx = hero.x + offsetX - self.x;
- var dy = hero.y + offsetY - self.y;
+ 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; // Return true if position was adjusted
+ };
+ self.moveTowards = function (targetX, targetY, moveSpeed) {
+ var dx = targetX - self.x;
+ var dy = targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
- var minDistance = 70;
- if (distance > minDistance) {
- self.x += dx / distance * self.speed;
- self.y += dy / distance * self.speed;
+ if (distance > 0.1) {
+ // Avoid division by zero and tiny movements
+ self.x += dx / distance * moveSpeed;
+ self.y += dy / distance * moveSpeed;
self.rotation = Math.atan2(dy, dx);
+ }
+ return distance;
+ };
+ self.takeDamage = function (amount) {
+ self.health -= amount;
+ if (self.health <= 0) {
+ self.die();
+ }
+ };
+ self.die = function () {
+ if (self.active) {
+ self.active = false; // Prevent further updates/actions
+ currentRoom.enemiesKilled++;
+ currentRoom.enemyCounter--; // Decrement active enemy count
+ updateRoomDisplay();
+ // Remove from room's enemy list AND destroy the object
+ var index = currentRoom.enemies.indexOf(self);
+ if (index > -1) {
+ currentRoom.enemies.splice(index, 1);
+ }
+ if (self.parent) {
+ self.parent.removeChild(self);
+ } // Remove from scene graph
+ // LK might have a proper destroy method: if (self.destroy) self.destroy();
+ checkRoomCleared(); // Check if room is cleared after kill
+ }
+ };
+ // --- Default Update (can be overridden) ---
+ self.update = function () {
+ if (!self.active || !hero) {
+ return;
+ } // Basic safety checks
+ // 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, self.speed);
+ self.handleWallCollision();
+ };
+ // --- Animation Helper ---
+ self.setupAnimation = function (frameAssets) {
+ self.frames = frameAssets;
+ self.currentFrameIndex = 0;
+ self.frameDelay = 30; // Frames per animation frame
+ self.frameCounter = 0;
+ self.graphics = self.attachAsset(self.frames[0], {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ };
+ self.updateAnimation = function () {
+ if (!self.frames || self.frames.length === 0) {
+ return;
+ }
+ self.frameCounter++;
+ if (self.frameCounter >= self.frameDelay) {
+ self.frameCounter = 0;
+ if (self.graphics) {
+ self.removeChild(self.graphics);
+ } // Remove old frame
+ self.currentFrameIndex = (self.currentFrameIndex + 1) % self.frames.length;
+ self.graphics = self.addChild(LK.getAsset(self.frames[self.currentFrameIndex], {
+ anchorX: 0.5,
+ anchorY: 0.5
+ })); // Add new frame asset
+ }
+ };
+ return self;
+});
+// TankEnemy class
+var TankEnemy = BaseEnemy.expand(function () {
+ var self = BaseEnemy.call(this);
+ // Call base constructor
+ self.setupAnimation(['tank_run1', 'tank_run2', 'tank_run3', 'tank_run4']);
+ self.speed = 1.0;
+ self.health = 50;
+ self.damageAmount = 10; // Tank hits harder?
+ self.minDistance = 80; // Slightly larger contact distance?
+ self.update = function () {
+ if (!self.active || !hero) {
+ return;
+ }
+ 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));
+ if (distance > self.minDistance) {
+ // Only move if further than min distance
+ self.moveTowards(targetX, targetY, self.speed);
} else {
+ // Close enough, deal damage?
if (self.damageCooldown <= 0) {
- hero.health -= 5;
- self.damageCooldown = 60;
+ if (hero.takeDamage) {
+ hero.takeDamage(self.damageAmount);
+ } else {
+ hero.health -= self.damageAmount;
+ }
+ self.damageCooldown = self.damageCooldownTime;
updateRoomDisplay();
} else {
self.damageCooldown--;
}
- self.x -= dx / distance * (minDistance - distance);
- self.y -= dy / distance * (minDistance - distance);
+ // Tank might not get pushed back, or only slightly
+ self.rotation = Math.atan2(targetY - self.y, targetX - self.x); // Keep facing hero
}
- if (self.x < wallThickness) {
- self.x = wallThickness;
+ self.handleWallCollision();
+ };
+ return self;
+});
+// RangedEnemy class
+var RangedEnemy = BaseEnemy.expand(function () {
+ var self = BaseEnemy.call(this);
+ // Call base constructor
+ self.setupAnimation(['ranged_run1', 'ranged_run2', 'ranged_run3', 'ranged_run4']);
+ self.speed = 2.0;
+ self.health = 20;
+ self.projectileCooldownTime = 180; // 3 seconds at 60 FPS
+ self.projectileCooldown = self.projectileCooldownTime; // Start ready? Or random initial cooldown?
+ self.desiredDistance = 350; // How far to stay from hero
+ self.update = function () {
+ if (!self.active || !hero) {
+ return;
}
- if (self.x > roomWidth - wallThickness) {
- self.x = roomWidth - wallThickness;
+ 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);
+ // Movement: Try to maintain desired distance
+ if (distance < self.desiredDistance - 10) {
+ // Move away if too close (with buffer)
+ self.moveTowards(targetX, targetY, -self.speed);
+ } else if (distance > self.desiredDistance + 10) {
+ // Move closer if too far (with buffer)
+ self.moveTowards(targetX, targetY, self.speed);
+ } else {
+ // Close enough, just rotate to face hero
+ self.rotation = Math.atan2(dy, dx);
}
- if (self.y < wallThickness) {
- self.y = wallThickness;
+ self.handleWallCollision();
+ // Shooting Logic
+ if (self.projectileCooldown <= 0) {
+ self.shootProjectile(targetX, targetY);
+ self.projectileCooldown = self.projectileCooldownTime;
+ } else {
+ self.projectileCooldown--;
}
- if (self.y > roomHeight - wallThickness) {
- self.y = roomHeight - wallThickness;
+ };
+ self.shootProjectile = function (targetX, targetY) {
+ var projectile = new Projectile(); // Create instance
+ projectile.x = self.x;
+ projectile.y = self.y;
+ projectile.damage = self.damageAmount; // Use enemy's damage stat
+ var dx = targetX - self.x;
+ var dy = targetY - self.y;
+ var angle = Math.atan2(dy, dx);
+ projectile.rotation = angle; // Set initial rotation
+ projectile.vx = Math.cos(angle) * projectile.speed;
+ projectile.vy = Math.sin(angle) * projectile.speed;
+ gameContainer.addChild(projectile); // Add to game world
+ };
+ return self;
+});
+// Enemy class (Melee)
+var Enemy = BaseEnemy.expand(function () {
+ var self = BaseEnemy.call(this);
+ // Call base constructor
+ self.setupAnimation(['enemy_run1', 'enemy_run2', 'enemy_run3', 'enemy_run4']); // Pass asset names
+ self.speed = 2 + Math.random() * 0.5;
+ self.health = 10;
+ self.damageAmount = 5;
+ self.update = function () {
+ if (!self.active || !hero) {
+ return;
}
+ self.updateAnimation(); // Update visual frame
+ var offsetX = (Math.random() - 0.5) * 20;
+ var offsetY = (Math.random() - 0.5) * 20;
+ var targetX = hero.x + offsetX;
+ var targetY = hero.y + offsetY;
+ var distance = self.moveTowards(targetX, targetY, self.speed);
+ var minDistance = 70;
+ if (distance <= minDistance) {
+ // Handle being too close (damage hero)
+ if (self.damageCooldown <= 0) {
+ if (hero.takeDamage) {
+ hero.takeDamage(self.damageAmount);
+ } // Use hero's method if exists
+ else {
+ hero.health -= self.damageAmount;
+ } // Direct modification fallback
+ self.damageCooldown = self.damageCooldownTime;
+ updateRoomDisplay(); // Update health display
+ } else {
+ self.damageCooldown--;
+ }
+ // Push back slightly
+ 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; // Push back less aggressively
+ self.x += pushDx / pushDist * pushAmount;
+ self.y += pushDy / pushDist * pushAmount;
+ }
+ }
+ self.handleWallCollision();
};
return self;
});
-// Hero class representing the player character
+// Hero class
var Hero = Container.expand(function () {
var self = Container.call(this);
var heroGraphics = self.attachAsset('hero', {
anchorX: 0.5,
@@ -182,148 +388,236 @@
self.bowGraphics = self.attachAsset('bow', {
anchorX: 0.5,
anchorY: 0.5
});
- self.bowGraphics.x = heroGraphics.width / 2;
+ self.bowGraphics.x = heroGraphics.width / 2; // Adjust position as needed
self.bowGraphics.visible = false;
self.attackCooldown = 0;
- self.isAttacking = false;
+ self.isAttacking = false; // Specifically for sword swing state
self.health = 100;
- self.attackDuration = 20;
- self.attackDurationCounter = 0; // Counter to track swing animation progress
- self.weaponType = 1;
- self.attackRange = 60;
- self.attackDamage = 10;
+ self.maxHealth = 100; // Add max health for potential healing skills
+ self.attackDuration = 20; // Duration of sword swing animation
+ self.attackDurationCounter = 0;
+ self.weaponType = 1; // 1: Sword, 2: Bow
+ self.attackRange = 60; // Sword range
+ self.attackDamage = 10; // Base damage
self.speed = 3;
- // Attack speeds for different weapons
- self.swordAttackSpeed = 45; // Frames between sword attacks
- self.bowAttackSpeed = 30; // Frames between arrow shots
- self.joystick = null;
- self.aimingJoystick = null;
- // Properties to track facing direction separately from aiming direction
- self.facingDirection = 0; // Angle in radians
+ self.swordAttackSpeed = 45; // Cooldown frames for sword
+ self.bowAttackSpeed = 30; // Cooldown frames for bow
+ self.joystick = null; // Movement joystick ref
+ self.aimingJoystick = null; // Aiming joystick ref
+ self.facingDirection = 0; // For movement rotation when not aiming
+ self.canExitRoom = false; // Flag set when room is cleared
+ // Method to take damage
+ self.takeDamage = function (amount) {
+ self.health -= amount;
+ if (self.health < 0) {
+ self.health = 0;
+ }
+ updateRoomDisplay(); // Update health text
+ if (self.health <= 0) {
+ LK.showGameOver(); // Handle death
+ }
+ };
self.update = function () {
- // Movement handling
- var direction = this.joystick ? this.joystick.getDirection() : {
+ if (self.health <= 0) {
+ return;
+ } // Don't update if dead
+ // --- Movement ---
+ var moveDirection = self.joystick ? self.joystick.getDirection() : {
x: 0,
y: 0
};
- var nextX = this.x + direction.x * this.speed;
- var nextY = this.y + direction.y * this.speed;
- // Wall collision detection
- if (nextX < wallThickness && !(nextY >= entrances.left.yStart && nextY <= entrances.left.yEnd)) {
- nextX = wallThickness;
- } else if (nextX > roomWidth - wallThickness && !(nextY >= entrances.right.yStart && nextY <= entrances.right.yEnd)) {
- nextX = roomWidth - wallThickness;
+ var nextX = self.x + moveDirection.x * self.speed;
+ var nextY = self.y + moveDirection.y * self.speed;
+ // --- Wall Collision & Room Transition Check ---
+ 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 && !(nextX >= entrances.top.xStart && nextX <= entrances.top.xEnd)) {
- nextY = wallThickness;
- } else if (nextY > roomHeight - wallThickness && !(nextX >= entrances.bottom.xStart && nextX <= entrances.bottom.xEnd)) {
- nextY = roomHeight - wallThickness;
- }
- this.x = nextX;
- this.y = nextY;
- // Update facing direction if moving
- if (direction.x !== 0 || direction.y !== 0) {
- self.facingDirection = Math.atan2(direction.y, direction.x);
- // Only rotate hero graphics for movement if not aiming
- if (!self.aimingJoystick || !self.aimingJoystick.hasInput()) {
- // Use facing direction for hero rotation
- self.rotation = self.facingDirection;
+ 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;
+ }
}
- // If aiming joystick has input, use that direction for weapon aiming
- if (self.aimingJoystick && self.aimingJoystick.hasInput()) {
- var aimAngle = self.aimingJoystick.getRotation();
- if (aimAngle !== null) {
- // Rotate the hero for aiming
- self.rotation = aimAngle;
- // Position weapons correctly based on aim direction
- if (self.weaponType === 1) {
- // Sword
- self.swordGraphics.x = heroGraphics.width / 2 + Math.cos(aimAngle - self.rotation) * 20;
- self.swordGraphics.y = Math.sin(aimAngle - self.rotation) * 20;
- } else if (self.weaponType === 2) {
- // Bow
- self.bowGraphics.x = heroGraphics.width / 2;
- self.bowGraphics.rotation = 0; // Bow follows hero rotation
+ self.x = nextX;
+ self.y = nextY;
+ // Handle Room Transition (Moved here from game.update)
+ if (self.canExitRoom && onEntrance) {
+ // Determine exit side based on final position near edge
+ var exitSide = '';
+ if (self.y < wallThickness + 10) {
+ exitSide = 'top';
+ } // Exiting top
+ else if (self.y > roomHeight - wallThickness - 10) {
+ exitSide = 'bottom';
+ } // Exiting bottom
+ else if (self.x < wallThickness + 10) {
+ exitSide = 'left';
+ } // Exiting left
+ else if (self.x > roomWidth - wallThickness - 10) {
+ exitSide = 'right';
+ } // Exiting right
+ if (exitSide) {
+ // Determine entry side for the *next* room
+ 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); // Trigger transition
+ return; // Stop further updates this frame after transition
}
}
- // Auto-attack system - continuously attack based on weapon type
- if (!isPopupActive) {
+ // --- 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) {
+ self.facingDirection = Math.atan2(moveDirection.y, moveDirection.x);
+ }
+ // Set hero rotation: Aiming takes priority
+ if (isAiming && aimAngle !== null) {
+ self.rotation = aimAngle;
+ // Position weapons based on aim direction (simplified for now)
+ if (self.weaponType === 1) {
+ // Sword
+ self.swordGraphics.rotation = 0; // Base rotation relative to hero
+ } else {
+ // Bow
+ self.bowGraphics.rotation = 0; // Base rotation relative to hero
+ }
+ } else if (isMoving) {
+ self.rotation = self.facingDirection; // Rotate based on movement if not aiming
+ }
+ // If not aiming and not moving, hero keeps last rotation
+ // --- Attack Logic ---
+ if (!isPopupActive && !isSkillDetailPopupActive) {
+ // Check both flags
if (self.attackCooldown > 0) {
self.attackCooldown--;
}
- // For sword attacks - only start a new attack if not already attacking and cooldown is complete
- if (self.weaponType === 1 && !self.isAttacking && self.attackCooldown <= 0) {
- // Start a new sword attack
- self.isAttacking = true;
- self.attackDurationCounter = self.attackDuration; // Track swing progress separately from cooldown
+ // Determine attack target direction (aiming joystick or facing direction)
+ var attackAngle = isAiming && aimAngle !== null ? aimAngle : self.rotation;
+ // Sword Attack
+ if (self.weaponType === 1) {
+ if (self.isAttacking) {
+ // Continue existing swing animation
+ if (self.attackDurationCounter > 0) {
+ self.attackDurationCounter--;
+ var swingProgress = (self.attackDuration - self.attackDurationCounter) / self.attackDuration;
+ // Simple swing animation
+ self.swordGraphics.rotation = Math.sin(swingProgress * Math.PI) * 1.2; // Swing arc
+ // Check for hits during the swing (e.g., mid-swing)
+ if (swingProgress > 0.3 && swingProgress < 0.7) {
+ // Check during the active part of swing
+ self.checkSwordHits(attackAngle);
+ }
+ } else {
+ // Swing finished
+ self.isAttacking = false;
+ self.swordGraphics.rotation = 0;
+ self.attackCooldown = self.swordAttackSpeed; // Set cooldown AFTER swing finishes
+ }
+ } else if (self.attackCooldown <= 0) {
+ // Start new swing if ready
+ self.isAttacking = true;
+ self.attackDurationCounter = self.attackDuration;
+ // Play swing sound?
+ }
}
- // For bow attacks
- if (self.weaponType === 2 && self.attackCooldown <= 0) {
- // Fire an arrow
- self.fireArrow();
- self.attackCooldown = self.bowAttackSpeed;
+ // Bow Attack
+ else if (self.weaponType === 2) {
+ if (self.attackCooldown <= 0 && (isAiming || isMoving)) {
+ // Require aim input or movement to fire bow? Or always fire? Assuming requires input/movement for now.
+ self.fireArrow(attackAngle);
+ self.attackCooldown = self.bowAttackSpeed;
+ // Play shoot sound?
+ }
}
- }
- // Attack animation and logic for sword
- if (self.isAttacking) {
- // Track swing progress using a dedicated counter
- if (self.attackDurationCounter > 0) {
- self.attackDurationCounter--;
- }
- var swingProgress = (self.attackDuration - self.attackDurationCounter) / self.attackDuration;
- self.swordGraphics.rotation = Math.sin(swingProgress * Math.PI) * 1.2;
- self.swordGraphics.x = heroGraphics.width / 2 + Math.cos(swingProgress * Math.PI) * 30;
- self.swordGraphics.y = Math.sin(swingProgress * Math.PI) * 30;
- // End swing animation when appropriate
- if (swingProgress >= 1) {
+ } else {
+ // Ensure attack state is reset if popup becomes active mid-swing
+ if (self.isAttacking) {
self.isAttacking = false;
self.swordGraphics.rotation = 0;
- self.attackCooldown = self.swordAttackSpeed; // Only set cooldown after swing completes
- } else {
- // Check for enemies hit by sword during swing
- for (var i = currentRoom.enemies.length - 1; i >= 0; i--) {
- var enemy = currentRoom.enemies[i];
- var angleToEnemy = Math.atan2(enemy.y - self.y, enemy.x - self.x);
- var angleDifference = Math.abs(angleToEnemy - self.rotation);
- var swordDx = enemy.x - (self.x + Math.cos(self.rotation) * self.attackRange);
- var swordDy = enemy.y - (self.y + Math.sin(self.rotation) * self.attackRange);
- var swordDistance = Math.sqrt(swordDx * swordDx + swordDy * swordDy);
- if (swordDistance < self.attackRange && angleDifference < Math.PI / 4) {
- enemy.health -= self.attackDamage;
- if (enemy.health <= 0) {
- currentRoom.enemiesKilled++;
- currentRoom.enemyCounter--;
- enemy.destroy();
- currentRoom.enemies.splice(i, 1);
- updateRoomDisplay();
- }
+ self.attackDurationCounter = 0;
+ // Don't set cooldown here, might allow instant attack when popup closes
+ }
+ }
+ }; // End of update function
+ // --- Helper Methods ---
+ self.checkSwordHits = function (attackAngle) {
+ for (var i = currentRoom.enemies.length - 1; i >= 0; i--) {
+ var enemy = currentRoom.enemies[i];
+ if (!enemy || !enemy.active) {
+ continue;
+ }
+ var dx = enemy.x - self.x;
+ var dy = enemy.y - self.y;
+ var distSqr = dx * dx + dy * dy; // Use squared distance for efficiency
+ var rangeSqr = self.attackRange * self.attackRange;
+ if (distSqr < rangeSqr) {
+ var angleToEnemy = Math.atan2(dy, dx);
+ // Calculate difference in angle, wrap around PI
+ var angleDifference = Math.atan2(Math.sin(angleToEnemy - attackAngle), Math.cos(angleToEnemy - attackAngle));
+ if (Math.abs(angleDifference) < Math.PI / 4) {
+ // Check if enemy is within 45-degree cone
+ if (enemy.takeDamage) {
+ enemy.takeDamage(self.attackDamage);
}
+ // Prevent hitting same enemy multiple times per swing - requires tracking hits
+ // For simplicity now, we hit every frame check within the swing arc.
+ // A better way: track which enemies were hit *this swing*.
}
}
- } else {
- self.swordGraphics.rotation = 0;
}
};
- // Method to fire arrow in aimed direction
- self.fireArrow = function () {
+ self.fireArrow = function (fireAngle) {
if (self.weaponType !== 2) {
return;
- } // Only fire if bow is equipped
+ }
var arrow = new Arrow();
- arrow.x = self.x;
- arrow.y = self.y;
- // Use aiming joystick direction if available, otherwise use facing direction
- var fireDirection = self.aimingJoystick && self.aimingJoystick.hasInput() ? self.aimingJoystick.getRotation() : self.rotation;
- arrow.rotation = fireDirection;
- arrows.push(arrow);
- game.addChild(arrow);
+ arrow.x = self.x + Math.cos(fireAngle) * 30; // Start slightly ahead
+ arrow.y = self.y + Math.sin(fireAngle) * 30;
+ arrow.rotation = fireAngle;
+ arrow.damage = self.attackDamage; // Use hero's damage
+ arrows.push(arrow); // Add to global array
+ gameContainer.addChild(arrow); // Add to scene
};
+ 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; // Reset sword attack state
+ self.attackDurationCounter = 0;
+ self.attackCooldown = 0; // Allow immediate attack with new weapon? Or add small delay?
+ };
+ return self;
});
-// Joystick class representing the on-screen joystick for movement
+// Joystick class (Movement)
var Joystick = Container.expand(function () {
var self = Container.call(this);
var joystickBackground = self.attachAsset('joystickBackground', {
anchorX: 0.5,
@@ -333,16 +627,22 @@
anchorX: 0.5,
anchorY: 0.5
});
self.x = joystickBackground.width / 2 + 100;
- self.y = 2732 - joystickBackground.height / 2 - 100;
+ 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) {
+ 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);
@@ -354,13 +654,8 @@
joystickHandle.x = dx;
joystickHandle.y = dy;
}
};
- self.up = function (x, y, obj) {
- isDragging = false;
- joystickHandle.x = 0;
- joystickHandle.y = 0;
- };
self.getDirection = function () {
var magnitude = Math.sqrt(joystickHandle.x * joystickHandle.x + joystickHandle.y * joystickHandle.y);
if (magnitude > 0) {
return {
@@ -374,749 +669,84 @@
};
};
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.vx = 0; // Velocity components
+ self.vy = 0;
self.update = function () {
self.x += self.vx;
self.y += self.vy;
- self.rotation += 0.1; // Maintain spinning effect
- if (self.x < 0 || self.x > 2048 || self.y < 0 || self.y > 2732) {
- self.destroy();
+ self.rotation += 0.1; // Spin effect
+ // Boundary check
+ if (self.x < 0 || self.x > roomWidth || self.y < 0 || self.y > roomHeight) {
+ if (self.parent) {
+ self.parent.removeChild(self);
+ } // Destroy if out of bounds
+ // No need to remove from an array if not tracked globally
+ return;
}
- if (self.intersects(hero)) {
- hero.health -= self.damage;
- self.destroy();
+ // Collision check with hero
+ if (hero && self.intersects && self.intersects(hero)) {
+ // Check intersects exists
+ if (hero.takeDamage) {
+ hero.takeDamage(self.damage);
+ } else {
+ hero.health -= self.damage;
+ }
updateRoomDisplay();
+ if (self.parent) {
+ self.parent.removeChild(self);
+ } // Destroy on hit
}
};
return self;
});
-var RangedEnemy = Container.expand(function () {
- var self = Container.call(this);
- var rangedGraphics = self.attachAsset('ranged_run1', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- var frames = [LK.getAsset('ranged_run1', {
- anchorX: 0.5,
- anchorY: 0.5
- }), LK.getAsset('ranged_run2', {
- anchorX: 0.5,
- anchorY: 0.5
- }), LK.getAsset('ranged_run3', {
- anchorX: 0.5,
- anchorY: 0.5
- }), LK.getAsset('ranged_run4', {
- anchorX: 0.5,
- anchorY: 0.5
- })];
- self.speed = 2.0;
- self.health = 20;
- var currentFrame = 0;
- var frameDelay = 30;
- var frameCounter = 0;
- var projectileCooldown = 180; // 3 seconds at 60 FPS
- self.update = function () {
- frameCounter++;
- if (frameCounter >= frameDelay) {
- frameCounter = 0;
- self.removeChild(rangedGraphics);
- currentFrame = (currentFrame + 1) % frames.length;
- rangedGraphics = self.addChild(frames[currentFrame]);
- }
- var offsetX = (Math.random() - 0.5) * 20;
- var offsetY = (Math.random() - 0.5) * 20;
- var dx = hero.x + offsetX - self.x;
- var dy = hero.y + offsetY - self.y;
- var distance = Math.sqrt(dx * dx + dy * dy);
- var desiredDistance = 350; // Further increase the desired distance to maintain from the hero
- if (distance < desiredDistance) {
- self.x -= dx / distance * self.speed;
- self.y -= dy / distance * self.speed;
- } else if (distance > desiredDistance) {
- self.x += dx / distance * self.speed;
- self.y += dy / distance * self.speed;
- }
- self.rotation = Math.atan2(dy, dx);
- if (self.x < wallThickness) {
- self.x = wallThickness;
- }
- if (self.x > roomWidth - wallThickness) {
- self.x = roomWidth - wallThickness;
- }
- if (self.y < wallThickness) {
- self.y = wallThickness;
- }
- if (self.y > roomHeight - wallThickness) {
- self.y = roomHeight - wallThickness;
- }
- if (projectileCooldown <= 0) {
- self.shootProjectile();
- projectileCooldown = 180; // Increase cooldown to 3 seconds at 60 FPS
- } else {
- projectileCooldown--;
- }
- };
- self.shootProjectile = function () {
- var projectile = new Projectile('ranged_attack');
- projectile.x = self.x;
- projectile.y = self.y;
- var targetX = hero.x;
- var targetY = hero.y;
- 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;
- game.addChild(projectile);
- };
- return self;
-});
-// StartGameButton class representing the start game button functionality
+// 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 = 2048 / 2;
- self.y = 2732 - 400;
+ self.x = roomWidth / 2;
+ self.y = roomHeight - 400;
self.down = function () {
- menuContainer.removeChild(skillTreePopup);
- isPopupActive = false;
- isDataSpikePopupActive = false;
- initializeGame();
- // Music is now handled in initializeGame -> playRoomMusic
+ if (self.parent) {
+ menuContainer.removeChild(self.parent);
+ } // Remove the popup it's on (main skill tree)
+ isPopupActive = false; // No longer in menu
+ isSkillDetailPopupActive = false;
+ initializeGame(); // Start the actual game
};
return self;
});
-var TankEnemy = Container.expand(function () {
- var self = Container.call(this);
- var tankGraphics = self.attachAsset('tank_run1', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- var frames = [LK.getAsset('tank_run1', {
- anchorX: 0.5,
- anchorY: 0.5
- }), LK.getAsset('tank_run2', {
- anchorX: 0.5,
- anchorY: 0.5
- }), LK.getAsset('tank_run3', {
- anchorX: 0.5,
- anchorY: 0.5
- }), LK.getAsset('tank_run4', {
- anchorX: 0.5,
- anchorY: 0.5
- })];
- self.speed = 1.0;
- self.health = 50;
- var currentFrame = 0;
- var frameDelay = 30;
- var frameCounter = 0;
- self.update = function () {
- frameCounter++;
- if (frameCounter >= frameDelay) {
- frameCounter = 0;
- self.removeChild(tankGraphics);
- currentFrame = (currentFrame + 1) % frames.length;
- tankGraphics = self.addChild(frames[currentFrame]);
- }
- // Movement logic similar to Enemy class
- var offsetX = (Math.random() - 0.5) * 20;
- var offsetY = (Math.random() - 0.5) * 20;
- var dx = hero.x + offsetX - self.x;
- var dy = hero.y + offsetY - self.y;
- var distance = Math.sqrt(dx * dx + dy * dy);
- var minDistance = 70;
- if (distance > minDistance) {
- self.x += dx / distance * self.speed;
- self.y += dy / distance * self.speed;
- self.rotation = Math.atan2(dy, dx);
- }
- if (self.x < wallThickness) {
- self.x = wallThickness;
- }
- if (self.x > roomWidth - wallThickness) {
- self.x = roomWidth - wallThickness;
- }
- if (self.y < wallThickness) {
- self.y = wallThickness;
- }
- if (self.y > roomHeight - wallThickness) {
- self.y = roomHeight - wallThickness;
- }
- };
- return self;
-});
/****
* Initialize Game
****/
var game = new LK.Game({
- backgroundColor: 0x000000
+ backgroundColor: 0x000000 // Default background color
});
/****
* Game Code
****/
-function addDirectionalArrows() {
- var arrowDirections = ['top', 'bottom', 'left', 'right'];
- arrowDirections.forEach(function (direction) {
- var arrow = LK.getAsset('directionalArrow', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- switch (direction) {
- case 'top':
- arrow.x = 2048 / 2;
- arrow.y = 500;
- arrow.rotation = 0;
- break;
- case 'bottom':
- arrow.x = 2048 / 2;
- arrow.y = 2732 - 500;
- arrow.rotation = Math.PI;
- break;
- case 'left':
- arrow.x = 500;
- arrow.y = 2732 / 2;
- arrow.rotation = -Math.PI / 2;
- break;
- case 'right':
- arrow.x = 2048 - 500;
- arrow.y = 2732 / 2;
- arrow.rotation = Math.PI / 2;
- break;
- }
- game.addChild(arrow);
- LK.effects.flashObject(arrow, 0xffffff, 1000); // Flash effect
- });
-}
-function displayWhisperedStepsPopup() {
- var popup = createSkillPopup('Whispered Steps', 'Move silently, becoming harder to detect. Occasionally evade attacks entirely, as your footsteps blend into the ambient darkness.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayNightfallVeilPopup() {
- var popup = createSkillPopup('Nightfall Veil', 'Briefly cloak yourself in shadow. While cloaked, project a faint silhouette to mislead foes. Attacks aimed at you may strike only empty air.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displaySerratedShadePopup() {
- var popup = createSkillPopup('Serrated Shade', 'Enhance your weapons with shadowy edges. Attacks may lacerate enemies, causing bleed-like damage over time as the darkness seeps into their wounds.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayShadowFamiliarPopup() {
- var popup = createSkillPopup('Shadow Familiar', 'Summon a small, dark entity that assists you. It distracts enemies, occasionally nipping at their heels and granting you openings to strike.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayMuffledStrikesPopup() {
- var popup = createSkillPopup('Muffled Strikes', 'Attacks produce little sound, reducing enemy awareness. Some foes fail to fully react, taking extra critical hits as they struggle to track your movements.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayUmbralEchoesPopup() {
- var popup = createSkillPopup('Umbral Echoes', 'Your attacks leave lingering echoes of shadow. These echoes confuse enemies, causing them to strike illusions or hesitate, giving you more control in battle.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayMidnightCrescendoPopup() {
- var popup = createSkillPopup('Midnight Crescendo', 'For a short duration, become one with the darkness. Your attacks ignore some armor, movement speed increases, and enemies are overwhelmed by disorienting echoes. A swirling concert of shadows amplifies your lethality.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayChaoticSparkPopup() {
- var popup = createSkillPopup('Chaotic Spark', 'Your attacks sometimes produce random elemental effects. Damage types and minor status effects vary unpredictably, keeping foes off-balance.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayEntropyShieldPopup() {
- var popup = createSkillPopup('Entropy Shield', 'Surround yourself with shifting energy fields. Each incoming hit has a chance to fizzle harmlessly or rebound, dealing random damage back to attackers.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayUnstableRiftPopup() {
- var popup = createSkillPopup('Unstable Rift', 'Occasionally summon a pocket of chaos in battle. Enemies near the rift suffer random debuffs—slowing, poisoning, burning—ever-changing, never stable.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayAnomalyShiftPopup() {
- var popup = createSkillPopup('Anomaly Shift', 'Briefly warp your form. Gain random beneficial buffs—from speed and regeneration to critical chance—but each application is uncertain, never guaranteed.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayCascadeOfDisorderPopup() {
- var popup = createSkillPopup('Cascade of Disorder', 'Trigger a chain reaction of volatile energies. Attacks may ignite unpredictable chain effects, spreading to multiple enemies or amplifying certain strikes.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayFracturedFatePopup() {
- var popup = createSkillPopup('Fractured Fate', 'Tap into temporal chaos. Sometimes attacks hit twice, or cooldowns shorten unexpectedly. Fate bends momentarily, granting you unpredictable bursts of advantage.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayRealityCollapsePopup() {
- var popup = createSkillPopup('Reality Collapse', 'For a short time, unravel normal rules. Attacks ignore some defenses, enemies may lose control, and your random benefits intensify. Chaos reigns supreme, leaving foes disoriented and vulnerable.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayRedRadiancePopup() {
- var popup = createSkillPopup('Red Radiance', 'Channel the crimson energy of aggression. Increases your attack damage slightly and grants a chance for attacks to ignite enemies, causing minor burn damage over time.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayBlueBulwarkPopup() {
- var popup = createSkillPopup('Blue Bulwark', 'Embrace cerulean hues of defense. Enhances your armor and magic resistance for a short duration, reducing incoming damage and occasionally shielding you with a barrier of ice.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayGreenGrowthPopup() {
- var popup = createSkillPopup('Green Growth', 'Tap into verdant life essence. Gradually regenerates a portion of your health and grants a chance for attacks to apply a minor heal-over-time effect to yourself or nearby allies.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayYellowZephyrPopup() {
- var popup = createSkillPopup('Yellow Zephyr', 'Harness the swiftness of golden winds. Boosts your movement speed slightly and improves your bow’s draw rate or sword swing speed, letting you dart around combat swiftly.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayIndigoInsightPopup() {
- var popup = createSkillPopup('Indigo Insight', 'Draw from deep indigo wisdom. Grants increased critical hit chance and a small probability to reflect a portion of incoming damage back at your attacker, showcasing keen foresight.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayChromaticFusionPopup() {
- var popup = createSkillPopup('Chromatic Fusion', 'Blend two chosen elemental colors (e.g., Red and Blue) to create a hybrid effect. Depending on the combination, gain unique bonuses, such as fiery storms or frostburn effects, strengthening both offense and defense.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayPrismaticAscendancePopup() {
- var popup = createSkillPopup('Prismatic Ascendance', 'Briefly harness all hues at once, greatly enhancing elemental power. Attacks may inflict multiple ailments, and you gain increased damage, defense, healing, and speed.');
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayAdaptiveConstructionPopup() {
- // Firebrand Enchantment: Imbue your weapon with searing flame—each rank adds +5 % fire damage and +2 % chance to ignite on hit.
- var skillName = 'Firebrand Enchantment';
- var points = skillPointAllocations[skillName] ? skillPointAllocations[skillName].points : 0;
- var maxPoints = skillPointAllocations[skillName] ? skillPointAllocations[skillName].max : 5;
- var fireDmgPerRank = 5;
- var ignitePerRank = 2;
- var currentFire = points * fireDmgPerRank;
- var currentIgnite = points * ignitePerRank;
- var description = '';
- if (points === 0) {
- description = "Imbue your weapon with searing flame, adding 5% fire damage and 2% chance to ignite on hit.";
- } else if (points < maxPoints) {
- description = "Imbue your weapon with searing flame, adding " + (points + 1) * fireDmgPerRank + "% fire damage and " + (points + 1) * ignitePerRank + "% chance to ignite on hit (Currently " + currentFire + "% fire damage / " + currentIgnite + "% ignite).";
- } else {
- description = "Maximum rank reached. Imbue your weapon with searing flame, adding 25% fire damage and 10% chance to ignite on hit.";
- }
- var popup = createSkillPopup(skillName, description);
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayElementalInfusionPopup() {
- // 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).
- var skillName = 'Elemental Tempo';
- var points = skillPointAllocations[skillName] ? skillPointAllocations[skillName].points : 0;
- var maxPoints = skillPointAllocations[skillName] ? skillPointAllocations[skillName].max : 5;
- var procs = [10, 8, 6, 4, 2];
- var currentProc = 0;
- for (var i = 0; i < points; i++) {
- currentProc += procs[i];
- }
- var description = '';
- if (points === 0) {
- description = "Every attack has a 10% chance to inflict a random elemental effect.";
- } else if (points < maxPoints) {
- var nextProc = procs[points];
- description = "Every attack has a " + (currentProc + nextProc) + "% chance to inflict a random elemental effect (Currently " + currentProc + "%).";
- } else {
- description = "Maximum rank reached. Every attack has a 30% chance to inflict a random elemental effect.";
- }
- var popup = createSkillPopup(skillName, description);
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayModularEnhancementPopup() {
- // 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).
- var skillName = 'Mechanical Reflex';
- var points = skillPointAllocations[skillName] ? skillPointAllocations[skillName].points : 0;
- var maxPoints = skillPointAllocations[skillName] ? skillPointAllocations[skillName].max : 5;
- var procs = [3, 6, 9, 12, 15];
- var currentProc = 0;
- for (var i = 0; i < points; i++) {
- currentProc += procs[i];
- }
- var description = '';
- if (points === 0) {
- description = "Each hit has a 3% chance to instantly ready your weapon for an extra strike.";
- } else if (points < maxPoints) {
- var nextProc = procs[points];
- description = "Each hit has a " + (currentProc + nextProc) + "% chance to instantly ready your weapon for an extra strike (Currently " + currentProc + "%).";
- } else {
- description = "Maximum rank reached. Each hit has a 15% chance to instantly ready your weapon for an extra strike.";
- }
- var popup = createSkillPopup(skillName, description);
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayResourcefulSalvagePopup() {
- // 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.
- var skillName = 'Resourceful Salvage';
- var points = skillPointAllocations[skillName] ? skillPointAllocations[skillName].points : 0;
- var maxPoints = skillPointAllocations[skillName] ? skillPointAllocations[skillName].max : 5;
- var procChances = [2, 3, 4, 5, 6];
- var buffStrengths = [10, 12, 14, 16, 20];
- var buffDurations = [5, 7, 9, 12, 15];
- var currentProc = points > 0 ? procChances[points - 1] : 0;
- var currentBuff = points > 0 ? buffStrengths[points - 1] : 0;
- var currentDur = points > 0 ? buffDurations[points - 1] : 0;
- var description = '';
- if (points === 0) {
- description = "Killing an enemy has a 2% chance to grant +10% skill‑point gain for 5 seconds.";
- } else if (points < maxPoints) {
- var nextProc = procChances[points];
- var nextBuff = buffStrengths[points];
- var nextDur = buffDurations[points];
- description = "Killing an enemy has a " + nextProc + "% chance to grant +" + nextBuff + "% skill‑point gain for " + nextDur + " seconds (Currently " + currentProc + "% / +" + currentBuff + "% / " + currentDur + "s).";
- } else {
- description = "Maximum rank reached. Killing an enemy has a 6% chance to grant +20% skill‑point gain for 15 seconds.";
- }
- var popup = createSkillPopup(skillName, description);
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayHybridCraftingPopup() {
- // 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.
- var skillName = 'Hybrid Crafting';
- var points = skillPointAllocations[skillName] ? skillPointAllocations[skillName].points : 0;
- var maxPoints = skillPointAllocations[skillName] ? skillPointAllocations[skillName].max : 1;
- var description = '';
- if (points === 0) {
- description = "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.";
- } else {
- description = "Maximum rank reached. 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.";
- }
- var popup = createSkillPopup(skillName, description);
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayRunicSlotsPopup() {
- // 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 %.
- var skillName = 'Gem Harvester';
- var points = skillPointAllocations[skillName] ? skillPointAllocations[skillName].points : 0;
- var maxPoints = skillPointAllocations[skillName] ? skillPointAllocations[skillName].max : 5;
- var dropChances = [5, 8, 12, 16, 20];
- var buffPotencies = [5, 7, 9, 12, 15];
- var buffDurations = [3, 4, 5, 6, 8];
- var currentDrop = points > 0 ? dropChances[points - 1] : 0;
- var currentBuff = points > 0 ? buffPotencies[points - 1] : 0;
- var currentDur = points > 0 ? buffDurations[points - 1] : 0;
- var description = '';
- if (points === 0) {
- description = "Killing an enemy has a 5% chance to drop a runic gem granting a random +5% buff for 3 seconds.";
- } else if (points < maxPoints) {
- var nextDrop = dropChances[points];
- var nextBuff = buffPotencies[points];
- var nextDur = buffDurations[points];
- description = "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 {
- description = "Maximum rank reached. Killing an enemy has a 20% chance to drop a runic gem granting a random +15% buff for 8 seconds.";
- }
- var popup = createSkillPopup(skillName, description);
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayMasterworkCreationPopup() {
- // 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.
- var skillName = 'Masterwork Creation';
- var points = skillPointAllocations[skillName] ? skillPointAllocations[skillName].points : 0;
- var maxPoints = skillPointAllocations[skillName] ? skillPointAllocations[skillName].max : 3;
- var dmgBonuses = [30, 60, 90];
- var cleaveCounts = [1, 2, 3];
- var shockwaveRadii = [1, 2, 3];
- var description = '';
- if (points === 0) {
- description = "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 < 3) {
- description = "Transforms your weapon for 6s, granting +" + dmgBonuses[points] + "% damage, arrows cleave " + cleaveCounts[points] + " extra target" + (cleaveCounts[points] > 1 ? "s" : "") + ", and melee triggers a " + shockwaveRadii[points] + "-unit shockwave (Currently +" + dmgBonuses[points - 1] + "% / " + cleaveCounts[points - 1] + " cleave / " + shockwaveRadii[points - 1] + "-unit shockwave).";
- } else {
- description = "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).";
- }
- var popup = createSkillPopup(skillName, description);
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayAncientWisdomPopup() {
- var skillName = 'Ancient Wisdom';
- var points = skillPointAllocations[skillName] ? skillPointAllocations[skillName].points : 0;
- var maxPoints = skillPointAllocations[skillName] ? skillPointAllocations[skillName].max : 5;
- var increases = [5, 4, 3, 2, 1]; // Diminishing returns
- var currentBenefit = 0;
- for (var i = 0; i < points; i++) {
- currentBenefit += increases[i];
- }
- var description = '';
- if (points < maxPoints) {
- var nextIncrease = increases[points];
- if (points === 0) {
- description = 'Increases elemental critical hit chance by 5%.';
- } else {
- description = 'Increases elemental critical hit chance by ' + nextIncrease + '% (Currently ' + currentBenefit + '%).';
- }
- } else {
- description = 'Maximum rank reached. Increases elemental critical hit chance by 15%.';
- }
- var popup = createSkillPopup(skillName, description);
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayElementalMasteryPopup() {
- var skillName = 'Elemental Mastery';
- var points = skillPointAllocations[skillName] ? skillPointAllocations[skillName].points : 0;
- var maxPoints = skillPointAllocations[skillName] ? skillPointAllocations[skillName].max : 5;
- var increases = [10, 8, 6, 4, 2]; // Diminishing returns
- var currentBenefit = 0;
- for (var i = 0; i < points; i++) {
- currentBenefit += increases[i];
- }
- var description = '';
- if (points < maxPoints) {
- var nextIncrease = increases[points];
- if (points === 0) {
- description = 'Increases elemental damage by 10%.';
- } else {
- description = 'Increases elemental damage by ' + nextIncrease + '% (Currently ' + currentBenefit + '%).';
- }
- } else {
- description = 'Maximum rank reached. Increases elemental damage by 30%.';
- }
- var popup = createSkillPopup(skillName, description);
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayWarriorSpiritPopup() {
- var skillName = 'Warrior Spirit';
- var points = skillPointAllocations[skillName] ? skillPointAllocations[skillName].points : 0;
- var maxPoints = skillPointAllocations[skillName] ? skillPointAllocations[skillName].max : 5;
- var increases = [10, 8, 6, 4, 2]; // Diminishing returns
- var currentBenefit = 0;
- for (var i = 0; i < points; i++) {
- currentBenefit += increases[i];
- }
- var description = '';
- if (points < maxPoints) {
- var nextIncrease = increases[points];
- if (points === 0) {
- description = 'Increases melee attack speed by 10%.';
- } else {
- description = 'Increases melee attack speed by ' + nextIncrease + '% (Currently ' + currentBenefit + '%).';
- }
- } else {
- description = 'Maximum rank reached. Increases melee attack speed by 30%.';
- }
- var popup = createSkillPopup(skillName, description);
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayBeastTamerPopup() {
- var skillName = 'Beast Tamer';
- var points = skillPointAllocations[skillName] ? skillPointAllocations[skillName].points : 0;
- var maxPoints = skillPointAllocations[skillName] ? skillPointAllocations[skillName].max : 5;
- var durations = [2, 3, 4, 4, 4]; // Duration in seconds
- var cooldowns = [20, 18, 16, 16, 16]; // Cooldown in seconds
- var currentDuration = 0;
- var currentCooldown = 0;
- if (points > 0) {
- currentDuration = durations[points - 1];
- currentCooldown = cooldowns[points - 1];
- }
- var description = '';
- if (points < maxPoints) {
- var nextDuration = durations[points];
- var nextCooldown = cooldowns[points];
- if (points === 0) {
- description = 'Summons a spirit beast for 2 seconds (Cooldown: 20s).';
- } else {
- description = 'Summons a spirit beast for ' + nextDuration + ' seconds (Cooldown: ' + nextCooldown + 's) (Currently ' + currentDuration + 's / ' + currentCooldown + 's).';
- }
- } else {
- description = 'Maximum rank reached. Summons a spirit beast for 4 seconds (Cooldown: 16s).';
- }
- var popup = createSkillPopup(skillName, description);
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayHerbalRemediesPopup() {
- var skillName = 'Herbal Remedies';
- var points = skillPointAllocations[skillName] ? skillPointAllocations[skillName].points : 0;
- var maxPoints = skillPointAllocations[skillName] ? skillPointAllocations[skillName].max : 5;
- var increases = [0.5, 0.6, 0.7, 0.8, 1.0]; // % max HP/sec
- var currentBenefit = 0;
- for (var i = 0; i < points; i++) {
- currentBenefit = increases[i];
- }
- var description = '';
- if (points < maxPoints) {
- var nextIncrease = increases[points];
- if (points === 0) {
- description = 'Heals 0.5% of max HP per second during combat.';
- } else {
- description = 'Heals ' + nextIncrease + '% per second (Currently ' + increases[points - 1] + '%).';
- }
- } else {
- description = 'Maximum rank reached. Heals 1.0% of max HP per second during combat.';
- }
- var popup = createSkillPopup(skillName, description);
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displaySpiritGuidancePopup() {
- var skillName = 'Spirit Guidance';
- var points = skillPointAllocations[skillName] ? skillPointAllocations[skillName].points : 0;
- var maxPoints = skillPointAllocations[skillName] ? skillPointAllocations[skillName].max : 5;
- var reductions = [5, 4, 3, 2, 1]; // Diminishing returns
- var currentBenefit = 0;
- for (var i = 0; i < points; i++) {
- currentBenefit += reductions[i];
- }
- var description = '';
- if (points < maxPoints) {
- var nextReduction = reductions[points];
- if (points === 0) {
- description = 'Reduces incoming damage by 5%.';
- } else {
- description = 'Reduces incoming damage by ' + nextReduction + '% (Currently ' + currentBenefit + '%).';
- }
- } else {
- description = 'Maximum rank reached. Reduces incoming damage by 15%.';
- }
- var popup = createSkillPopup(skillName, description);
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function displayLegendaryAncestorPopup() {
- var skillName = 'Legendary Ancestor';
- var points = skillPointAllocations[skillName] ? skillPointAllocations[skillName].points : 0;
- var maxPoints = skillPointAllocations[skillName] ? skillPointAllocations[skillName].max : 1;
- var description = 'Temporarily increases your damage by 100% and defense by 50% for 6 seconds (Cooldown: 60s).';
- if (points >= 1) {
- description = 'Maximum rank reached. Temporarily increases your damage by 100% and defense by 50% for 6 seconds (Cooldown: 60s).';
- }
- var popup = createSkillPopup(skillName, description);
- menuContainer.addChild(popup);
- isDataSpikePopupActive = true;
-}
-function updateSkillPointsDisplay() {
- if (typeof skillPointsDisplay !== 'undefined') {
- skillPointsDisplay.setText('Skill Points: ' + heroSkillPoints);
- }
- if (typeof skillPointsText !== 'undefined') {
- skillPointsText.setText('Skill Points: ' + heroSkillPoints);
- }
-}
-updateSkillPointsDisplay(); // call once to set initial text
-// Global variables
-var heroSkillPoints = 3; // Starting skill points
-var skillTreePointsText = null; // Will hold the text object once created
-// Define refreshTreePointsText globally so it's accessible everywhere
-function refreshTreePointsText() {
- if (skillTreePointsText) {
- skillTreePointsText.setText('Skill Points: ' + heroSkillPoints);
- }
-}
-// Function to update skill node point displays in all skill tree screens
-function updateSkillNodePointDisplays() {
- // Get all currently visible skill tree containers
- var activeSkillTree = menuContainer.children.find(function (child) {
- return child instanceof Container && child.width === 2048 && child.height === 2732 && child !== skillTreePopup && child.children.some(function (c) {
- return c instanceof Text2 && c.text && c.text.includes && c.text.includes('/');
- });
- });
- if (activeSkillTree) {
- // Find all point text elements and update them
- activeSkillTree.children.forEach(function (child) {
- if (child instanceof Text2 && child.text && child.text.includes && child.text.includes('/')) {
- // Find the closest skill node to determine which skill this text belongs to
- var closestNode = null;
- var closestDistance = Infinity;
- var childPosition = {
- x: child.x,
- y: child.y
- };
- // Find the skill node icon closest to this text
- activeSkillTree.children.forEach(function (potentialNode) {
- if (potentialNode.x === child.x && Math.abs(potentialNode.y - child.y) < 200) {
- var distance = Math.abs(potentialNode.y - child.y);
- if (distance < closestDistance) {
- closestDistance = distance;
- closestNode = potentialNode;
- }
- }
- });
- // If we found a skill node, find its skill name by position
- if (closestNode) {
- var skillNodes = null;
- // Determine which skill tree we're in based on assets or titles in the tree
- if (activeSkillTree.children.some(function (c) {
- return c.texture && c.texture.baseTexture && c.texture.baseTexture.imageUrl && c.texture.baseTexture.imageUrl.includes('circuitofascension');
- })) {
- for (var skillName in skillPointAllocations) {
- if (skillPointAllocations.hasOwnProperty(skillName)) {
- var points = skillPointAllocations[skillName].points;
- var maxPoints = skillPointAllocations[skillName].max;
- // Update this specific text
- child.setText(points + '/' + maxPoints);
- break;
- }
- }
- } else {
- // Just update any text that matches the X/Y format
- if (child.text.match(/^\d+\/\d+$/)) {
- var parts = child.text.split('/');
- var skillName = null;
- // Try to find matching skill name by looping through all skill nodes positions
- for (var sName in skillPointAllocations) {
- if (skillPointAllocations.hasOwnProperty(sName)) {
- // Update with appropriate points and max
- var points = skillPointAllocations[sName].points;
- var maxPoints = skillPointAllocations[sName].max;
- child.setText(points + '/' + maxPoints);
- break;
- }
- }
- }
- }
- }
- }
- });
- }
-}
-var roomBackgroundImages = ['backgroundImage1', 'backgroundImage2', 'backgroundImage3', 'backgroundImage4', 'backgroundImage5'];
-var arrows = [];
-var currentRoom;
-var visitedRooms = [];
-var totalRooms = 5; // Assuming there are 5 rooms
-var currentRoomMusic;
-var hero;
-var isPopupActive = false;
-var isDataSpikePopupActive = false;
+/****
+* Game Configuration & Data
+****/
+// Adjusted default size
+// Add containers to the game stage
var roomWidth = 2048;
var roomHeight = 2732;
var wallThickness = 400;
var entranceWidth = 200;
@@ -1143,1684 +773,1716 @@
}
};
var spawnPoints = {
topLeft: {
- x: wallThickness,
- y: wallThickness
+ x: wallThickness + 50,
+ y: wallThickness + 50
},
+ // Offset slightly from corner
topRight: {
- x: roomWidth - wallThickness,
- y: wallThickness
+ x: roomWidth - wallThickness - 50,
+ y: wallThickness + 50
},
bottomLeft: {
- x: wallThickness,
- y: roomHeight - wallThickness
+ x: wallThickness + 50,
+ y: roomHeight - wallThickness - 50
},
bottomRight: {
- x: roomWidth - wallThickness,
- y: roomHeight - wallThickness
+ x: roomWidth - wallThickness - 50,
+ y: roomHeight - wallThickness - 50
}
};
-var gameContainer = new Container();
-var uiContainer = new Container();
-var menuContainer = new Container();
-game.addChild(gameContainer);
-game.addChild(uiContainer);
-game.addChild(menuContainer);
-var skillTreePopup = new Container();
-skillTreePopup.width = 2048;
-skillTreePopup.height = 2732;
-skillTreePopup.x = 0;
-skillTreePopup.y = 0;
-var skillTreeBackground = LK.getAsset('skillTree_Background', {
- anchorX: 0.5,
- anchorY: 0.5
-});
-skillTreeBackground.x = 2048 / 2;
-skillTreeBackground.y = 2732 / 2;
-skillTreePopup.addChild(skillTreeBackground);
-// Add skill points display
-var skillPointsDisplay = new Text2('Skill Points: ' + heroSkillPoints, {
- size: 70,
- fill: 0xFFFFFF,
- fontWeight: "bold"
-});
-skillPointsDisplay.x = 2048 / 2; // center horizontally
-skillPointsDisplay.y = 2732 - 100; // near bottom
-skillPointsDisplay.anchor.set(0.5, 0);
-skillTreePopup.addChild(skillPointsDisplay);
-var skillTrees = [{
- title: 'The Circuit of Ascension',
- theme: 'Futuristic, cybernetic evolution'
-}, {
- title: 'The Echoes of Ancestry',
- theme: 'Mystical and historical'
-}, {
- title: 'The Forge of Possibilities',
- theme: 'Creative and modular'
-}, {
- title: 'The Prism of Potential',
- theme: 'Magical and elemental'
-}, {
- title: 'The Nexus of Chaos',
- theme: 'Unpredictable and chaotic'
-}, {
- title: 'The Symphony of Shadows',
- theme: 'Dark and stealth-focused'
-}];
-var positions = [{
- x: 1024 - 450,
- y: 1266 - 550
-}, {
- x: 1024 + 450,
- y: 1266 - 550
-}, {
- x: 1024 - 450,
- y: 1266
-}, {
- x: 1024 + 450,
- y: 1266
-}, {
- x: 1024 - 450,
- y: 1266 + 550
-}, {
- x: 1024 + 450,
- y: 1266 + 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'
-};
-positions.forEach(function (position, index) {
- var skillTree = skillTrees[index];
- var titleText = new Text2(skillTree.title, {
- size: 50,
- fill: 0xFFFFFF,
- fontWeight: "bold",
- fontFamily: "Techno, sans-serif",
- letterSpacing: 2,
- stroke: 0x00FFCC,
- strokeThickness: 3
- });
- titleText.x = position.x;
- titleText.y = position.y + 200;
- titleText.anchor.set(0.5, 0);
- var skillTreeAssetId = skillTreeAssetMap[skillTree.title];
- var skillTreeAsset = LK.getAsset(skillTreeAssetId, {
- anchorX: 0.5,
- anchorY: 0.5
- });
- if (!skillTreePopup.children.includes(skillTreeAsset)) {
- skillTreePopup.addChild(skillTreeAsset);
- }
- skillTreeAsset.x = position.x;
- skillTreeAsset.y = position.y;
- // Make the skill tree asset icon clickable
- skillTreeAsset.interactive = true;
- skillTreeAsset.down = function () {
- openSkillTreePopup(skillTree.title);
- };
- if (!skillTreePopup.children.includes(skillTreeAsset)) {
- skillTreePopup.addChild(skillTreeAsset);
- }
- // Keep the title text clickable as well
- titleText.down = function () {
- openSkillTreePopup(skillTree.title);
- };
- skillTreePopup.addChild(titleText);
-});
-isPopupActive = true;
-menuContainer.addChild(skillTreePopup);
-var testRectangle = LK.getAsset('box', {
- anchorX: 0.5,
- anchorY: 0.5,
- width: 100,
- height: 50,
- color: 0xff0000
-});
-testRectangle.x = 100;
-testRectangle.y = 100;
-skillTreePopup.addChild(testRectangle);
-var startGameButton = new StartGameButton();
-skillTreePopup.addChild(startGameButton);
-function initializeGame() {
- currentRoom = new Room(1);
- hero.x = 2048 / 2;
- hero.y = 2732 / 2;
- updateRoomDisplay();
- playRoomMusic(currentRoom.number);
-}
-// Room class representing individual rooms
-var Room = function Room(number) {
- this.number = number;
- this.enemies = [];
- this.isCleared = false;
- this.spawnLimit = this.number === 1 ? 29 : 12 * this.number;
- this.killGoal = this.spawnLimit;
- this.enemiesSpawned = 0;
- this.enemiesKilled = 0;
- this.enemyCounter = this.spawnLimit;
- this.init = function () {
- var self = this;
- var spawnInterval = LK.setInterval(function () {
- if (self.enemiesSpawned < self.spawnLimit && currentRoom.number === self.number && !isPopupActive) {
- self.spawnEnemy();
- } else {
- LK.clearInterval(spawnInterval);
+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];
}
- }, 500);
- };
- this.spawnEnemy = function () {
- if (this.enemiesSpawned < this.spawnLimit && !isPopupActive && currentRoom.number === this.number) {
- var spawnKeys = Object.keys(spawnPoints);
- var spawnIndex = Math.floor(Math.random() * spawnKeys.length);
- var spawnPoint = spawnPoints[spawnKeys[spawnIndex]];
- var enemy;
- if (this.number === 1 && Math.random() < 5 / 29) {
- enemy = new TankEnemy();
- } else if (this.number === 2) {
- if (Math.random() < 10 / 40) {
- enemy = new TankEnemy();
- } else if (Math.random() < 5 / 40) {
- enemy = new RangedEnemy();
- } else {
- enemy = new Enemy();
- }
+ 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 {
- enemy = new Enemy();
+ return 'Maximum rank reached. Your critical hit chance is increased by 15%.';
}
- enemy.x = spawnPoint.x;
- enemy.y = spawnPoint.y;
- enemy.active = true;
- enemy.health = 10;
- enemy.visible = true;
- this.enemies.push(enemy);
- game.addChild(enemy);
- this.enemiesSpawned++;
}
- };
- this.init();
-};
-var initialBgImage = LK.getAsset('backgroundImage1', {
- anchorX: 0.5,
- anchorY: 0.5
-});
-initialBgImage.x = 2048 / 2;
-initialBgImage.y = 2732 / 2;
-game.addChildAt(initialBgImage, 0);
-game.background = initialBgImage;
-var statsBar = LK.getAsset('box', {
- anchorX: 0.0,
- anchorY: 0.0,
- width: 2048,
- height: 100,
- x: 0,
- y: 0
-});
-game.addChild(statsBar);
-var roomDisplay = new Text2('Room: 1 | Enemies Left: 0', {
- size: 70,
- fill: 0xFFFFFF,
- fontWeight: "bold"
-});
-roomDisplay.x = 1000;
-roomDisplay.y = 20;
-game.addChild(roomDisplay);
-function updateRoomDisplay() {
- var enemiesLeft = currentRoom.spawnLimit - currentRoom.enemiesKilled;
- roomDisplay.setText('Room: ' + currentRoom.number + ' | Enemies Left: ' + Math.max(enemiesLeft, 0));
- healthText.setText('Health: ' + hero.health);
-}
-function stopRoomMusic() {
- // Stop any playing music regardless of how it was started
- LK.stopMusic();
-}
-function playRoomMusic(roomNumber) {
- // First stop any currently playing music
- stopRoomMusic();
- // Always use LK.playMusic for consistency
- switch (roomNumber) {
- case 1:
- LK.playMusic('room1_music', {
- loop: true
- });
- break;
- case 2:
- LK.playMusic('room2_music', {
- loop: true
- });
- break;
- case 3:
- LK.playMusic('room3_music', {
- loop: true
- });
- break;
- case 4:
- LK.playMusic('room4_music', {
- loop: true
- });
- break;
- case 5:
- LK.playMusic('room5_music', {
- loop: true
- });
- break;
- default:
- // Do nothing for default case
- }
-}
-function checkRoomCleared() {
- if (currentRoom.enemiesKilled >= currentRoom.killGoal && !currentRoom.isCleared) {
- // Play room cleared sound
- var roomClearedSound = LK.getSound('room' + currentRoom.number + '_cleared');
- if (roomClearedSound) {
- roomClearedSound.play();
- }
- // Stop the current room's music
- stopRoomMusic();
- // Mark room as cleared and allow exit
- currentRoom.isCleared = true;
- hero.canExitRoom = true;
- updateRoomDisplay();
- addDirectionalArrows();
- }
-}
-function transitionToNextRoom() {
- // Remove the first call to playRoomMusic which was in the wrong place
- // Clean up the current room
- if (game.background) {
- game.removeChild(game.background);
- }
- // Clear all enemies
- currentRoom.enemies.forEach(function (enemy) {
- enemy.destroy();
- });
- currentRoom.enemies = [];
- currentRoom.enemiesSpawned = 0;
- currentRoom.enemyCounter = currentRoom.spawnLimit;
- currentRoom.enemiesKilled = 0;
- // Track visited rooms using array methods
- if (!visitedRooms.includes(currentRoom.number)) {
- visitedRooms.push(currentRoom.number);
- }
- // Find next room number
- var nextRoomNumber;
- do {
- nextRoomNumber = Math.floor(Math.random() * totalRooms) + 1;
- } while (visitedRooms.includes(nextRoomNumber) && visitedRooms.length < totalRooms);
- if (visitedRooms.length >= totalRooms) {
- // All rooms visited, reset for replayability
- visitedRooms = [];
- }
- // Initialize the new room
- currentRoom = new Room(nextRoomNumber);
- currentRoom.enemiesKilled = 0;
- // Set up background
- var bgImageIndex = (currentRoom.number - 1) % roomBackgroundImages.length;
- var bgImage = LK.getAsset(roomBackgroundImages[bgImageIndex], {
- anchorX: 0.5,
- anchorY: 0.5
- });
- bgImage.x = 2048 / 2;
- bgImage.y = 2732 / 2;
- game.addChildAt(bgImage, 0);
- game.background = bgImage;
- // Set room state
- currentRoom.isCleared = false;
- hero.canExitRoom = false;
- // Update UI
- updateRoomDisplay();
- // Play the appropriate music for the new room
- playRoomMusic(currentRoom.number);
-}
-function transitionToNewRoom(entrySide) {
- // Position hero based on entry side
- if (entrySide === 'top') {
- hero.y = 2732 - 10;
- } else if (entrySide === 'bottom') {
- hero.y = 10;
- } else if (entrySide === 'left') {
- hero.x = 2048 - 10;
- } else if (entrySide === 'right') {
- hero.x = 10;
- }
- // Stop current music before transitioning
- stopRoomMusic();
- // Set up the new room
- transitionToNextRoom();
- // Play music for the new room - already called inside transitionToNextRoom
- hero.canExitRoom = false;
-}
-var hero = gameContainer.addChild(new Hero());
-hero.x = 2048 / 2;
-hero.y = 2732 / 2;
-var healthText = new Text2('Health: ' + hero.health, {
- size: 70,
- fill: 0xFFFFFF,
- fontWeight: "bold"
-});
-healthText.x = 300;
-healthText.y = 20;
-game.addChild(healthText);
-// Movement joystick (left side)
-var joystick = uiContainer.addChild(new Joystick());
-joystick.x = joystick.width / 2 + 100;
-joystick.y = 2732 - joystick.height / 2 - 100;
-hero.joystick = joystick;
-// Aiming joystick (right side)
-var aimingJoystick = uiContainer.addChild(new AimingJoystick());
-aimingJoystick.x = 2048 - aimingJoystick.width / 2 - 100;
-aimingJoystick.y = 2732 - aimingJoystick.height / 2 - 100;
-hero.aimingJoystick = aimingJoystick;
-var killAllButton = LK.getAsset('killAllButton', {
- anchorX: 0.5,
- anchorY: 0.5
-});
-killAllButton.x = 100;
-killAllButton.y = 2732 - 350;
-uiContainer.addChild(killAllButton);
-killAllButton.down = function (x, y, obj) {
- if (isPopupActive || isDataSpikePopupActive) {
- return;
- }
- for (var i = currentRoom.enemies.length - 1; i >= 0; i--) {
- var enemy = currentRoom.enemies[i];
- enemy.destroy();
- currentRoom.enemies.splice(i, 1);
- currentRoom.enemiesKilled++;
- }
- updateRoomDisplay();
- checkRoomCleared();
- updateRoomDisplay();
- checkRoomCleared();
-};
-// Attack button removed - hero now attacks automatically
-var switchWeaponZone = LK.getAsset('switchWeaponZone', {
- anchorX: 0.5,
- anchorY: 0.5
-});
-switchWeaponZone.x = 2048 - switchWeaponZone.width / 2;
-switchWeaponZone.y = 2732 / 2;
-uiContainer.addChild(switchWeaponZone);
-game.down = function (x, y, obj) {
- if (isPopupActive) {
- return;
- }
- if (x >= switchWeaponZone.x - switchWeaponZone.width / 2 && x <= switchWeaponZone.x + switchWeaponZone.width / 2 && y >= switchWeaponZone.y - switchWeaponZone.height / 2 && y <= switchWeaponZone.y + switchWeaponZone.height / 2) {
- hero.weaponType = hero.weaponType === 1 ? 2 : 1;
- hero.swordGraphics.visible = hero.weaponType === 1;
- hero.bowGraphics.visible = hero.weaponType === 2;
- return;
- }
-};
-game.update = function () {
- if (isPopupActive) {
- return;
- }
- // We now handle movement inside hero.update() rather than here
- hero.update();
- if (hero.health <= 0) {
- hero.health = 0;
- LK.showGameOver();
- return;
- }
- if (hero.canExitRoom) {
- if (hero.y < 10 || hero.y > 2732 - 10 || hero.x < 10 || hero.x > 2048 - 10) {
- var entrySide = hero.y < 10 ? 'bottom' : hero.y > 2732 - 10 ? 'top' : hero.x < 10 ? 'right' : 'left';
- transitionToNewRoom(entrySide);
- }
- }
- // Update arrows
- for (var j = arrows.length - 1; j >= 0; j--) {
- arrows[j].update();
- for (var i = currentRoom.enemies.length - 1; i >= 0; i--) {
- var hitDx = currentRoom.enemies[i].x - arrows[j].x;
- var hitDy = currentRoom.enemies[i].y - arrows[j].y;
- var hitDist = Math.sqrt(hitDx * hitDx + hitDy * hitDy);
- if (hitDist < 60) {
- currentRoom.enemies[i].health -= arrows[j].damage;
- if (currentRoom.enemies[i].health <= 0) {
- currentRoom.enemiesKilled++;
- currentRoom.enemyCounter--;
- if (currentRoom.enemies[i].parent) {
- currentRoom.enemies[i].parent.removeChild(currentRoom.enemies[i]);
- }
- currentRoom.enemies[i].destroy();
- currentRoom.enemies.splice(i, 1);
- updateRoomDisplay();
- }
- if (arrows[j]) {
- arrows[j].destroy();
- arrows.splice(j, 1);
- }
- break;
+ },
+ '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];
}
- }
- }
- // Update enemies
- if (!isPopupActive) {
- for (var k = 0; k < currentRoom.enemies.length; k++) {
- currentRoom.enemies[k].update();
- }
- }
- checkRoomCleared();
-};
-function createSkillPopup(skillTitle, skillDescription) {
- var skillPopup = new Container();
- skillPopup.width = 2048;
- skillPopup.height = 2732;
- skillPopup.x = 0;
- skillPopup.y = 0;
- var translucentBackground = LK.getAsset('box', {
- anchorX: 0.5,
- anchorY: 0.5,
- width: 2048,
- height: 2732,
- color: 0x000000,
- alpha: 0.5
- });
- translucentBackground.x = 2048 / 2;
- translucentBackground.y = 2732 / 2;
- skillPopup.addChild(translucentBackground);
- var skillTitleText = new Text2(skillTitle, {
- size: 120,
- fill: 0xFFFFFF,
- fontWeight: "bold",
- fontFamily: "Techno, sans-serif",
- letterSpacing: 2,
- stroke: 0x00FFCC,
- strokeThickness: 3
- });
- skillTitleText.x = 2048 / 2;
- skillTitleText.y = 2732 / 2 + 150 + 50; // Shifted down by 50 pixels
- skillTitleText.anchor.set(0.5, 0.5);
- skillPopup.addChild(skillTitleText);
- var skillDescriptionText = new Text2(skillDescription, {
- size: 80,
- fill: 0xFFFFFF,
- fontFamily: "Arial, sans-serif",
- letterSpacing: 1,
- stroke: 0x00FFCC,
- strokeThickness: 2,
- wordWrap: true,
- wordWrapWidth: 1800
- });
- skillDescriptionText.x = 2048 / 2;
- skillDescriptionText.y = skillTitleText.y + 300; // Position relative to shifted title
- skillDescriptionText.anchor.set(0.5, 0.5);
- skillPopup.addChild(skillDescriptionText);
- // Add skill point allocation counter if this skill has allocation tracking
- if (skillPointAllocations[skillTitle]) {
- var points = skillPointAllocations[skillTitle].points;
- var maxPoints = skillPointAllocations[skillTitle].max;
- var allocationText = new Text2(points + '/' + maxPoints, {
- size: 80,
- fill: 0xFFFFFF,
- fontWeight: "bold",
- stroke: 0x00FFCC,
- strokeThickness: 2
- });
- allocationText.x = 2048 / 2;
- allocationText.y = skillTitleText.y + 500; // Position below description
- allocationText.anchor.set(0.5, 0.5);
- allocationText.name = skillTitle + "_allocation"; // Add a name for easier finding
- skillPopup.addChild(allocationText);
- }
- var exitButton = LK.getAsset('cancelButton', {
- anchorX: 0.5,
- anchorY: 0.5,
- scaleX: 3,
- scaleY: 3
- });
- exitButton.x = 2048 / 2 - 600;
- exitButton.y = 2732 / 2 + 950; // Shifted down by 100 pixels
- exitButton.down = function () {
- menuContainer.removeChild(skillPopup);
- isDataSpikePopupActive = false;
- };
- skillPopup.addChild(exitButton);
- var spendButton = LK.getAsset('spendButton', {
- anchorX: 0.5,
- anchorY: 0.5,
- scaleX: 3,
- scaleY: 3
- });
- spendButton.x = 2048 / 2 + 600;
- spendButton.y = 2732 / 2 + 950; // Shifted down by 100 pixels
- function refreshSkillPointsText() {
- if (typeof skillPointsDisplay !== 'undefined') {
- skillPointsDisplay.setText('Skill Points: ' + heroSkillPoints);
- }
- if (typeof skillPointsText !== 'undefined') {
- skillPointsText.setText('Skill Points: ' + heroSkillPoints);
- }
- }
- spendButton.down = function () {
- if (heroSkillPoints > 0 && skillPointAllocations[skillTitle] && skillPointAllocations[skillTitle].points < skillPointAllocations[skillTitle].max) {
- heroSkillPoints--;
- refreshSkillPointsText(); // Update text after spending
- refreshTreePointsText(); // Update tree points text if visible
- // Increment the skill's points
- skillPointAllocations[skillTitle].points++;
- // Update the current popup to show the new point allocation
- var pointsElement = skillPopup.children.find(function (child) {
- return child instanceof Text2 && child.text && child.text.includes && child.text.includes('/') && child.y > skillPopup.height / 2;
- });
- if (pointsElement) {
- var points = skillPointAllocations[skillTitle].points;
- var maxPoints = skillPointAllocations[skillTitle].max;
- pointsElement.setText(points + '/' + maxPoints);
- }
- // Update all skill node point displays in any open skill tree
- updateSkillNodePointDisplays();
- // Handle skills with diminishing returns descriptions
- if (skillTitle === 'Data Spike') {
- dataSpikePoints = skillPointAllocations[skillTitle].points;
- // Close this popup and reopen it with updated description
- menuContainer.removeChild(skillPopup);
- isDataSpikePopupActive = false;
- displayDataSpikePopup();
- return; // Exit early since we're reopening the popup
- } else if (skillTitle === 'Reflex Accelerator') {
- // Close this popup and reopen it with updated description
- menuContainer.removeChild(skillPopup);
- isDataSpikePopupActive = false;
- displayReflexAcceleratorPopup();
- return; // Exit early since we're reopening the popup
- } else if (skillTitle === 'Overclock Core') {
- // Close this popup and reopen it with updated description
- menuContainer.removeChild(skillPopup);
- isDataSpikePopupActive = false;
- displayOverclockCorePopup();
- return; // Exit early since we're reopening the popup
- } else if (skillTitle === 'Neural Hijack') {
- // Close this popup and reopen it with updated description
- menuContainer.removeChild(skillPopup);
- isDataSpikePopupActive = false;
- displayNeuralHijackPopup();
- return; // Exit early since we're reopening the popup
- } else if (skillTitle === 'Dodge Matrix') {
- // Close this popup and reopen it with updated description
- menuContainer.removeChild(skillPopup);
- isDataSpikePopupActive = false;
- displayDodgeMatrixPopup();
- return; // Exit early since we're reopening the popup
- } else if (skillTitle === 'Power Surge') {
- // Close this popup and reopen it with updated description
- menuContainer.removeChild(skillPopup);
- isDataSpikePopupActive = false;
- displayPowerSurgePopup();
- return; // Exit early since we're reopening the popup
- }
- // For all other skills, just update the counter
- menuContainer.removeChild(skillPopup);
- isDataSpikePopupActive = false;
- // Re-create the popup with the updated skill points count
- // Dynamically call the correct popup function for Echoes of Ancestry skills
- if (skillTitle === 'Ancient Wisdom') {
- displayAncientWisdomPopup();
- } else if (skillTitle === 'Elemental Mastery') {
- displayElementalMasteryPopup();
- } else if (skillTitle === 'Warrior Spirit') {
- displayWarriorSpiritPopup();
- } else if (skillTitle === 'Beast Tamer') {
- displayBeastTamerPopup();
- } else if (skillTitle === 'Herbal Remedies') {
- displayHerbalRemediesPopup();
- } else if (skillTitle === 'Spirit Guidance') {
- displaySpiritGuidancePopup();
- } else if (skillTitle === 'Legendary Ancestor') {
- displayLegendaryAncestorPopup();
+ 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 {
- // fallback for all other skills
- var updatedPopup = createSkillPopup(skillTitle, skillDescription);
- menuContainer.addChild(updatedPopup);
+ return 'Maximum rank reached. Your movement speed is increased by 30%.';
}
- } else {
- // No skill points left, optionally do nothing
}
- };
- skillPopup.addChild(spendButton);
- var skillPointsText = new Text2('Skill Points: ' + heroSkillPoints, {
- size: 70,
- fill: 0xFFFFFF,
- fontWeight: "bold"
- });
- skillPointsText.x = 2048 / 2;
- skillPointsText.y = 2732 - 100; // bottom area of popup
- skillPointsText.anchor.set(0.5, 0);
- skillPopup.addChild(skillPointsText);
- return skillPopup;
-}
-function displayOverloadBlastPopup() {
- var skillName = 'Overload Blast';
- var points = skillPointAllocations[skillName] ? skillPointAllocations[skillName].points : 0;
- var maxPoints = skillPointAllocations[skillName] ? skillPointAllocations[skillName].max : 1;
- var description = '';
- if (points < maxPoints) {
- description = 'Unleashes a massive explosion, dealing 300% fire damage to all enemies in the room (Cooldown: 60s).';
- } else {
- description = 'Maximum rank reached. Unleashes a massive explosion, dealing 300% fire damage to all enemies in the room (Cooldown: 60s).';
- }
- var overloadBlastPopup = createSkillPopup(skillName, description);
- menuContainer.addChild(overloadBlastPopup);
- isDataSpikePopupActive = true;
-}
-function displayPowerSurgePopup() {
- var skillName = 'Power Surge';
- var points = skillPointAllocations[skillName].points;
- var maxPoints = skillPointAllocations[skillName].max;
- // Define scaling values for each rank
- var damageMultipliers = [1.0, 1.25, 1.5, 1.75, 2.0]; // Damage multiplier at each rank
- var radiusSizes = [2.0, 2.5, 3.0, 3.5, 4.0]; // Radius size at each rank
- // Calculate current benefits based on points already allocated
- var currentDamageMultiplier = 0;
- var currentRadius = 0;
- if (points > 0) {
- currentDamageMultiplier = damageMultipliers[points - 1];
- currentRadius = radiusSizes[points - 1];
- }
- // Prepare description based on points allocated
- var description = '';
- if (points < maxPoints) {
- var nextDamageMultiplier = damageMultipliers[points];
- var nextRadius = radiusSizes[points];
- if (points === 0) {
- description = 'Releases an energy burst dealing 100% magic damage in a 2.0-unit radius.';
- } else {
- description = 'Deals ' + nextDamageMultiplier * 100 + '% magic damage in a ' + nextRadius + '-unit radius (Currently ' + currentDamageMultiplier * 100 + '% / ' + currentRadius + ')';
- }
- } else {
- description = 'Maximum rank reached. Your energy burst deals 175% magic damage in a 3.5-unit radius.';
- }
- var powerSurgePopup = createSkillPopup(skillName, description);
- menuContainer.addChild(powerSurgePopup);
- isDataSpikePopupActive = true;
-}
-function displayDodgeMatrixPopup() {
- var skillName = 'Dodge Matrix';
- var points = skillPointAllocations[skillName].points;
- var maxPoints = skillPointAllocations[skillName].max;
- // Define diminishing returns values
- var dodgeIncreases = [10, 8, 6, 4, 2]; // Diminishing returns
- // Calculate current benefit based on points already allocated
- var currentBenefit = 0;
- for (var i = 0; i < points; i++) {
- currentBenefit += dodgeIncreases[i];
- }
- // Prepare description based on points allocated
- var description = '';
- if (points < maxPoints) {
- var nextIncrease = dodgeIncreases[points];
- if (points === 0) {
- description = 'Grants a 10% chance to dodge enemy attacks.';
- } else {
- description = 'Grants an ' + nextIncrease + '% chance to dodge enemy attacks (Currently ' + currentBenefit + '%)';
- }
- } else {
- description = 'Maximum rank reached. Your chance to dodge enemy attacks is increased by 30%.';
- }
- var dodgeMatrixPopup = createSkillPopup(skillName, description);
- menuContainer.addChild(dodgeMatrixPopup);
- isDataSpikePopupActive = true;
-}
-function displayNeuralHijackPopup() {
- var skillName = 'Neural Hijack';
- var points = skillPointAllocations[skillName].points;
- var maxPoints = skillPointAllocations[skillName].max;
- // Define diminishing returns values
- var durationIncreases = [3, 4, 5, 6, 8]; // Seconds duration
- var cooldownDecreases = [30, 27, 24, 21, 15]; // Seconds cooldown
- // Calculate current benefits based on points already allocated
- var currentDuration = 0;
- var currentCooldown = 0;
- if (points > 0) {
- currentDuration = durationIncreases[points - 1];
- currentCooldown = cooldownDecreases[points - 1];
- }
- // Prepare description based on points allocated
- var description = '';
- if (points < maxPoints) {
- var nextDuration = durationIncreases[points];
- var nextCooldown = cooldownDecreases[points];
- if (points === 0) {
- description = 'Temporarily converts an enemy to fight for you for ' + nextDuration + ' seconds (Cooldown: ' + nextCooldown + 's).';
- } else {
- description = 'Converts an enemy for ' + nextDuration + ' seconds (Cooldown: ' + nextCooldown + 's) (Currently ' + currentDuration + 's / ' + currentCooldown + 's).';
- }
- } else {
- description = 'Maximum rank reached. Converts an enemy to fight for you for 8 seconds with a 15 second cooldown.';
- }
- var neuralHijackPopup = createSkillPopup(skillName, description);
- menuContainer.addChild(neuralHijackPopup);
- isDataSpikePopupActive = true;
-}
-function displayOverclockCorePopup() {
- var skillName = 'Overclock Core';
- var points = skillPointAllocations[skillName].points;
- var maxPoints = skillPointAllocations[skillName].max;
- // Define diminishing returns values
- var attackSpeedIncreases = [2.5, 2.0, 1.5, 1.0, 0.5]; // Diminishing returns for attack speed
- var energyRegenIncreases = [2.0, 1.6, 1.2, 0.8, 0.4]; // Diminishing returns for energy regen
- // Calculate current benefits based on points already allocated
- var currentAttackSpeedBenefit = 0;
- var currentEnergyRegenBenefit = 0;
- for (var i = 0; i < points; i++) {
- currentAttackSpeedBenefit += attackSpeedIncreases[i];
- currentEnergyRegenBenefit += energyRegenIncreases[i];
- }
- // Prepare description based on points allocated
- var description = '';
- if (points < maxPoints) {
- var nextAttackSpeedIncrease = attackSpeedIncreases[points];
- var nextEnergyRegenIncrease = energyRegenIncreases[points];
- if (points === 0) {
- description = 'Increases attack speed by ' + nextAttackSpeedIncrease + '% and energy regeneration by ' + nextEnergyRegenIncrease + '%.';
- } else {
- description = 'Increases attack speed by ' + nextAttackSpeedIncrease + '% and energy regeneration by ' + nextEnergyRegenIncrease + '% (Currently ' + currentAttackSpeedBenefit.toFixed(1) + '% / ' + currentEnergyRegenBenefit.toFixed(1) + '%).';
- }
- } else {
- description = 'Maximum rank reached. Your attack speed is increased by 7.5% and energy regeneration by 6.0%.';
- }
- var overclockCorePopup = createSkillPopup(skillName, description);
- menuContainer.addChild(overclockCorePopup);
- isDataSpikePopupActive = true;
-}
-function displayReflexAcceleratorPopup() {
- var skillName = 'Reflex Accelerator';
- var points = skillPointAllocations[skillName].points;
- var maxPoints = skillPointAllocations[skillName].max;
- // Define diminishing returns values
- var speedIncreases = [10, 8, 6, 4, 2]; // Diminishing returns
- // Calculate current benefit based on points already allocated
- var currentBenefit = 0;
- for (var i = 0; i < points; i++) {
- currentBenefit += speedIncreases[i];
- }
- // Prepare description based on points allocated
- var description = '';
- if (points < maxPoints) {
- var nextIncrease = speedIncreases[points];
- if (points === 0) {
- description = 'Boosts movement speed by 10% for the first point.';
- } else {
- description = 'Boosts movement speed by ' + nextIncrease + '% (Currently ' + currentBenefit + '%)';
- }
- } else {
- description = 'Maximum rank reached. Your movement speed is increased by 30%.';
- }
- var reflexAcceleratorPopup = createSkillPopup(skillName, description);
- menuContainer.addChild(reflexAcceleratorPopup);
- isDataSpikePopupActive = true;
-}
-// Track skill point allocation for all skills
-var dataSpikePoints = 0;
-var dataSpikeMaxPoints = 5;
-var dataSpikeIncreases = [5, 4, 3, 2, 1]; // Diminishing returns
-// Track skill point allocations for all skill trees
-var skillPointAllocations = {
- // Circuit of Ascension
- 'Data Spike': {
- points: 0,
- max: 5
},
- 'Reflex Accelerator': {
- points: 0,
- max: 5
- },
'Overclock Core': {
points: 0,
- max: 5
+ 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
+ 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
+ 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
+ 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
+ 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
+ // --- Echoes of Ancestry ---
'Ancient Wisdom': {
points: 0,
- max: 5
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ // --- Forge of Possibilities ---
'Adaptive Construction': {
points: 0,
- max: 5
+ 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
+ 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
+ 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
+ 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: 5
+ 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
+ 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: 5
+ 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
+ // --- Prism of Potential --- (Assuming all max 5, simple descriptions for now)
'Red Radiance': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Prism of Potential',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Channel the crimson energy of aggression. Increases your attack damage slightly and grants a chance for attacks to ignite enemies, causing minor burn damage over time.';
+ }
},
'Blue Bulwark': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Prism of Potential',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Embrace cerulean hues of defense. Enhances your armor and magic resistance for a short duration, reducing incoming damage and occasionally shielding you with a barrier of ice.';
+ }
},
'Green Growth': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Prism of Potential',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Tap into verdant life essence. Gradually regenerates a portion of your health and grants a chance for attacks to apply a minor heal-over-time effect to yourself or nearby allies.';
+ }
},
'Yellow Zephyr': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Prism of Potential',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Harness the swiftness of golden winds. Boosts your movement speed slightly and improves your bow’s draw rate or sword swing speed, letting you dart around combat swiftly.';
+ }
},
'Indigo Insight': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Prism of Potential',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Draw from deep indigo wisdom. Grants increased critical hit chance and a small probability to reflect a portion of incoming damage back at your attacker, showcasing keen foresight.';
+ }
},
'Chromatic Fusion': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Prism of Potential',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Blend two chosen elemental colors (e.g., Red and Blue) to create a hybrid effect. Depending on the combination, gain unique bonuses, such as fiery storms or frostburn effects, strengthening both offense and defense.';
+ }
},
'Prismatic Ascendance': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Prism of Potential',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Briefly harness all hues at once, greatly enhancing elemental power. Attacks may inflict multiple ailments, and you gain increased damage, defense, healing, and speed.';
+ }
},
- // Nexus of Chaos
+ // --- Nexus of Chaos --- (Assuming all max 5, simple descriptions for now)
'Chaotic Spark': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Nexus of Chaos',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Your attacks sometimes produce random elemental effects. Damage types and minor status effects vary unpredictably, keeping foes off-balance.';
+ }
},
'Entropy Shield': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Nexus of Chaos',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Surround yourself with shifting energy fields. Each incoming hit has a chance to fizzle harmlessly or rebound, dealing random damage back to attackers.';
+ }
},
'Unstable Rift': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Nexus of Chaos',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Occasionally summon a pocket of chaos in battle. Enemies near the rift suffer random debuffs—slowing, poisoning, burning—ever-changing, never stable.';
+ }
},
'Anomaly Shift': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Nexus of Chaos',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Briefly warp your form. Gain random beneficial buffs—from speed and regeneration to critical chance—but each application is uncertain, never guaranteed.';
+ }
},
'Cascade of Disorder': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Nexus of Chaos',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Trigger a chain reaction of volatile energies. Attacks may ignite unpredictable chain effects, spreading to multiple enemies or amplifying certain strikes.';
+ }
},
'Fractured Fate': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Nexus of Chaos',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Tap into temporal chaos. Sometimes attacks hit twice, or cooldowns shorten unexpectedly. Fate bends momentarily, granting you unpredictable bursts of advantage.';
+ }
},
'Reality Collapse': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Nexus of Chaos',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'For a short time, unravel normal rules. Attacks ignore some defenses, enemies may lose control, and your random benefits intensify. Chaos reigns supreme, leaving foes disoriented and vulnerable.';
+ }
},
- // Symphony of Shadows
+ // --- Symphony of Shadows --- (Assuming all max 5, simple descriptions for now)
'Whispered Steps': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Symphony of Shadows',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Move silently, becoming harder to detect. Occasionally evade attacks entirely, as your footsteps blend into the ambient darkness.';
+ }
},
'Nightfall Veil': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Symphony of Shadows',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Briefly cloak yourself in shadow. While cloaked, project a faint silhouette to mislead foes. Attacks aimed at you may strike only empty air.';
+ }
},
'Serrated Shade': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Symphony of Shadows',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Enhance your weapons with shadowy edges. Attacks may lacerate enemies, causing bleed-like damage over time as the darkness seeps into their wounds.';
+ }
},
'Shadow Familiar': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Symphony of Shadows',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Summon a small, dark entity that assists you. It distracts enemies, occasionally nipping at their heels and granting you openings to strike.';
+ }
},
'Muffled Strikes': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Symphony of Shadows',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Attacks produce little sound, reducing enemy awareness. Some foes fail to fully react, taking extra critical hits as they struggle to track your movements.';
+ }
},
'Umbral Echoes': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Symphony of Shadows',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'Your attacks leave lingering echoes of shadow. These echoes confuse enemies, causing them to strike illusions or hesitate, giving you more control in battle.';
+ }
},
'Midnight Crescendo': {
points: 0,
- max: 5
+ max: 5,
+ tree: 'The Symphony of Shadows',
+ descriptionFn: function descriptionFn(p, d) {
+ return 'For a short duration, become one with the darkness. Your attacks ignore some armor, movement speed increases, and enemies are overwhelmed by disorienting echoes. A swirling concert of shadows amplifies your lethality.';
+ }
}
};
-function displayDataSpikePopup() {
- var currentBenefit = 0;
- // Calculate current benefit based on points allocated
- for (var i = 0; i < dataSpikePoints; i++) {
- currentBenefit += dataSpikeIncreases[i];
+// --- Centralized Skill Tree Layout Data ---
+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: {
+ '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: { 'Data Spike': 'data_spike_icon', ... } // Add actual icon asset IDs here if defined
+ },
+ 'The Echoes of Ancestry': {
+ backgroundAsset: 'popup_echoesofancestry',
+ nodes: ['Ancient Wisdom', 'Elemental Mastery', 'Warrior Spirit', 'Beast Tamer', 'Herbal Remedies', 'Spirit Guidance', 'Legendary Ancestor'],
+ positions: {
+ '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: { ... }
+ },
+ 'The Forge of Possibilities': {
+ backgroundAsset: 'popup_forgeofpossibilities',
+ nodes: ['Adaptive Construction', 'Elemental Infusion', 'Modular Enhancement', 'Resourceful Salvage', 'Hybrid Crafting', 'Runic Slots', 'Masterwork Creation'],
+ positions: {
+ '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: { ... }
+ },
+ 'The Prism of Potential': {
+ backgroundAsset: 'popup_prismofpotential',
+ nodes: ['Red Radiance', 'Blue Bulwark', 'Green Growth', 'Yellow Zephyr', 'Indigo Insight', 'Chromatic Fusion', 'Prismatic Ascendance'],
+ positions: {
+ '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: { ... }
+ },
+ 'The Nexus of Chaos': {
+ backgroundAsset: 'popup_nexusofchaos',
+ nodes: ['Chaotic Spark', 'Entropy Shield', 'Unstable Rift', 'Anomaly Shift', 'Cascade of Disorder', 'Fractured Fate', 'Reality Collapse'],
+ positions: {
+ '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: { ... }
+ },
+ 'The Symphony of Shadows': {
+ backgroundAsset: 'popup_symphonyofshadows',
+ nodes: ['Whispered Steps', 'Nightfall Veil', 'Serrated Shade', 'Shadow Familiar', 'Muffled Strikes', 'Umbral Echoes', 'Midnight Crescendo'],
+ positions: {
+ '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
+ },
+ 'Muffled Strikes': {
+ 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: { ... }
}
- // Show next increase if not maxed
- var description = '';
- if (dataSpikePoints < dataSpikeMaxPoints) {
- var nextIncrease = dataSpikeIncreases[dataSpikePoints];
- if (dataSpikePoints === 0) {
- description = 'Increases critical strike chance by 5%';
+};
+/****
+* 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;
+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);
+ // 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);
+ // 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;
+ spendButton.down = function () {
+ if (skillData && 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 ---
+ var descTextElement = skillPopup.getChildByName("skillDescriptionText");
+ var allocTextElement = skillPopup.getChildByName("allocationText");
+ if (descTextElement && skillData.descriptionFn) {
+ var newDescription = skillData.descriptionFn(skillData.points, skillData.calcData);
+ descTextElement.setText(newDescription);
+ // Recalculate position if height changes significantly? Might not be needed for most cases.
+ allocTextElement.y = descTextElement.y + descTextElement.height / 2 + 100; // Adjust allocation text pos
+ }
+ if (allocTextElement) {
+ allocTextElement.setText(skillData.points + '/' + skillData.max);
+ }
+ // Update the points on the node behind this popup immediately
+ updateSkillNodePointDisplays();
} else {
- description = 'Increases critical strike chance by ' + nextIncrease + '%. (Currently ' + currentBenefit + '%)';
+ LK.log("Cannot spend point: Not enough points or max rank reached.");
+ // 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) {
+ LK.log('Error: Skill data not found for ' + skillName);
+ 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
+function createSkillTreeNode(skillName, position, parentPopup) {
+ var skillData = skillDefinitions[skillName];
+ if (!skillData) {
+ return;
+ }
+ // --- Icon Placeholder ---
+ // var iconAssetId = skillTreeLayouts[skillData.tree]?.icons?.[skillName] || 'glowing_line_asset'; // Use specific icon if available
+ var iconAssetId = 'glowing_line_asset'; // Placeholder
+ var skillNodeGraphic = LK.getAsset(iconAssetId, {
+ anchorX: 0.5,
+ anchorY: 0.5
+ // scaleX: 1, scaleY: 1, // Use default scale or adjust as needed
+ // width: 100, height: 100, // Use asset's default size or override
+ });
+ // Apply color only if it's the placeholder shape
+ if (iconAssetId === 'glowing_line_asset') {
+ skillNodeGraphic.color = 0xc05bf1;
+ skillNodeGraphic.width = 80; // Adjust size
+ skillNodeGraphic.height = 80;
} else {
- description = 'Maximum rank reached. Your critical hit chance is increased by 15%.';
+ // Optional: Scale real icons if needed
+ skillNodeGraphic.scale.set(3);
}
- var dataSpikePopup = createSkillPopup('Data Spike', description);
- menuContainer.addChild(dataSpikePopup);
- isDataSpikePopupActive = true;
+ skillNodeGraphic.x = position.x;
+ skillNodeGraphic.y = position.y;
+ skillNodeGraphic.interactive = true;
+ // --- Points Text ---
+ var points = skillData.points;
+ var maxPoints = skillData.max;
+ var pointsText = new Text2(points + '/' + maxPoints, {
+ size: 50,
+ fill: 0xFFFFFF,
+ fontWeight: "bold",
+ stroke: 0x000000,
+ strokeThickness: 4
+ });
+ pointsText.x = position.x;
+ pointsText.y = position.y + 60; // Offset below node (adjust based on icon size)
+ pointsText.anchor.set(0.5, 0);
+ pointsText.name = skillName + "_pointsText"; // Unique name for updates
+ // --- Click Handler ---
+ skillNodeGraphic.down = function () {
+ displaySkillDetailPopup(skillName); // Call the single generic display function
+ // Visual feedback (Flash)
+ var originalTint = skillNodeGraphic.tint;
+ skillNodeGraphic.tint = 0xFFFF00; // Yellow flash
+ LK.setTimeout(function () {
+ skillNodeGraphic.tint = originalTint;
+ }, 150);
+ };
+ parentPopup.addChild(skillNodeGraphic);
+ parentPopup.addChild(pointsText);
}
+// Opens the specific skill tree view popup
function openSkillTreePopup(skillTreeName) {
- menuContainer.removeChild(skillTreePopup);
+ menuContainer.removeChild(skillTreePopup); // Remove main selection screen
+ var layoutData = skillTreeLayouts[skillTreeName];
+ if (!layoutData) {
+ LK.log("Error: Layout data not found for " + skillTreeName);
+ menuContainer.addChild(skillTreePopup); // Go back to main selection
+ return;
+ }
var newPopup = new Container();
- newPopup.width = 2048;
- newPopup.height = 2732;
- newPopup.x = 0;
- newPopup.y = 0;
- var newPopupBackground = LK.getAsset('skillTree_Background', {
+ 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 = 2048 / 2;
- newPopupBackground.y = 2732 / 2;
+ newPopupBackground.x = roomWidth / 2;
+ newPopupBackground.y = roomHeight / 2;
newPopup.addChild(newPopupBackground);
- if (skillTreeName === 'The Circuit of Ascension') {
- var circuitAsset = LK.getAsset('popup_circuitofascension', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- circuitAsset.x = 2048 / 2;
- circuitAsset.y = 2732 / 2;
- newPopup.addChild(circuitAsset);
- var backButton = new BackButton();
- newPopup.addChild(backButton);
- var skillNodes = [{
- id: 'data_spike_icon',
- x: 2048 / 4 - 50,
- y: 2732 / 6 - 50 + 100,
- name: 'Data Spike'
- }, {
- id: 'reflex_accelerator_icon',
- x: 2048 / 2,
- y: 2732 / 6 - 50 + 100,
- name: 'Reflex Accelerator'
- }, {
- id: 'overclock_core_icon',
- x: 3 * 2048 / 4 + 50,
- y: 2732 / 6 - 50 + 100,
- name: 'Overclock Core'
- }, {
- id: 'neural_hijack_icon',
- x: 2048 / 4 - 50,
- y: 2 * 2732 / 6 + 100,
- name: 'Neural Hijack'
- }, {
- id: 'dodge_matrix_icon',
- x: 2048 / 2,
- y: 2 * 2732 / 6 + 100,
- name: 'Dodge Matrix'
- }, {
- id: 'power_surge_icon',
- x: 3 * 2048 / 4 + 50,
- y: 2 * 2732 / 6 + 100,
- name: 'Power Surge'
- }, {
- id: 'overload_blast_icon',
- x: 2048 / 2,
- y: 3 * 2732 / 6 + 50 + 100,
- name: 'Overload Blast'
- }];
- skillNodes.forEach(function (node) {
- var skillNode = LK.getAsset(node.id, {
- anchorX: 0.5,
- anchorY: 0.5,
- scaleX: 3,
- scaleY: 3
- });
- skillNode.x = node.x;
- skillNode.y = node.y;
- // Add skill point allocation text below each node
- var points = skillPointAllocations[node.name] ? skillPointAllocations[node.name].points : 0;
- var maxPoints = skillPointAllocations[node.name] ? skillPointAllocations[node.name].max : 5;
- var pointsText = new Text2(points + '/' + maxPoints, {
- size: 60,
- fill: 0xFFFFFF,
- fontWeight: "bold"
- });
- pointsText.x = node.x;
- pointsText.y = node.y + 150;
- pointsText.anchor.set(0.5, 0);
- pointsText.alpha = 1; // Ensure text is visible and large enough
- newPopup.addChild(pointsText);
- // Create a node representation for the skillNode
- var nodeCircle = LK.getAsset('glowing_line_asset', {
- anchorX: 0.5,
- anchorY: 0.5,
- width: 60,
- height: 60,
- color: 0xc05bf1
- });
- nodeCircle.x = node.x;
- nodeCircle.y = node.y;
- newPopup.addChildAt(nodeCircle, 0); // Add below the skill icon
- skillNode.down = function () {
- if (isDataSpikePopupActive) {
- return;
- }
- if (node.name === 'Data Spike') {
- displayDataSpikePopup();
- isDataSpikePopupActive = true;
- } else if (node.name === 'Overclock Core') {
- displayOverclockCorePopup();
- isDataSpikePopupActive = true;
- } else if (node.name === 'Reflex Accelerator') {
- displayReflexAcceleratorPopup();
- isDataSpikePopupActive = true;
- } else if (node.name === 'Neural Hijack') {
- displayNeuralHijackPopup();
- isDataSpikePopupActive = true;
- } else if (node.name === 'Dodge Matrix') {
- displayDodgeMatrixPopup();
- isDataSpikePopupActive = true;
- } else if (node.name === 'Power Surge') {
- displayPowerSurgePopup();
- isDataSpikePopupActive = true;
- } else if (node.name === 'Overload Blast') {
- displayOverloadBlastPopup();
- isDataSpikePopupActive = true;
- }
- var originalColor = skillNode.tint;
- skillNode.tint = 0xffff00;
- LK.setTimeout(function () {
- skillNode.tint = originalColor;
- }, 1000);
- };
- newPopup.addChild(skillNode);
- });
- } else if (skillTreeName === 'The Echoes of Ancestry') {
- var ancestryAsset = LK.getAsset('popup_echoesofancestry', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- ancestryAsset.x = 2048 / 2;
- ancestryAsset.y = 2732 / 2;
- newPopup.addChild(ancestryAsset);
- var backButton = new BackButton();
- newPopup.addChild(backButton);
- var skillNodes = [{
- id: 'ancient_wisdom_icon',
- x: 2048 / 4 - 50,
- y: 2732 / 6 - 50 + 100,
- name: 'Ancient Wisdom'
- }, {
- id: 'elemental_mastery_icon',
- x: 2048 / 2,
- y: 2732 / 6 - 50 + 100,
- name: 'Elemental Mastery'
- }, {
- id: 'warrior_spirit_icon',
- x: 3 * 2048 / 4 + 50,
- y: 2732 / 6 - 50 + 100,
- name: 'Warrior Spirit'
- }, {
- id: 'beast_tamer_icon',
- x: 2048 / 4 - 50,
- y: 2 * 2732 / 6 + 100,
- name: 'Beast Tamer'
- }, {
- id: 'herbal_remedies_icon',
- x: 2048 / 2,
- y: 2 * 2732 / 6 + 100,
- name: 'Herbal Remedies'
- }, {
- id: 'spirit_guidance_icon',
- x: 3 * 2048 / 4 + 50,
- y: 2 * 2732 / 6 + 100,
- name: 'Spirit Guidance'
- }, {
- id: 'legendary_ancestor_icon',
- x: 2048 / 2,
- y: 3 * 2732 / 6 + 50 + 100,
- name: 'Legendary Ancestor'
- }];
- skillNodes.forEach(function (node) {
- var skillNode = LK.getAsset(node.id, {
- anchorX: 0.5,
- anchorY: 0.5,
- scaleX: 3,
- scaleY: 3
- });
- skillNode.x = node.x;
- skillNode.y = node.y;
- // Add skill point allocation text below each node
- var points = skillPointAllocations[node.name] ? skillPointAllocations[node.name].points : 0;
- var maxPoints = skillPointAllocations[node.name] ? skillPointAllocations[node.name].max : 5;
- var pointsText = new Text2(points + '/' + maxPoints, {
- size: 60,
- fill: 0xFFFFFF,
- fontWeight: "bold"
- });
- pointsText.x = node.x;
- pointsText.y = node.y + 150;
- pointsText.anchor.set(0.5, 0);
- pointsText.alpha = 1; // Ensure text is visible
- newPopup.addChild(pointsText);
- // Create a node representation for the skillNode
- var nodeCircle = LK.getAsset('glowing_line_asset', {
- anchorX: 0.5,
- anchorY: 0.5,
- width: 60,
- height: 60,
- color: 0xc05bf1
- });
- nodeCircle.x = node.x;
- nodeCircle.y = node.y;
- newPopup.addChildAt(nodeCircle, 0); // Add below the skill icon
- skillNode.down = function () {
- if (isDataSpikePopupActive) {
- return;
- }
- if (node.name === 'Ancient Wisdom') {
- displayAncientWisdomPopup();
- } else if (node.name === 'Elemental Mastery') {
- displayElementalMasteryPopup();
- } else if (node.name === 'Warrior Spirit') {
- displayWarriorSpiritPopup();
- } else if (node.name === 'Beast Tamer') {
- displayBeastTamerPopup();
- } else if (node.name === 'Herbal Remedies') {
- displayHerbalRemediesPopup();
- } else if (node.name === 'Spirit Guidance') {
- displaySpiritGuidancePopup();
- } else if (node.name === 'Legendary Ancestor') {
- displayLegendaryAncestorPopup();
- }
- isDataSpikePopupActive = true;
- };
- newPopup.addChild(skillNode);
- });
- } else if (skillTreeName === 'The Forge of Possibilities') {
- var forgeAsset = LK.getAsset('popup_forgeofpossibilities', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- forgeAsset.x = 2048 / 2;
- forgeAsset.y = 2732 / 2;
- newPopup.addChild(forgeAsset);
- var backButton = new BackButton();
- newPopup.addChild(backButton);
- var skillNodes = [{
- id: 'adaptive_construction_icon',
- x: 2048 / 4 - 50,
- y: 2732 / 6 - 50 + 100,
- name: 'Adaptive Construction'
- }, {
- id: 'elemental_infusion_icon',
- x: 2048 / 2,
- y: 2732 / 6 - 50 + 100,
- name: 'Elemental Infusion'
- }, {
- id: 'modular_enhancement_icon',
- x: 3 * 2048 / 4 + 50,
- y: 2732 / 6 - 50 + 100,
- name: 'Modular Enhancement'
- }, {
- id: 'resourceful_salvage_icon',
- x: 2048 / 4 - 50,
- y: 2 * 2732 / 6 + 100,
- name: 'Resourceful Salvage'
- }, {
- id: 'hybrid_crafting_icon',
- x: 2048 / 2,
- y: 2 * 2732 / 6 + 100,
- name: 'Hybrid Crafting'
- }, {
- id: 'runic_slots_icon',
- x: 3 * 2048 / 4 + 50,
- y: 2 * 2732 / 6 + 100,
- name: 'Runic Slots'
- }, {
- id: 'masterwork_creation_icon',
- x: 2048 / 2,
- y: 3 * 2732 / 6 + 50 + 100,
- name: 'Masterwork Creation'
- }];
- skillNodes.forEach(function (node) {
- var skillNode = LK.getAsset(node.id, {
- anchorX: 0.5,
- anchorY: 0.5,
- scaleX: 3,
- scaleY: 3
- });
- skillNode.x = node.x;
- skillNode.y = node.y;
- // Add skill point allocation text below each node
- var points = skillPointAllocations[node.name] ? skillPointAllocations[node.name].points : 0;
- var maxPoints = skillPointAllocations[node.name] ? skillPointAllocations[node.name].max : 5;
- var pointsText = new Text2(points + '/' + maxPoints, {
- size: 60,
- fill: 0xFFFFFF,
- fontWeight: "bold"
- });
- pointsText.x = node.x;
- pointsText.y = node.y + 150;
- pointsText.anchor.set(0.5, 0);
- pointsText.alpha = 1; // Ensure text is visible
- newPopup.addChild(pointsText);
- // Create a node representation for the skillNode
- var nodeCircle = LK.getAsset('glowing_line_asset', {
- anchorX: 0.5,
- anchorY: 0.5,
- width: 60,
- height: 60,
- color: 0xc05bf1
- });
- nodeCircle.x = node.x;
- nodeCircle.y = node.y;
- newPopup.addChildAt(nodeCircle, 0); // Add below the skill icon
- skillNode.down = function () {
- if (isDataSpikePopupActive) {
- return;
- }
- if (node.name === 'Adaptive Construction') {
- displayAdaptiveConstructionPopup();
- } else if (node.name === 'Elemental Infusion') {
- displayElementalInfusionPopup();
- } else if (node.name === 'Modular Enhancement') {
- displayModularEnhancementPopup();
- } else if (node.name === 'Resourceful Salvage') {
- displayResourcefulSalvagePopup();
- } else if (node.name === 'Hybrid Crafting') {
- displayHybridCraftingPopup();
- } else if (node.name === 'Runic Slots') {
- displayRunicSlotsPopup();
- } else if (node.name === 'Masterwork Creation') {
- displayMasterworkCreationPopup();
- }
- isDataSpikePopupActive = true;
- };
- newPopup.addChild(skillNode);
- });
- } else if (skillTreeName === 'The Prism of Potential') {
- var prismAsset = LK.getAsset('popup_prismofpotential', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- prismAsset.x = 2048 / 2;
- prismAsset.y = 2732 / 2;
- newPopup.addChild(prismAsset);
- var backButton = new BackButton();
- newPopup.addChild(backButton);
- var skillNodes = [{
- id: 'red_radiance_icon',
- x: 2048 / 4 - 50,
- y: 2732 / 6 - 50 + 100,
- name: 'Red Radiance'
- }, {
- id: 'blue_bulwark_icon',
- x: 2048 / 2,
- y: 2732 / 6 - 50 + 100,
- name: 'Blue Bulwark'
- }, {
- id: 'green_growth_icon',
- x: 3 * 2048 / 4 + 50,
- y: 2732 / 6 - 50 + 100,
- name: 'Green Growth'
- }, {
- id: 'yellow_zephyr_icon',
- x: 2048 / 4 - 50,
- y: 2 * 2732 / 6 + 100,
- name: 'Yellow Zephyr'
- }, {
- id: 'indigo_insight_icon',
- x: 2048 / 2,
- y: 2 * 2732 / 6 + 100,
- name: 'Indigo Insight'
- }, {
- id: 'chromatic_fusion_icon',
- x: 3 * 2048 / 4 + 50,
- y: 2 * 2732 / 6 + 100,
- name: 'Chromatic Fusion'
- }, {
- id: 'prismatic_ascendance_icon',
- x: 2048 / 2,
- y: 3 * 2732 / 6 + 50 + 100,
- name: 'Prismatic Ascendance'
- }];
- skillNodes.forEach(function (node) {
- var skillNode = LK.getAsset(node.id, {
- anchorX: 0.5,
- anchorY: 0.5,
- scaleX: 3,
- scaleY: 3
- });
- skillNode.x = node.x;
- skillNode.y = node.y;
- // Add skill point allocation text below each node
- var points = skillPointAllocations[node.name] ? skillPointAllocations[node.name].points : 0;
- var maxPoints = skillPointAllocations[node.name] ? skillPointAllocations[node.name].max : 5;
- var pointsText = new Text2(points + '/' + maxPoints, {
- size: 60,
- fill: 0xFFFFFF,
- fontWeight: "bold"
- });
- pointsText.x = node.x;
- pointsText.y = node.y + 150;
- pointsText.anchor.set(0.5, 0);
- pointsText.alpha = 1; // Ensure text is visible
- newPopup.addChild(pointsText);
- // Create a node representation for the skillNode
- var nodeCircle = LK.getAsset('glowing_line_asset', {
- anchorX: 0.5,
- anchorY: 0.5,
- width: 60,
- height: 60,
- color: 0xc05bf1
- });
- nodeCircle.x = node.x;
- nodeCircle.y = node.y;
- newPopup.addChildAt(nodeCircle, 0); // Add below the skill icon
- skillNode.down = function () {
- if (isDataSpikePopupActive) {
- return;
- }
- if (node.name === 'Red Radiance') {
- displayRedRadiancePopup();
- } else if (node.name === 'Blue Bulwark') {
- displayBlueBulwarkPopup();
- } else if (node.name === 'Green Growth') {
- displayGreenGrowthPopup();
- } else if (node.name === 'Yellow Zephyr') {
- displayYellowZephyrPopup();
- } else if (node.name === 'Indigo Insight') {
- displayIndigoInsightPopup();
- } else if (node.name === 'Chromatic Fusion') {
- displayChromaticFusionPopup();
- } else if (node.name === 'Prismatic Ascendance') {
- displayPrismaticAscendancePopup();
- }
- isDataSpikePopupActive = true;
- };
- newPopup.addChild(skillNode);
- });
- } else if (skillTreeName === 'The Nexus of Chaos') {
- var nexusAsset = LK.getAsset('popup_nexusofchaos', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- nexusAsset.x = 2048 / 2;
- nexusAsset.y = 2732 / 2;
- newPopup.addChild(nexusAsset);
- var backButton = new BackButton();
- newPopup.addChild(backButton);
- var skillNodes = [{
- id: 'chaotic_spark_icon',
- x: 2048 / 4 - 50,
- y: 2732 / 6 - 50 + 100,
- name: 'Chaotic Spark'
- }, {
- id: 'entropy_shield_icon',
- x: 2048 / 2,
- y: 2732 / 6 - 50 + 100,
- name: 'Entropy Shield'
- }, {
- id: 'unstable_rift_icon',
- x: 3 * 2048 / 4 + 50,
- y: 2732 / 6 - 50 + 100,
- name: 'Unstable Rift'
- }, {
- id: 'anomaly_shift_icon',
- x: 2048 / 4 - 50,
- y: 2 * 2732 / 6 + 100,
- name: 'Anomaly Shift'
- }, {
- id: 'cascade_of_disorder_icon',
- x: 2048 / 2,
- y: 2 * 2732 / 6 + 100,
- name: 'Cascade of Disorder'
- }, {
- id: 'fractured_fate_icon',
- x: 3 * 2048 / 4 + 50,
- y: 2 * 2732 / 6 + 100,
- name: 'Fractured Fate'
- }, {
- id: 'reality_collapse_icon',
- x: 2048 / 2,
- y: 3 * 2732 / 6 + 50 + 100,
- name: 'Reality Collapse'
- }];
- skillNodes.forEach(function (node) {
- var skillNode = LK.getAsset(node.id, {
- anchorX: 0.5,
- anchorY: 0.5,
- scaleX: 3,
- scaleY: 3
- });
- skillNode.x = node.x;
- skillNode.y = node.y;
- // Add skill point allocation text below each node
- var points = skillPointAllocations[node.name] ? skillPointAllocations[node.name].points : 0;
- var maxPoints = skillPointAllocations[node.name] ? skillPointAllocations[node.name].max : 5;
- var pointsText = new Text2(points + '/' + maxPoints, {
- size: 60,
- fill: 0xFFFFFF,
- fontWeight: "bold"
- });
- pointsText.x = node.x;
- pointsText.y = node.y + 150;
- pointsText.anchor.set(0.5, 0);
- pointsText.alpha = 1; // Ensure text is visible
- newPopup.addChild(pointsText);
- // Create a node representation for the skillNode
- var nodeCircle = LK.getAsset('glowing_line_asset', {
- anchorX: 0.5,
- anchorY: 0.5,
- width: 60,
- height: 60,
- color: 0xc05bf1
- });
- nodeCircle.x = node.x;
- nodeCircle.y = node.y;
- newPopup.addChildAt(nodeCircle, 0); // Add below the skill icon
- skillNode.down = function () {
- if (isDataSpikePopupActive) {
- return;
- }
- if (node.name === 'Chaotic Spark') {
- displayChaoticSparkPopup();
- } else if (node.name === 'Entropy Shield') {
- displayEntropyShieldPopup();
- } else if (node.name === 'Unstable Rift') {
- displayUnstableRiftPopup();
- } else if (node.name === 'Anomaly Shift') {
- displayAnomalyShiftPopup();
- } else if (node.name === 'Cascade of Disorder') {
- displayCascadeOfDisorderPopup();
- } else if (node.name === 'Fractured Fate') {
- displayFracturedFatePopup();
- } else if (node.name === 'Reality Collapse') {
- displayRealityCollapsePopup();
- }
- isDataSpikePopupActive = true;
- };
- newPopup.addChild(skillNode);
- });
- } else if (skillTreeName === 'The Symphony of Shadows') {
- var symphonyAsset = LK.getAsset('popup_symphonyofshadows', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- symphonyAsset.x = 2048 / 2;
- symphonyAsset.y = 2732 / 2;
- newPopup.addChild(symphonyAsset);
- var backButton = new BackButton();
- newPopup.addChild(backButton);
- var skillNodes = [{
- id: 'whispered_steps_icon',
- x: 2048 / 4 - 50,
- y: 2732 / 6 - 50 + 100,
- name: 'Whispered Steps'
- }, {
- id: 'nightfall_veil_icon',
- x: 2048 / 2,
- y: 2732 / 6 - 50 + 100,
- name: 'Nightfall Veil'
- }, {
- id: 'serrated_shade_icon',
- x: 3 * 2048 / 4 + 50,
- y: 2732 / 6 - 50 + 100,
- name: 'Serrated Shade'
- }, {
- id: 'shadow_familiar_icon',
- x: 2048 / 4 - 50,
- y: 2 * 2732 / 6 + 100,
- name: 'Shadow Familiar'
- }, {
- id: 'muffled_strikes_icon',
- x: 2048 / 2,
- y: 2 * 2732 / 6 + 100,
- name: 'Muffled Strikes'
- }, {
- id: 'umbral_echoes_icon',
- x: 3 * 2048 / 4 + 50,
- y: 2 * 2732 / 6 + 100,
- name: 'Umbral Echoes'
- }, {
- id: 'midnight_crescendo_icon',
- x: 2048 / 2,
- y: 3 * 2732 / 6 + 50 + 100,
- name: 'Midnight Crescendo'
- }];
- skillNodes.forEach(function (node) {
- var skillNode = LK.getAsset(node.id, {
- anchorX: 0.5,
- anchorY: 0.5,
- scaleX: 3,
- scaleY: 3
- });
- skillNode.x = node.x;
- skillNode.y = node.y;
- // Add skill point allocation text below each node
- var points = skillPointAllocations[node.name] ? skillPointAllocations[node.name].points : 0;
- var maxPoints = skillPointAllocations[node.name] ? skillPointAllocations[node.name].max : 5;
- var pointsText = new Text2(points + '/' + maxPoints, {
- size: 60,
- fill: 0xFFFFFF,
- fontWeight: "bold"
- });
- pointsText.x = node.x;
- pointsText.y = node.y + 150;
- pointsText.anchor.set(0.5, 0);
- pointsText.alpha = 1; // Ensure text is visible
- newPopup.addChild(pointsText);
- // Create a node representation for the skillNode
- var nodeCircle = LK.getAsset('glowing_line_asset', {
- anchorX: 0.5,
- anchorY: 0.5,
- width: 60,
- height: 60,
- color: 0xc05bf1
- });
- nodeCircle.x = node.x;
- nodeCircle.y = node.y;
- newPopup.addChildAt(nodeCircle, 0); // Add below the skill icon
- skillNode.down = function () {
- if (isDataSpikePopupActive) {
- return;
- }
- if (node.name === 'Whispered Steps') {
- displayWhisperedStepsPopup();
- } else if (node.name === 'Nightfall Veil') {
- displayNightfallVeilPopup();
- } else if (node.name === 'Serrated Shade') {
- displaySerratedShadePopup();
- } else if (node.name === 'Shadow Familiar') {
- displayShadowFamiliarPopup();
- } else if (node.name === 'Muffled Strikes') {
- displayMuffledStrikesPopup();
- } else if (node.name === 'Umbral Echoes') {
- displayUmbralEchoesPopup();
- } else if (node.name === 'Midnight Crescendo') {
- displayMidnightCrescendoPopup();
- }
- isDataSpikePopupActive = true;
- };
- newPopup.addChild(skillNode);
- });
- }
+ // Back Button (Use the class)
+ var backButton = new BackButton();
+ newPopup.addChild(backButton);
+ // Title
var skillTreeTitle = new Text2(skillTreeName, {
- size: 150,
+ size: 100,
fill: 0xFFFFFF,
fontWeight: "bold",
+ // Slightly smaller title
fontFamily: "Techno, sans-serif",
letterSpacing: 2,
stroke: 0x00FFCC,
- strokeThickness: 3
+ strokeThickness: 3,
+ align: 'center'
});
- skillTreeTitle.x = 2048 / 2;
- skillTreeTitle.y = 100;
+ skillTreeTitle.x = roomWidth / 2;
+ skillTreeTitle.y = 100; // Position near top
skillTreeTitle.anchor.set(0.5, 0);
newPopup.addChild(skillTreeTitle);
- // Create the skillTreePointsText at the bottom of the skill tree popup
+ // Create Nodes
+ layoutData.nodes.forEach(function (skillName) {
+ var position = layoutData.positions[skillName];
+ if (position) {
+ createSkillTreeNode(skillName, position, newPopup);
+ } else {
+ LK.log("Warning: Position not defined for skill " + skillName);
+ }
+ });
+ // Skill Points Display at Bottom (use global reference)
skillTreePointsText = new Text2('Skill Points: ' + heroSkillPoints, {
size: 70,
fill: 0xFFFFFF,
fontWeight: "bold"
});
- skillTreePointsText.x = 2048 / 2;
- skillTreePointsText.y = 2732 - 100; // bottom of the screen
- skillTreePointsText.anchor.set(0.5, 0);
+ skillTreePointsText.x = roomWidth / 2;
+ skillTreePointsText.y = roomHeight - 100; // bottom of the screen
+ skillTreePointsText.anchor.set(0.5, 1); // Anchor bottom center
newPopup.addChild(skillTreePointsText);
- // Now that skillTreePointsText is defined, we can safely call refreshTreePointsText
- refreshTreePointsText();
+ refreshTreePointsText(); // Update the text content
menuContainer.addChild(newPopup);
-}
\ No newline at end of file
+ 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
+ for (var i = menuContainer.children.length - 1; i >= 0; i--) {
+ var child = menuContainer.children[i];
+ if (child.name && child.name.endsWith("_popup")) {
+ // Relies on name set in openSkillTreePopup
+ activeSkillTreePopup = child;
+ break;
+ }
+ }
+ if (activeSkillTreePopup) {
+ for (var skillName in skillDefinitions) {
+ if (skillDefinitions.hasOwnProperty(skillName)) {
+ var skillData = skillDefinitions[skillName];
+ // Find the text node by name convention
+ var pointsTextNode = activeSkillTreePopup.getChildByName(skillName + "_pointsText");
+ if (pointsTextNode) {
+ pointsTextNode.setText(skillData.points + '/' + skillData.max);
+ }
+ }
+ }
+ }
+}
+// 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);
+ }
+}
+// --- 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) {
+ healthText.setText('Health: ' + Math.max(hero.health, 0));
+ } // Show 0 if health < 0
+}
+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) {
+ LK.log("Error playing music asset: " + musicAsset + " - " + 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) {
+ LK.log("Error playing sound asset: " + soundAsset + " - " + 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 logic
+ 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
+ }
+ };
+ // 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
+}
+function initializeGame() {
+ // Initial setup when the game actually begins playing
+ // Create Hero if not already created
+ if (!hero) {
+ hero = gameContainer.addChild(new Hero());
+ hero.x = roomWidth / 2;
+ hero.y = roomHeight / 2;
+ // Assign joysticks to hero
+ 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.health = hero.maxHealth;
+ hero.x = roomWidth / 2;
+ hero.y = roomHeight / 2;
+ hero.canExitRoom = false;
+ // Reset any active effects or cooldowns if needed
+ }
+ // Set up initial room
+ currentRoom = new Room(1); // Start in room 1
+ visitedRooms = [1]; // Mark room 1 as visited initially
+ // Set initial background
+ 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', {
+ // Simple black bar background for stats
+ anchorX: 0.0,
+ anchorY: 0.0,
+ width: roomWidth,
+ height: 100,
+ x: 0,
+ y: 0,
+ color: 0x111111,
+ alpha: 0.7
+ });
+ uiContainer.addChild(statsBar); // Add to UI container
+ roomDisplay = new Text2('Room: 1 | Enemies Left: 0', {
+ size: 60,
+ fill: 0xFFFFFF,
+ fontWeight: "bold"
+ });
+ roomDisplay.x = roomWidth / 2; // Center
+ roomDisplay.y = 50; // Middle of bar
+ roomDisplay.anchor.set(0.5, 0.5);
+ uiContainer.addChild(roomDisplay);
+ healthText = new Text2('Health: ' + hero.health, {
+ size: 60,
+ fill: 0x00FF00,
+ fontWeight: "bold"
+ });
+ healthText.x = 150; // Left side
+ healthText.y = 50;
+ healthText.anchor.set(0, 0.5);
+ uiContainer.addChild(healthText);
+ updateRoomDisplay(); // Initial UI text update
+ playRoomMusic(currentRoom.number); // Play music for room 1
+ currentRoom.startSpawning(); // Start spawning enemies
+}
+// --- Global UI Buttons ---
+// Kill All Button (Debug?)
+var killAllButton = LK.getAsset('killAllButton', {
+ anchorX: 0.5,
+ anchorY: 0.5
+});
+killAllButton.x = 100;
+killAllButton.y = roomHeight - 200; // 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 - 150; // Position lower right
+switchWeaponZone.y = roomHeight - 200;
+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 any popup is active, skip core game updates
+ if (isPopupActive || isSkillDetailPopupActive) {
+ // Can add logic here for animating menus if needed
+ return;
+ }
+ // --- Core Gameplay Update ---
+ if (!hero || !currentRoom) {
+ return;
+ } // Wait until game is initialized
+ // Update Hero (handles movement, attacks, collision checks)
+ hero.update();
+ // Update Arrows
+ // Loop backwards for safe removal while iterating
+ for (var j = arrows.length - 1; j >= 0; j--) {
+ var arrow = arrows[j];
+ if (!arrow) {
+ // Safety check if arrow was destroyed elsewhere
+ 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 for collision
+ var hitDx = enemy.x - arrow.x;
+ var hitDy = enemy.y - arrow.y;
+ var hitDistSqr = hitDx * hitDx + hitDy * hitDy;
+ var enemyRadius = (enemy.width || 50) / 2; // Estimate radius
+ var arrowRadius = (arrow.width || 10) / 2;
+ var collisionDist = enemyRadius + arrowRadius;
+ if (hitDistSqr < collisionDist * collisionDist) {
+ // Check squared distance
+ if (enemy.takeDamage) {
+ enemy.takeDamage(arrow.damage);
+ } // Damage enemy
+ arrow.destroy(); // Destroy arrow (this should remove it from array too)
+ // No need to splice here if arrow.destroy() handles it
+ break; // Arrow hits one enemy and is destroyed
+ }
+ }
+ } // End arrow update loop
+ // Update Enemies (already loop backwards in takeDamage/die)
+ // Make a copy of the array to iterate over, in case enemies are removed during update (e.g., by hero attack)
+ var enemiesToUpdate = currentRoom.enemies.slice();
+ for (var k = 0; k < enemiesToUpdate.length; k++) {
+ var enemy = enemiesToUpdate[k];
+ // Ensure the enemy wasn't destroyed by a previous update in the same frame
+ if (enemy && enemy.active && enemy.update) {
+ enemy.update();
+ }
+ }
+ // Room cleared check is now handled within enemy.die()
+ // checkRoomCleared(); // Can be removed if die() handles it reliably
+}; // End Game Update
\ No newline at end of file
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