├── CLAUDE.MD ├── .gitignore ├── .beads ├── bd.sock.startlock ├── metadata.json ├── deletions.jsonl ├── .gitignore ├── config.yaml └── README.md ├── .vscode └── settings.json ├── images ├── image1.png ├── image2.png └── image3.png ├── .gitattributes ├── .cursor └── commands │ └── luacheck.md ├── Diablo.love ├── resources │ ├── icons │ │ ├── bag.png │ │ ├── book.png │ │ ├── trash.png │ │ ├── scroll.png │ │ ├── book_open.png │ │ ├── gold │ │ │ ├── gold_1.png │ │ │ ├── gold_2.png │ │ │ ├── gold_3.png │ │ │ ├── gold_4.png │ │ │ └── gold_5.png │ │ ├── mana_potion.png │ │ ├── chests │ │ │ ├── chest_1.png │ │ │ ├── chest_2.png │ │ │ ├── chest_3.png │ │ │ ├── chest_4.png │ │ │ ├── chest_5.png │ │ │ ├── chest_6.png │ │ │ ├── chest_7.png │ │ │ └── chest_8.png │ │ └── health_potion.png │ ├── foe │ │ ├── orc1_walk.png │ │ ├── orc2_walk.png │ │ ├── orc3_walk.png │ │ ├── goblin1_walk.png │ │ ├── goblin2_walk.png │ │ ├── goblin3_walk.png │ │ ├── orc1_attack.png │ │ ├── orc1_death.png │ │ ├── orc2_attack.png │ │ ├── orc2_death.png │ │ ├── orc3_attack.png │ │ ├── orc3_death.png │ │ ├── goblin1_attack.png │ │ ├── goblin1_death.png │ │ ├── goblin2_attack.png │ │ ├── goblin2_death.png │ │ ├── goblin3_attack.png │ │ └── goblin3_death.png │ ├── skills │ │ ├── fireball.png │ │ └── thunder.png │ ├── armor │ │ ├── feet │ │ │ ├── feet_1.png │ │ │ ├── feet_10.png │ │ │ ├── feet_2.png │ │ │ ├── feet_3.png │ │ │ ├── feet_4.png │ │ │ ├── feet_5.png │ │ │ ├── feet_6.png │ │ │ ├── feet_7.png │ │ │ ├── feet_8.png │ │ │ └── feet_9.png │ │ ├── ring │ │ │ ├── ring_1.png │ │ │ ├── ring_10.png │ │ │ ├── ring_11.png │ │ │ ├── ring_12.png │ │ │ ├── ring_13.png │ │ │ ├── ring_14.png │ │ │ ├── ring_15.png │ │ │ ├── ring_16.png │ │ │ ├── ring_17.png │ │ │ ├── ring_18.png │ │ │ ├── ring_19.png │ │ │ ├── ring_2.png │ │ │ ├── ring_20.png │ │ │ ├── ring_21.png │ │ │ ├── ring_22.png │ │ │ ├── ring_23.png │ │ │ ├── ring_24.png │ │ │ ├── ring_25.png │ │ │ ├── ring_26.png │ │ │ ├── ring_27.png │ │ │ ├── ring_28.png │ │ │ ├── ring_29.png │ │ │ ├── ring_3.png │ │ │ ├── ring_30.png │ │ │ ├── ring_31.png │ │ │ ├── ring_32.png │ │ │ ├── ring_33.png │ │ │ ├── ring_34.png │ │ │ ├── ring_35.png │ │ │ ├── ring_36.png │ │ │ ├── ring_37.png │ │ │ ├── ring_38.png │ │ │ ├── ring_39.png │ │ │ ├── ring_4.png │ │ │ ├── ring_40.png │ │ │ ├── ring_5.png │ │ │ ├── ring_6.png │ │ │ ├── ring_7.png │ │ │ ├── ring_8.png │ │ │ └── ring_9.png │ │ ├── amulet │ │ │ ├── amulet_1.png │ │ │ ├── amulet_2.png │ │ │ ├── amulet_3.png │ │ │ ├── amulet_4.png │ │ │ ├── amulet_5.png │ │ │ ├── amulet_6.png │ │ │ ├── amulet_7.png │ │ │ ├── amulet_8.png │ │ │ ├── amulet_9.png │ │ │ ├── amulet_10.png │ │ │ ├── amulet_11.png │ │ │ ├── amulet_12.png │ │ │ ├── amulet_13.png │ │ │ ├── amulet_14.png │ │ │ ├── amulet_15.png │ │ │ ├── amulet_16.png │ │ │ ├── amulet_17.png │ │ │ ├── amulet_18.png │ │ │ ├── amulet_19.png │ │ │ ├── amulet_20.png │ │ │ ├── amulet_21.png │ │ │ ├── amulet_22.png │ │ │ ├── amulet_23.png │ │ │ ├── amulet_24.png │ │ │ ├── amulet_25.png │ │ │ ├── amulet_26.png │ │ │ ├── amulet_27.png │ │ │ ├── amulet_28.png │ │ │ ├── amulet_29.png │ │ │ ├── amulet_30.png │ │ │ ├── amulet_31.png │ │ │ ├── amulet_32.png │ │ │ ├── amulet_33.png │ │ │ ├── amulet_34.png │ │ │ ├── amulet_35.png │ │ │ ├── amulet_36.png │ │ │ ├── amulet_37.png │ │ │ ├── amulet_38.png │ │ │ ├── amulet_39.png │ │ │ ├── amulet_40.png │ │ │ ├── amulet_41.png │ │ │ ├── amulet_42.png │ │ │ ├── amulet_43.png │ │ │ ├── amulet_44.png │ │ │ ├── amulet_45.png │ │ │ ├── amulet_46.png │ │ │ ├── amulet_47.png │ │ │ ├── amulet_48.png │ │ │ ├── amulet_49.png │ │ │ ├── amulet_50.png │ │ │ ├── amulet_51.png │ │ │ ├── amulet_52.png │ │ │ ├── amulet_53.png │ │ │ ├── amulet_54.png │ │ │ ├── amulet_55.png │ │ │ ├── amulet_56.png │ │ │ ├── amulet_57.png │ │ │ ├── amulet_58.png │ │ │ ├── amulet_59.png │ │ │ └── amulet_60.png │ │ ├── helmet │ │ │ ├── helmet_1.png │ │ │ ├── helmet_2.png │ │ │ ├── helmet_3.png │ │ │ ├── helmet_4.png │ │ │ ├── helmet_5.png │ │ │ ├── helmet_6.png │ │ │ ├── helmet_7.png │ │ │ ├── helmet_8.png │ │ │ ├── helmet_9.png │ │ │ └── helmet_10.png │ │ ├── torso │ │ │ ├── torso_1.png │ │ │ ├── torso_10.png │ │ │ ├── torso_2.png │ │ │ ├── torso_3.png │ │ │ ├── torso_4.png │ │ │ ├── torso_5.png │ │ │ ├── torso_6.png │ │ │ ├── torso_7.png │ │ │ ├── torso_8.png │ │ │ └── torso_9.png │ │ └── gauntlet │ │ │ ├── gauntlet_1.png │ │ │ ├── gauntlet_10.png │ │ │ ├── gauntlet_2.png │ │ │ ├── gauntlet_3.png │ │ │ ├── gauntlet_4.png │ │ │ ├── gauntlet_5.png │ │ │ ├── gauntlet_6.png │ │ │ ├── gauntlet_7.png │ │ │ ├── gauntlet_8.png │ │ │ └── gauntlet_9.png │ └── weapons │ │ ├── axe │ │ ├── axe_1.png │ │ ├── axe_10.png │ │ ├── axe_2.png │ │ ├── axe_3.png │ │ ├── axe_4.png │ │ ├── axe_5.png │ │ ├── axe_6.png │ │ ├── axe_7.png │ │ ├── axe_8.png │ │ └── axe_9.png │ │ ├── mace │ │ ├── mace_1.png │ │ ├── mace_10.png │ │ ├── mace_2.png │ │ ├── mace_3.png │ │ ├── mace_4.png │ │ ├── mace_5.png │ │ ├── mace_6.png │ │ ├── mace_7.png │ │ ├── mace_8.png │ │ └── mace_9.png │ │ ├── sword │ │ ├── sword_1.png │ │ ├── sword_2.png │ │ ├── sword_3.png │ │ ├── sword_4.png │ │ ├── sword_5.png │ │ ├── sword_6.png │ │ ├── sword_7.png │ │ ├── sword_8.png │ │ ├── sword_9.png │ │ └── sword_10.png │ │ └── dagger │ │ ├── dagger_1.png │ │ ├── dagger_2.png │ │ ├── dagger_3.png │ │ ├── dagger_4.png │ │ ├── dagger_5.png │ │ ├── dagger_6.png │ │ ├── dagger_7.png │ │ ├── dagger_8.png │ │ ├── dagger_9.png │ │ └── dagger_10.png ├── components │ ├── dead.lua │ ├── player_controlled.lua │ ├── position.lua │ ├── size.lua │ ├── chase.lua │ ├── chunk_resident.lua │ ├── inventory.lua │ ├── inactive.lua │ ├── structure.lua │ ├── skills.lua │ ├── recently_damaged.lua │ ├── mana.lua │ ├── equipment.lua │ ├── health.lua │ ├── knockback.lua │ ├── targeting.lua │ ├── loot_scatter.lua │ ├── foe.lua │ ├── lootable.lua │ ├── movement.lua │ ├── experience.lua │ ├── physics_body.lua │ ├── detection.lua │ ├── potions.lua │ ├── renderable.lua │ ├── base_stats.lua │ ├── death_animation.lua │ ├── combat.lua │ ├── blood_burst.lua │ ├── projectile.lua │ ├── notification.lua │ ├── wander.lua │ └── floating_damage.lua ├── conf.lua ├── modules │ ├── scene_kinds.lua │ ├── vector.lua │ ├── leveling.lua │ ├── world │ │ ├── boss_name_generator.lua │ │ └── biome_resolver.lua │ ├── stats_derivation.lua │ ├── stats.lua │ ├── input_actions.lua │ ├── power_glow_shader.lua │ └── lifetime_stats.lua ├── systems │ ├── core │ │ ├── physics.lua │ │ ├── mana_regen.lua │ │ ├── camera.lua │ │ ├── walking_animation.lua │ │ ├── lifetime_stats.lua │ │ ├── death_animation.lua │ │ ├── foe_animation.lua │ │ ├── loot_scatter.lua │ │ ├── starter_gear.lua │ │ ├── apply_stats.lua │ │ └── loot_tooltip.lua │ ├── ui │ │ ├── main.lua │ │ ├── experience_bar.lua │ │ └── config.lua │ ├── helpers │ │ ├── sprite_direction.lua │ │ ├── coordinates.lua │ │ ├── combat_timing.lua │ │ ├── sprite_renderer.lua │ │ ├── aggro.lua │ │ └── scrollable_content.lua │ ├── input │ │ ├── mouse_targeting.lua │ │ ├── mouse_input.lua │ │ ├── mouse_look.lua │ │ ├── player_input.lua │ │ └── mouse_movement.lua │ ├── ai │ │ ├── spawn.lua │ │ └── detection.lua │ ├── render │ │ ├── blood_burst.lua │ │ ├── window │ │ │ └── scrollbar.lua │ │ ├── loot.lua │ │ ├── mouse_look.lua │ │ ├── health.lua │ │ └── inventory │ │ │ └── tooltip.lua │ └── combat │ │ ├── foe_attack.lua │ │ └── player_attack.lua ├── entities │ ├── notification.lua │ ├── loot.lua │ ├── projectile.lua │ ├── structures │ │ └── factory.lua │ └── player.lua ├── data │ ├── component_defaults.lua │ ├── foe_rarities.lua │ └── biomes.lua ├── shaders │ ├── power_glow.glsl │ └── crt.glsl └── scenes │ └── controls.lua ├── .luarc.json ├── MASTER_PLAN.MD ├── spec ├── system_helpers │ ├── tooltips_stub.lua │ ├── item_generator_stub.lua │ ├── coordinates_stub.lua │ ├── projectile_effects_stub.lua │ ├── items_data_stub.lua │ └── loot_entity_stub.lua ├── spec_helper.lua ├── support │ └── test_world.lua └── systems │ ├── ui │ └── target_spec.lua │ ├── ai │ └── chase_spec.lua │ └── core │ └── movement_spec.lua ├── .luacheckrc ├── .github └── workflows │ ├── lint.yml │ └── tests.yml └── docs └── README.md /CLAUDE.MD: -------------------------------------------------------------------------------- 1 | AGENTS.MD -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /.beads/bd.sock.startlock: -------------------------------------------------------------------------------- 1 | 9328 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "pixelbyte.love2d.srcDir": "Diablo.love" 3 | } 4 | -------------------------------------------------------------------------------- /images/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/images/image1.png -------------------------------------------------------------------------------- /images/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/images/image2.png -------------------------------------------------------------------------------- /images/image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/images/image3.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | 2 | # Use bd merge for beads JSONL files 3 | .beads/issues.jsonl merge=beads 4 | -------------------------------------------------------------------------------- /.cursor/commands/luacheck.md: -------------------------------------------------------------------------------- 1 | Run a .luacheck and fix everything in the project. 2 | 3 | ``` 4 | luacheck . 5 | ``` 6 | -------------------------------------------------------------------------------- /Diablo.love/resources/icons/bag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/bag.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/book.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/trash.png -------------------------------------------------------------------------------- /.beads/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": "beads.db", 3 | "jsonl_export": "issues.jsonl", 4 | "last_bd_version": "0.27.0" 5 | } -------------------------------------------------------------------------------- /Diablo.love/resources/foe/orc1_walk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/foe/orc1_walk.png -------------------------------------------------------------------------------- /Diablo.love/resources/foe/orc2_walk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/foe/orc2_walk.png -------------------------------------------------------------------------------- /Diablo.love/resources/foe/orc3_walk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/foe/orc3_walk.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/scroll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/scroll.png -------------------------------------------------------------------------------- /Diablo.love/resources/foe/goblin1_walk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/foe/goblin1_walk.png -------------------------------------------------------------------------------- /Diablo.love/resources/foe/goblin2_walk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/foe/goblin2_walk.png -------------------------------------------------------------------------------- /Diablo.love/resources/foe/goblin3_walk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/foe/goblin3_walk.png -------------------------------------------------------------------------------- /Diablo.love/resources/foe/orc1_attack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/foe/orc1_attack.png -------------------------------------------------------------------------------- /Diablo.love/resources/foe/orc1_death.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/foe/orc1_death.png -------------------------------------------------------------------------------- /Diablo.love/resources/foe/orc2_attack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/foe/orc2_attack.png -------------------------------------------------------------------------------- /Diablo.love/resources/foe/orc2_death.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/foe/orc2_death.png -------------------------------------------------------------------------------- /Diablo.love/resources/foe/orc3_attack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/foe/orc3_attack.png -------------------------------------------------------------------------------- /Diablo.love/resources/foe/orc3_death.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/foe/orc3_death.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/book_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/book_open.png -------------------------------------------------------------------------------- /Diablo.love/resources/skills/fireball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/skills/fireball.png -------------------------------------------------------------------------------- /Diablo.love/resources/skills/thunder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/skills/thunder.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/feet/feet_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/feet/feet_1.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/feet/feet_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/feet/feet_10.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/feet/feet_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/feet/feet_2.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/feet/feet_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/feet/feet_3.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/feet/feet_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/feet/feet_4.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/feet/feet_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/feet/feet_5.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/feet/feet_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/feet/feet_6.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/feet/feet_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/feet/feet_7.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/feet/feet_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/feet/feet_8.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/feet/feet_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/feet/feet_9.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_1.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_10.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_11.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_12.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_13.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_14.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_15.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_16.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_17.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_18.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_19.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_2.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_20.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_21.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_22.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_23.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_24.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_25.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_26.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_27.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_28.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_29.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_3.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_30.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_31.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_32.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_33.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_34.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_35.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_36.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_37.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_38.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_39.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_4.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_40.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_5.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_6.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_7.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_8.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/ring/ring_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/ring/ring_9.png -------------------------------------------------------------------------------- /Diablo.love/resources/foe/goblin1_attack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/foe/goblin1_attack.png -------------------------------------------------------------------------------- /Diablo.love/resources/foe/goblin1_death.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/foe/goblin1_death.png -------------------------------------------------------------------------------- /Diablo.love/resources/foe/goblin2_attack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/foe/goblin2_attack.png -------------------------------------------------------------------------------- /Diablo.love/resources/foe/goblin2_death.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/foe/goblin2_death.png -------------------------------------------------------------------------------- /Diablo.love/resources/foe/goblin3_attack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/foe/goblin3_attack.png -------------------------------------------------------------------------------- /Diablo.love/resources/foe/goblin3_death.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/foe/goblin3_death.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/gold/gold_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/gold/gold_1.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/gold/gold_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/gold/gold_2.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/gold/gold_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/gold/gold_3.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/gold/gold_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/gold/gold_4.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/gold/gold_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/gold/gold_5.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/mana_potion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/mana_potion.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/axe/axe_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/axe/axe_1.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/axe/axe_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/axe/axe_10.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/axe/axe_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/axe/axe_2.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/axe/axe_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/axe/axe_3.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/axe/axe_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/axe/axe_4.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/axe/axe_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/axe/axe_5.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/axe/axe_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/axe/axe_6.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/axe/axe_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/axe/axe_7.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/axe/axe_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/axe/axe_8.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/axe/axe_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/axe/axe_9.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_1.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_2.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_3.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_4.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_5.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_6.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_7.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_8.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_9.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/helmet/helmet_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/helmet/helmet_1.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/helmet/helmet_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/helmet/helmet_2.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/helmet/helmet_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/helmet/helmet_3.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/helmet/helmet_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/helmet/helmet_4.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/helmet/helmet_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/helmet/helmet_5.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/helmet/helmet_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/helmet/helmet_6.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/helmet/helmet_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/helmet/helmet_7.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/helmet/helmet_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/helmet/helmet_8.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/helmet/helmet_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/helmet/helmet_9.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/torso/torso_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/torso/torso_1.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/torso/torso_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/torso/torso_10.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/torso/torso_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/torso/torso_2.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/torso/torso_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/torso/torso_3.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/torso/torso_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/torso/torso_4.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/torso/torso_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/torso/torso_5.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/torso/torso_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/torso/torso_6.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/torso/torso_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/torso/torso_7.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/torso/torso_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/torso/torso_8.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/torso/torso_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/torso/torso_9.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/chests/chest_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/chests/chest_1.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/chests/chest_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/chests/chest_2.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/chests/chest_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/chests/chest_3.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/chests/chest_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/chests/chest_4.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/chests/chest_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/chests/chest_5.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/chests/chest_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/chests/chest_6.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/chests/chest_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/chests/chest_7.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/chests/chest_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/chests/chest_8.png -------------------------------------------------------------------------------- /Diablo.love/resources/icons/health_potion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/icons/health_potion.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/mace/mace_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/mace/mace_1.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/mace/mace_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/mace/mace_10.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/mace/mace_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/mace/mace_2.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/mace/mace_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/mace/mace_3.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/mace/mace_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/mace/mace_4.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/mace/mace_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/mace/mace_5.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/mace/mace_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/mace/mace_6.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/mace/mace_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/mace/mace_7.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/mace/mace_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/mace/mace_8.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/mace/mace_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/mace/mace_9.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/sword/sword_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/sword/sword_1.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/sword/sword_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/sword/sword_2.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/sword/sword_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/sword/sword_3.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/sword/sword_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/sword/sword_4.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/sword/sword_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/sword/sword_5.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/sword/sword_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/sword/sword_6.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/sword/sword_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/sword/sword_7.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/sword/sword_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/sword/sword_8.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/sword/sword_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/sword/sword_9.png -------------------------------------------------------------------------------- /.luarc.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "diagnostics": { 4 | "globals": ["love"] 5 | }, 6 | "runtime": { 7 | "version": "LuaJIT" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Diablo.love/components/dead.lua: -------------------------------------------------------------------------------- 1 | local function createDeadComponent(_opts) 2 | return { flag = true } 3 | end 4 | 5 | return createDeadComponent 6 | -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_10.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_11.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_12.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_13.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_14.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_15.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_16.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_17.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_18.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_19.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_20.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_21.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_22.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_23.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_24.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_25.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_26.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_27.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_28.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_29.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_30.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_31.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_32.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_33.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_34.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_35.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_36.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_37.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_38.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_39.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_40.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_41.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_42.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_43.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_44.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_45.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_46.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_46.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_47.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_48.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_49.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_50.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_51.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_51.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_52.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_52.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_53.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_53.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_54.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_54.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_55.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_56.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_57.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_58.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_59.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/amulet/amulet_60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/amulet/amulet_60.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/helmet/helmet_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/helmet/helmet_10.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/dagger/dagger_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/dagger/dagger_1.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/dagger/dagger_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/dagger/dagger_2.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/dagger/dagger_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/dagger/dagger_3.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/dagger/dagger_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/dagger/dagger_4.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/dagger/dagger_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/dagger/dagger_5.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/dagger/dagger_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/dagger/dagger_6.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/dagger/dagger_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/dagger/dagger_7.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/dagger/dagger_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/dagger/dagger_8.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/dagger/dagger_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/dagger/dagger_9.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/sword/sword_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/sword/sword_10.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/gauntlet/gauntlet_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/gauntlet/gauntlet_1.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/gauntlet/gauntlet_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/gauntlet/gauntlet_10.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/gauntlet/gauntlet_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/gauntlet/gauntlet_2.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/gauntlet/gauntlet_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/gauntlet/gauntlet_3.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/gauntlet/gauntlet_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/gauntlet/gauntlet_4.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/gauntlet/gauntlet_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/gauntlet/gauntlet_5.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/gauntlet/gauntlet_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/gauntlet/gauntlet_6.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/gauntlet/gauntlet_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/gauntlet/gauntlet_7.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/gauntlet/gauntlet_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/gauntlet/gauntlet_8.png -------------------------------------------------------------------------------- /Diablo.love/resources/armor/gauntlet/gauntlet_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/armor/gauntlet/gauntlet_9.png -------------------------------------------------------------------------------- /Diablo.love/resources/weapons/dagger/dagger_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dimillian/Diablo2D/main/Diablo.love/resources/weapons/dagger/dagger_10.png -------------------------------------------------------------------------------- /Diablo.love/components/player_controlled.lua: -------------------------------------------------------------------------------- 1 | local function createPlayerControlledComponent() 2 | return { 3 | inputScheme = "keyboard", 4 | } 5 | end 6 | 7 | return createPlayerControlledComponent 8 | -------------------------------------------------------------------------------- /.beads/deletions.jsonl: -------------------------------------------------------------------------------- 1 | {"id":"Diablo-5za","ts":"2025-11-30T08:48:32.194317Z","by":"dimillian","reason":"manual delete"} 2 | {"id":"Diablo-849","ts":"2025-11-30T08:48:38.109399Z","by":"dimillian","reason":"manual delete"} 3 | -------------------------------------------------------------------------------- /MASTER_PLAN.MD: -------------------------------------------------------------------------------- 1 | # Master Plan - Upcoming Features 2 | 3 | This document outlines concrete implementation details for upcoming features. Each section provides actionable guidance that fits within the existing ECS architecture. 4 | 5 | --- 6 | 7 | TBD 8 | -------------------------------------------------------------------------------- /Diablo.love/components/position.lua: -------------------------------------------------------------------------------- 1 | local function createPositionComponent(opts) 2 | opts = opts or {} 3 | 4 | return { 5 | x = opts.x or 0, 6 | y = opts.y or 0, 7 | } 8 | end 9 | 10 | return createPositionComponent 11 | -------------------------------------------------------------------------------- /Diablo.love/components/size.lua: -------------------------------------------------------------------------------- 1 | local function createSizeComponent(opts) 2 | opts = opts or {} 3 | 4 | return { 5 | w = opts.w or opts.width or 16, 6 | h = opts.h or opts.height or 24, 7 | } 8 | end 9 | 10 | return createSizeComponent 11 | -------------------------------------------------------------------------------- /spec/system_helpers/tooltips_stub.lua: -------------------------------------------------------------------------------- 1 | local Tooltips = {} 2 | 3 | Tooltips.rarityColors = { 4 | common = { 1, 1, 1, 1 }, 5 | } 6 | 7 | function Tooltips.getRarityColor(_rarity) 8 | return Tooltips.rarityColors.common 9 | end 10 | 11 | return Tooltips 12 | -------------------------------------------------------------------------------- /Diablo.love/components/chase.lua: -------------------------------------------------------------------------------- 1 | local function createChaseComponent(opts) 2 | opts = opts or {} 3 | 4 | return { 5 | targetId = opts.targetId or nil, -- ID of the entity being chased 6 | removeOnDeath = true, 7 | } 8 | end 9 | 10 | return createChaseComponent 11 | -------------------------------------------------------------------------------- /Diablo.love/components/chunk_resident.lua: -------------------------------------------------------------------------------- 1 | local function createChunkResidentComponent(opts) 2 | opts = opts or {} 3 | 4 | return { 5 | chunkKey = opts.chunkKey, 6 | descriptorId = opts.descriptorId, 7 | kind = opts.kind, 8 | } 9 | end 10 | 11 | return createChunkResidentComponent 12 | -------------------------------------------------------------------------------- /Diablo.love/components/inventory.lua: -------------------------------------------------------------------------------- 1 | local ComponentDefaults = require("data.component_defaults") 2 | 3 | local function createInventoryComponent() 4 | return { 5 | items = {}, 6 | capacity = ComponentDefaults.INVENTORY_CAPACITY, 7 | gold = 0, 8 | } 9 | end 10 | 11 | return createInventoryComponent 12 | -------------------------------------------------------------------------------- /Diablo.love/conf.lua: -------------------------------------------------------------------------------- 1 | function love.conf(t) 2 | t.window = t.window or {} 3 | t.window.width = 1024 4 | t.window.height = 800 5 | t.window.highdpi = true 6 | t.window.title = "Diablo2D" 7 | -- Set background color to dark gray instead of black 8 | t.window.backgroundColor = { 0.05, 0.05, 0.05, 1 } 9 | end 10 | -------------------------------------------------------------------------------- /Diablo.love/components/inactive.lua: -------------------------------------------------------------------------------- 1 | local ComponentDefaults = require("data.component_defaults") 2 | 3 | local function createInactiveComponent(opts) 4 | opts = opts or {} 5 | 6 | return { 7 | isInactive = opts.isInactive or ComponentDefaults.INACTIVE_STATE, 8 | } 9 | end 10 | 11 | return createInactiveComponent 12 | -------------------------------------------------------------------------------- /Diablo.love/components/structure.lua: -------------------------------------------------------------------------------- 1 | local function createStructureComponent(opts) 2 | opts = opts or {} 3 | 4 | return { 5 | id = opts.id, 6 | structureId = opts.structureId, 7 | lootable = opts.lootable or false, 8 | persistent = true, 9 | } 10 | end 11 | 12 | return createStructureComponent 13 | -------------------------------------------------------------------------------- /Diablo.love/components/skills.lua: -------------------------------------------------------------------------------- 1 | local function createSkillsComponent() 2 | local equipped = {} 3 | 4 | for index = 1, 4 do 5 | equipped[index] = nil 6 | end 7 | 8 | return { 9 | equipped = equipped, 10 | availablePoints = 0, 11 | allocations = {}, 12 | } 13 | end 14 | 15 | return createSkillsComponent 16 | -------------------------------------------------------------------------------- /spec/system_helpers/item_generator_stub.lua: -------------------------------------------------------------------------------- 1 | local ItemGenerator = {} 2 | 3 | function ItemGenerator.roll(_opts) 4 | return { 5 | id = "test_item", 6 | name = "Test Blade", 7 | rarity = "common", 8 | slot = "weapon", 9 | stats = {}, 10 | source = "monster", 11 | } 12 | end 13 | 14 | return ItemGenerator 15 | -------------------------------------------------------------------------------- /Diablo.love/components/recently_damaged.lua: -------------------------------------------------------------------------------- 1 | local ComponentDefaults = require("data.component_defaults") 2 | 3 | local function createRecentlyDamagedComponent() 4 | return { 5 | timer = ComponentDefaults.DAMAGE_FLASH_DURATION, 6 | maxTimer = ComponentDefaults.DAMAGE_FLASH_DURATION, 7 | } 8 | end 9 | 10 | return createRecentlyDamagedComponent 11 | -------------------------------------------------------------------------------- /Diablo.love/components/mana.lua: -------------------------------------------------------------------------------- 1 | local ComponentDefaults = require("data.component_defaults") 2 | 3 | local function createManaComponent(opts) 4 | opts = opts or {} 5 | 6 | local max = opts.max or ComponentDefaults.PLAYER_STARTING_MANA 7 | 8 | return { 9 | current = opts.current or max, 10 | max = max, 11 | } 12 | end 13 | 14 | return createManaComponent 15 | -------------------------------------------------------------------------------- /Diablo.love/components/equipment.lua: -------------------------------------------------------------------------------- 1 | local equipmentSlots = { "weapon", "head", "chest", "feet", "gloves", "ringLeft", "ringRight", "amulet" } 2 | 3 | local function createEquipmentComponent() 4 | local equipment = {} 5 | for _, slot in ipairs(equipmentSlots) do 6 | equipment[slot] = nil 7 | end 8 | 9 | return equipment 10 | end 11 | 12 | return createEquipmentComponent 13 | -------------------------------------------------------------------------------- /Diablo.love/components/health.lua: -------------------------------------------------------------------------------- 1 | local ComponentDefaults = require("data.component_defaults") 2 | 3 | local function createHealthComponent(opts) 4 | opts = opts or {} 5 | 6 | local max = opts.max or ComponentDefaults.PLAYER_STARTING_HEALTH 7 | 8 | return { 9 | current = opts.current or max, 10 | max = max, 11 | } 12 | end 13 | 14 | return createHealthComponent 15 | -------------------------------------------------------------------------------- /Diablo.love/components/knockback.lua: -------------------------------------------------------------------------------- 1 | local function createKnockbackComponent(opts) 2 | opts = opts or {} 3 | 4 | local maxTimer = opts.maxTimer or 0.15 5 | 6 | return { 7 | x = opts.x or 0, 8 | y = opts.y or 0, 9 | timer = opts.timer or maxTimer, 10 | maxTimer = maxTimer, 11 | strength = opts.strength or 20, 12 | } 13 | end 14 | 15 | return createKnockbackComponent 16 | -------------------------------------------------------------------------------- /spec/system_helpers/coordinates_stub.lua: -------------------------------------------------------------------------------- 1 | local coordinates = {} 2 | 3 | function coordinates.getEntityCenter(entity) 4 | if not entity or not entity.position then 5 | return nil, nil 6 | end 7 | 8 | local w = entity.size and entity.size.w or 0 9 | local h = entity.size and entity.size.h or 0 10 | 11 | return entity.position.x + (w / 2), entity.position.y + (h / 2) 12 | end 13 | 14 | return coordinates 15 | -------------------------------------------------------------------------------- /Diablo.love/components/targeting.lua: -------------------------------------------------------------------------------- 1 | local ComponentDefaults = require("data.component_defaults") 2 | 3 | local function createTargetingComponent(opts) 4 | opts = opts or {} 5 | 6 | return { 7 | currentTargetId = opts.currentTargetId, 8 | displayTimer = opts.displayTimer or 0, 9 | keepAlive = opts.keepAlive or ComponentDefaults.TARGET_KEEP_ALIVE, 10 | } 11 | end 12 | 13 | return createTargetingComponent 14 | -------------------------------------------------------------------------------- /Diablo.love/components/loot_scatter.lua: -------------------------------------------------------------------------------- 1 | local function createLootScatterComponent(opts) 2 | opts = opts or {} 3 | 4 | return { 5 | vx = opts.vx or 0, 6 | vy = opts.vy or 0, 7 | friction = opts.friction or 8, 8 | maxDuration = opts.maxDuration or 0.5, 9 | stopThreshold = opts.stopThreshold or 8, 10 | elapsed = opts.elapsed or 0, 11 | } 12 | end 13 | 14 | return createLootScatterComponent 15 | -------------------------------------------------------------------------------- /spec/system_helpers/projectile_effects_stub.lua: -------------------------------------------------------------------------------- 1 | local projectileEffects = {} 2 | 3 | function projectileEffects.triggerImpact(_world, projectile, _opts) 4 | local component = projectile.projectile 5 | if component then 6 | component.state = "impact" 7 | end 8 | -- Movement system will clean up; collision relies on removal after impact. 9 | -- Stub: no-op in test environment 10 | end 11 | 12 | return projectileEffects 13 | -------------------------------------------------------------------------------- /Diablo.love/components/foe.lua: -------------------------------------------------------------------------------- 1 | local function createFoeTag(opts) 2 | opts = opts or {} 3 | local typeId = opts.typeId or opts.type 4 | 5 | return { 6 | type = opts.type or typeId, 7 | typeId = typeId, 8 | packId = opts.packId, 9 | packAggro = opts.packAggro or false, 10 | rarity = opts.rarity or "common", 11 | rarityLabel = opts.rarityLabel, 12 | } 13 | end 14 | 15 | return createFoeTag 16 | -------------------------------------------------------------------------------- /Diablo.love/modules/scene_kinds.lua: -------------------------------------------------------------------------------- 1 | ---Scene kind constants used across the game. 2 | ---Prevents duplicate string literals and enables centralized updates. 3 | local SceneKinds = {} 4 | 5 | SceneKinds.WORLD = "world" 6 | SceneKinds.MAIN_MENU = "main_menu" 7 | SceneKinds.GAME_OVER = "game_over" 8 | SceneKinds.INVENTORY = "inventory" 9 | SceneKinds.SKILLS = "skills" 10 | SceneKinds.PAUSE = "pause" 11 | SceneKinds.CONTROLS = "controls" 12 | SceneKinds.WORLD_MAP = "world_map" 13 | 14 | return SceneKinds 15 | -------------------------------------------------------------------------------- /Diablo.love/components/lootable.lua: -------------------------------------------------------------------------------- 1 | local function createLootableComponent(opts) 2 | opts = opts or {} 3 | 4 | return { 5 | item = opts.item, 6 | gold = opts.gold, 7 | pickupRadius = opts.pickupRadius, 8 | source = opts.source, 9 | despawnTimer = opts.despawnTimer, 10 | maxDespawnTimer = opts.maxDespawnTimer or opts.despawnTimer, 11 | iconPath = opts.iconPath, 12 | goldIcon = opts.goldIcon, 13 | } 14 | end 15 | 16 | return createLootableComponent 17 | -------------------------------------------------------------------------------- /Diablo.love/components/movement.lua: -------------------------------------------------------------------------------- 1 | local ComponentDefaults = require("data.component_defaults") 2 | 3 | local function createMovementComponent(opts) 4 | opts = opts or {} 5 | 6 | return { 7 | speed = opts.speed or ComponentDefaults.BASE_MOVEMENT_SPEED, 8 | vx = opts.vx or 0, 9 | vy = opts.vy or 0, 10 | lookDirection = opts.lookDirection or { x = 0, y = -1 }, 11 | walkAnimationTime = opts.walkAnimationTime or 0, 12 | } 13 | end 14 | 15 | return createMovementComponent 16 | -------------------------------------------------------------------------------- /Diablo.love/systems/core/physics.lua: -------------------------------------------------------------------------------- 1 | local Physics = require("modules.physics") 2 | 3 | local physicsSystem = {} 4 | 5 | function physicsSystem.update(world, dt) 6 | Physics.updateWorld(world, dt) 7 | 8 | if not world.queryEntities then 9 | return 10 | end 11 | 12 | local entities = world:queryEntities({ "physicsBody", "position", "size" }) 13 | 14 | for _, entity in ipairs(entities) do 15 | Physics.syncEntityPositionFromBody(entity) 16 | end 17 | end 18 | 19 | return physicsSystem 20 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "love" 2 | globals = { 3 | -- Standard Lua globals 4 | "require", 5 | "pairs", 6 | "ipairs", 7 | "next", 8 | "type", 9 | "assert", 10 | "pcall", 11 | "setmetatable", 12 | "math", 13 | "table", 14 | "string", 15 | "os", 16 | "tonumber", 17 | "tostring", 18 | -- Standard Lua globals for command-line tools 19 | "debug", 20 | "package", 21 | "arg", 22 | "print", 23 | -- Love2D globals (std = "love" already covers these, but explicit is fine) 24 | "love", 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main, develop ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | lint: 12 | name: Run luacheck 13 | runs-on: macos-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Install luacheck 20 | run: | 21 | brew install luacheck 22 | 23 | - name: Run luacheck 24 | run: | 25 | luacheck Diablo.love --codes --ranges 26 | -------------------------------------------------------------------------------- /spec/system_helpers/items_data_stub.lua: -------------------------------------------------------------------------------- 1 | local itemsData = { 2 | types = { 3 | health_potion = { 4 | id = "health_potion", 5 | label = "Health Potion", 6 | slot = "potion", 7 | consumable = true, 8 | restoreHealth = 50, 9 | }, 10 | mana_potion = { 11 | id = "mana_potion", 12 | label = "Mana Potion", 13 | slot = "potion", 14 | consumable = true, 15 | restoreMana = 40, 16 | }, 17 | }, 18 | } 19 | 20 | return itemsData 21 | -------------------------------------------------------------------------------- /.beads/.gitignore: -------------------------------------------------------------------------------- 1 | # SQLite databases 2 | *.db 3 | *.db?* 4 | *.db-journal 5 | *.db-wal 6 | *.db-shm 7 | 8 | # Daemon runtime files 9 | daemon.lock 10 | daemon.log 11 | daemon.pid 12 | bd.sock 13 | 14 | # Legacy database files 15 | db.sqlite 16 | bd.db 17 | 18 | # Merge artifacts (temporary files from 3-way merge) 19 | beads.base.jsonl 20 | beads.base.meta.json 21 | beads.left.jsonl 22 | beads.left.meta.json 23 | beads.right.jsonl 24 | beads.right.meta.json 25 | 26 | # Keep JSONL exports and config (source of truth for git) 27 | !issues.jsonl 28 | !metadata.json 29 | !config.json 30 | -------------------------------------------------------------------------------- /Diablo.love/components/experience.lua: -------------------------------------------------------------------------------- 1 | local Leveling = require("modules.leveling") 2 | 3 | local function createExperienceComponent(opts) 4 | opts = opts or {} 5 | 6 | local level = opts.level or 1 7 | local currentXP = opts.currentXP or 0 8 | local xpForNextLevel = Leveling.getXPRequiredForNextLevel(level) 9 | 10 | return { 11 | level = level, 12 | currentXP = currentXP, 13 | xpForNextLevel = xpForNextLevel, 14 | unallocatedPoints = opts.unallocatedPoints or 0, 15 | } 16 | end 17 | 18 | return createExperienceComponent 19 | -------------------------------------------------------------------------------- /Diablo.love/systems/ui/main.lua: -------------------------------------------------------------------------------- 1 | local uiPlayerStatus = require("systems.ui.player_status") 2 | local uiExperienceBar = require("systems.ui.experience_bar") 3 | local uiBottomBar = require("systems.ui.bottom_bar") 4 | local uiSkillsBar = require("systems.ui.skills_bar") 5 | 6 | local uiMain = {} 7 | 8 | ---Draw all UI systems in order 9 | ---@param world WorldScene The world scene 10 | function uiMain.draw(world) 11 | uiPlayerStatus.draw(world) 12 | uiExperienceBar.draw(world) 13 | uiBottomBar.draw(world) 14 | uiSkillsBar.draw(world) 15 | end 16 | 17 | return uiMain 18 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main, develop ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | busted: 12 | name: Run unit tests 13 | runs-on: macos-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Install dependencies 20 | run: | 21 | brew install luarocks 22 | luarocks install busted 23 | 24 | - name: Run busted suite 25 | run: | 26 | busted spec 27 | -------------------------------------------------------------------------------- /Diablo.love/components/physics_body.lua: -------------------------------------------------------------------------------- 1 | local function createPhysicsBodyComponent(opts) 2 | opts = opts or {} 3 | 4 | return { 5 | bodyType = opts.bodyType or "dynamic", 6 | linearDamping = opts.linearDamping or 18, 7 | fixedRotation = opts.fixedRotation ~= false, 8 | friction = opts.friction or 0, 9 | density = opts.density or 1, 10 | userData = opts.userData, 11 | body = nil, 12 | shape = nil, 13 | fixture = nil, 14 | contactNormals = nil, 15 | } 16 | end 17 | 18 | return createPhysicsBodyComponent 19 | -------------------------------------------------------------------------------- /Diablo.love/components/detection.lua: -------------------------------------------------------------------------------- 1 | local function createDetectionComponent(opts) 2 | opts = opts or {} 3 | 4 | return { 5 | range = opts.range, -- Detection radius in pixels 6 | detectedTargetId = nil, -- ID of the target being tracked 7 | leashExtension = opts.leashExtension, -- Additional distance foes will chase before disengaging 8 | leashRange = nil, -- Dynamic leash range when aggro is forced 9 | forceAggro = false, -- Whether the foe is currently forced to stay aggroed 10 | removeOnDeath = true, 11 | } 12 | end 13 | 14 | return createDetectionComponent 15 | -------------------------------------------------------------------------------- /Diablo.love/components/potions.lua: -------------------------------------------------------------------------------- 1 | local ComponentDefaults = require("data.component_defaults") 2 | 3 | local function createPotionsComponent() 4 | return { 5 | healthPotionCount = ComponentDefaults.HEALTH_POTION_STARTING_COUNT, 6 | maxHealthPotionCount = ComponentDefaults.MAX_HEALTH_POTION_COUNT, 7 | manaPotionCount = ComponentDefaults.MANA_POTION_STARTING_COUNT, 8 | maxManaPotionCount = ComponentDefaults.MAX_MANA_POTION_COUNT, 9 | cooldownDuration = ComponentDefaults.POTION_COOLDOWN_DURATION, 10 | cooldownRemaining = 0, 11 | } 12 | end 13 | 14 | return createPotionsComponent 15 | -------------------------------------------------------------------------------- /Diablo.love/systems/helpers/sprite_direction.lua: -------------------------------------------------------------------------------- 1 | local spriteDirection = {} 2 | 3 | function spriteDirection.getSpriteRow(lookDirection) 4 | if not lookDirection then 5 | return 0 6 | end 7 | 8 | local x = lookDirection.x or 0 9 | local y = lookDirection.y or 0 10 | 11 | local absX = math.abs(x) 12 | local absY = math.abs(y) 13 | 14 | if y > 0 and absY >= absX then 15 | return 0 16 | end 17 | 18 | if y < 0 and absY >= absX then 19 | return 1 20 | end 21 | 22 | if x < 0 and absX > absY then 23 | return 2 24 | end 25 | 26 | return 3 27 | end 28 | 29 | return spriteDirection 30 | -------------------------------------------------------------------------------- /Diablo.love/systems/input/mouse_targeting.lua: -------------------------------------------------------------------------------- 1 | local Targeting = require("systems.helpers.targeting") 2 | 3 | local mouseTargetingSystem = {} 4 | 5 | function mouseTargetingSystem.update(world, _dt) 6 | local input = world.input and world.input.mouse and world.input.mouse.primary 7 | local isAttacking = input and (input.held or input.pressed) 8 | 9 | if isAttacking then 10 | return 11 | end 12 | 13 | local hoverRange = 60 14 | 15 | Targeting.resolveMouseTarget(world, { 16 | range = hoverRange, 17 | checkPlayerRange = false, 18 | clearOnNoTarget = true, 19 | }) 20 | end 21 | 22 | return mouseTargetingSystem 23 | -------------------------------------------------------------------------------- /Diablo.love/components/renderable.lua: -------------------------------------------------------------------------------- 1 | local function createRenderableComponent(opts) 2 | opts = opts or {} 3 | 4 | return { 5 | kind = opts.kind or "rect", 6 | color = opts.color or { 1, 1, 1, 1 }, 7 | secondaryColor = opts.secondaryColor, 8 | coreColor = opts.coreColor, 9 | sparkleSeed = opts.sparkleSeed, 10 | spriteSheetPath = opts.spriteSheetPath, 11 | spritePrefix = opts.spritePrefix, 12 | animationState = opts.animationState or "idle", 13 | scaleMultiplier = opts.scaleMultiplier or 1, 14 | outlineColor = opts.outlineColor, 15 | } 16 | end 17 | 18 | return createRenderableComponent 19 | -------------------------------------------------------------------------------- /Diablo.love/entities/notification.lua: -------------------------------------------------------------------------------- 1 | local createNotificationComponent = require("components.notification") 2 | 3 | local Notification = {} 4 | Notification.__index = Notification 5 | 6 | ---Create a notification entity wrapping the notification component data. 7 | ---@param opts table|nil 8 | ---@return table 9 | function Notification.new(opts) 10 | opts = opts or {} 11 | 12 | local entity = { 13 | id = opts.id or ("notification_entity_" .. tostring(os.clock()):gsub("%.", "")), 14 | notification = createNotificationComponent(opts.notification or {}), 15 | } 16 | 17 | return setmetatable(entity, Notification) 18 | end 19 | 20 | return Notification 21 | -------------------------------------------------------------------------------- /Diablo.love/modules/vector.lua: -------------------------------------------------------------------------------- 1 | local vector = {} 2 | 3 | function vector.lengthSquared(dx, dy) 4 | return dx * dx + dy * dy 5 | end 6 | 7 | function vector.length(dx, dy) 8 | return math.sqrt(vector.lengthSquared(dx, dy)) 9 | end 10 | 11 | function vector.normalize(dx, dy) 12 | local len = vector.length(dx, dy) 13 | if len == 0 then 14 | return 0, 0, 0 15 | end 16 | return dx / len, dy / len, len 17 | end 18 | 19 | function vector.distanceSquared(x1, y1, x2, y2) 20 | return vector.lengthSquared(x2 - x1, y2 - y1) 21 | end 22 | 23 | function vector.distance(x1, y1, x2, y2) 24 | return math.sqrt(vector.distanceSquared(x1, y1, x2, y2)) 25 | end 26 | 27 | return vector 28 | -------------------------------------------------------------------------------- /Diablo.love/components/base_stats.lua: -------------------------------------------------------------------------------- 1 | local defaultStats = { 2 | -- Primary attributes 3 | strength = 5, 4 | dexterity = 5, 5 | vitality = 50, 6 | intelligence = 25, 7 | -- Direct stats (not derived from attributes) 8 | defense = 2, 9 | moveSpeed = 0, 10 | dodgeChance = 0, 11 | goldFind = 0, 12 | lifeSteal = 0, 13 | attackSpeed = 0, 14 | resistAll = 0, 15 | manaRegen = 0.5, 16 | } 17 | 18 | local function createBaseStatsComponent(opts) 19 | opts = opts or {} 20 | 21 | local stats = {} 22 | for key, value in pairs(defaultStats) do 23 | stats[key] = opts[key] or value 24 | end 25 | 26 | return stats 27 | end 28 | 29 | return createBaseStatsComponent 30 | -------------------------------------------------------------------------------- /Diablo.love/components/death_animation.lua: -------------------------------------------------------------------------------- 1 | ---Death animation component tracks death animation state and timing 2 | ---@param opts table|nil Optional parameters 3 | ---@return table Death animation component 4 | local function createDeathAnimation(opts) 5 | opts = opts or {} 6 | return { 7 | timer = opts.timer or 0, 8 | animationDuration = opts.animationDuration or 0.5, -- Time to play through all frames 9 | holdDuration = opts.holdDuration or 20.0, -- Time to hold last frame (20 seconds for fun!) 10 | totalFrames = opts.totalFrames or 8, -- Number of frames in death sprite sheet (8 columns) 11 | started = opts.started or false, 12 | } 13 | end 14 | 15 | return createDeathAnimation 16 | -------------------------------------------------------------------------------- /Diablo.love/modules/leveling.lua: -------------------------------------------------------------------------------- 1 | local Leveling = {} 2 | 3 | function Leveling.getXPForLevel(targetLevel) 4 | local baseXP = 100 5 | local multiplier = 1.5 6 | 7 | if not targetLevel or targetLevel <= 1 then 8 | return 0 9 | end 10 | 11 | local totalXP = 0 12 | for level = 2, targetLevel do 13 | totalXP = totalXP + math.floor(baseXP * (multiplier ^ (level - 2))) 14 | end 15 | 16 | return totalXP 17 | end 18 | 19 | function Leveling.getXPRequiredForNextLevel(currentLevel) 20 | local baseXP = 100 21 | local multiplier = 1.5 22 | 23 | if not currentLevel or currentLevel < 1 then 24 | return baseXP 25 | end 26 | 27 | return math.floor(baseXP * (multiplier ^ (currentLevel - 1))) 28 | end 29 | 30 | return Leveling 31 | -------------------------------------------------------------------------------- /Diablo.love/systems/core/mana_regen.lua: -------------------------------------------------------------------------------- 1 | local manaRegenSystem = {} 2 | 3 | ---Apply mana regeneration over time based on player's manaRegen stat 4 | ---@param world WorldScene 5 | ---@param dt number 6 | function manaRegenSystem.update(world, dt) 7 | local player = world:getPlayer() 8 | if not player or not player.mana then 9 | return 10 | end 11 | 12 | if not player.stats or not player.stats.total then 13 | return 14 | end 15 | 16 | local manaRegen = player.stats.total.manaRegen or 0 17 | if manaRegen <= 0 then 18 | return 19 | end 20 | 21 | local mana = player.mana 22 | if mana.current >= mana.max then 23 | return 24 | end 25 | 26 | mana.current = math.min(mana.current + (manaRegen * dt), mana.max) 27 | end 28 | 29 | return manaRegenSystem 30 | -------------------------------------------------------------------------------- /Diablo.love/components/combat.lua: -------------------------------------------------------------------------------- 1 | local ComponentDefaults = require("data.component_defaults") 2 | 3 | local function createCombatComponent(opts) 4 | opts = opts or {} 5 | 6 | return { 7 | attackSpeed = opts.attackSpeed or ComponentDefaults.BASE_ATTACK_SPEED, 8 | range = opts.range or ComponentDefaults.DEFAULT_COMBAT_RANGE, 9 | cooldown = opts.cooldown or 0, 10 | queuedAttack = opts.queuedAttack or nil, 11 | swingTimer = opts.swingTimer or 0, 12 | swingDuration = opts.swingDuration or ComponentDefaults.COMBAT_SWING_DURATION, 13 | baseDamageMin = opts.baseDamageMin or 5, 14 | baseDamageMax = opts.baseDamageMax or 8, 15 | critChance = opts.critChance or 0, 16 | lastAttackTime = opts.lastAttackTime, 17 | } 18 | end 19 | 20 | return createCombatComponent 21 | -------------------------------------------------------------------------------- /Diablo.love/systems/core/camera.lua: -------------------------------------------------------------------------------- 1 | local coordinates = require("systems.helpers.coordinates") 2 | 3 | local cameraSystem = {} 4 | 5 | function cameraSystem.update(world, _dt) 6 | local player = world:getPlayer() 7 | if not player or not player.position or not player.size then 8 | return 9 | end 10 | 11 | local camera = world.camera 12 | if not camera then 13 | camera = { x = 0, y = 0 } 14 | world.camera = camera 15 | end 16 | 17 | local screenWidth, screenHeight = love.graphics.getDimensions() 18 | local halfWidth = screenWidth / 2 19 | local halfHeight = screenHeight / 2 20 | 21 | local playerCenterX, playerCenterY = coordinates.getEntityCenter(player) 22 | if not playerCenterX or not playerCenterY then 23 | return 24 | end 25 | 26 | camera.x = playerCenterX - halfWidth 27 | camera.y = playerCenterY - halfHeight 28 | end 29 | 30 | return cameraSystem 31 | -------------------------------------------------------------------------------- /Diablo.love/components/blood_burst.lua: -------------------------------------------------------------------------------- 1 | local EmberEffect = require("effects.ember") 2 | 3 | local function createBloodBurst(opts) 4 | opts = opts or {} 5 | local position = opts.position or { x = 0, y = 0 } 6 | 7 | local emitter = EmberEffect.createRadialEmitter({ 8 | radius = 22, 9 | rate = 380, 10 | sizeMin = 2, 11 | sizeMax = 5, 12 | lifeBase = 0.4, 13 | speedMin = 160, 14 | speedMax = 300, 15 | driftMin = -80, 16 | driftMax = 80, 17 | pixelScale = 1.0, 18 | startColor = { 1.0, 0.15, 0.1, 1.0 }, 19 | endColor = { 0.6, 0.05, 0.03, 0.0 }, 20 | }) 21 | EmberEffect.setAnchor(emitter, position.x, position.y) 22 | EmberEffect.update(emitter, 0.04) 23 | emitter.rate = 0 24 | 25 | return { 26 | emitter = emitter, 27 | timeToLive = opts.timeToLive or 0.65, 28 | } 29 | end 30 | 31 | return createBloodBurst 32 | -------------------------------------------------------------------------------- /Diablo.love/components/projectile.lua: -------------------------------------------------------------------------------- 1 | local function createProjectileComponent(opts) 2 | opts = opts or {} 3 | 4 | return { 5 | spellId = opts.spellId, 6 | targetId = opts.targetId, 7 | targetX = opts.targetX, 8 | targetY = opts.targetY, 9 | damage = opts.damage, 10 | ownerId = opts.ownerId, 11 | lifetime = opts.lifetime or 3.0, 12 | maxLifetime = opts.maxLifetime or opts.lifetime or 3.0, 13 | speed = opts.speed, 14 | state = opts.state or "flying", 15 | impactDuration = opts.impactDuration or 0.3, 16 | impactTimer = opts.impactTimer or 0, 17 | impactStartedAt = opts.impactStartedAt or 0, 18 | impactPosition = opts.impactPosition, 19 | lastDirectionX = opts.directionX or 0, 20 | lastDirectionY = opts.directionY or 0, 21 | hitEnemies = opts.hitEnemies or {}, 22 | } 23 | end 24 | 25 | return createProjectileComponent 26 | -------------------------------------------------------------------------------- /spec/system_helpers/loot_entity_stub.lua: -------------------------------------------------------------------------------- 1 | local function cloneTable(tbl) 2 | if not tbl then 3 | return nil 4 | end 5 | 6 | local copy = {} 7 | for key, value in pairs(tbl) do 8 | if type(value) == "table" then 9 | copy[key] = cloneTable(value) 10 | else 11 | copy[key] = value 12 | end 13 | end 14 | return copy 15 | end 16 | 17 | local LootEntity = {} 18 | local lootCounter = 0 19 | 20 | function LootEntity.new(opts) 21 | opts = opts or {} 22 | lootCounter = lootCounter + 1 23 | return { 24 | id = opts.id or ("loot_entity_" .. lootCounter), 25 | position = { x = opts.x or 0, y = opts.y or 0 }, 26 | size = { w = opts.width or 16, h = opts.height or 16 }, 27 | renderable = cloneTable(opts.renderable), 28 | lootable = cloneTable(opts.lootable), 29 | lootScatter = cloneTable(opts.lootScatter), 30 | } 31 | end 32 | 33 | return LootEntity 34 | -------------------------------------------------------------------------------- /Diablo.love/systems/core/walking_animation.lua: -------------------------------------------------------------------------------- 1 | local walkingAnimationSystem = {} 2 | 3 | ---Update walking animation time for player-controlled entities 4 | ---@param world WorldScene 5 | ---@param dt number 6 | function walkingAnimationSystem.update(world, dt) 7 | local entities = world:queryEntities({ "movement", "playerControlled" }) 8 | 9 | for _, entity in ipairs(entities) do 10 | local movement = entity.movement 11 | if not movement then 12 | goto continue 13 | end 14 | 15 | -- Check if player is moving (has non-zero velocity) 16 | local isMoving = (movement.vx ~= 0) or (movement.vy ~= 0) 17 | 18 | if isMoving then 19 | -- Update animation time when moving 20 | movement.walkAnimationTime = (movement.walkAnimationTime or 0) + dt 21 | else 22 | -- Reset animation time when stopped for clean restart 23 | movement.walkAnimationTime = 0 24 | end 25 | 26 | ::continue:: 27 | end 28 | end 29 | 30 | return walkingAnimationSystem 31 | -------------------------------------------------------------------------------- /Diablo.love/systems/ai/spawn.lua: -------------------------------------------------------------------------------- 1 | local ChunkManager = require("modules.world.chunk_manager") 2 | 3 | local spawnSystem = {} 4 | 5 | local function ensurePlayerChunkLoaded(world) 6 | if not world or not world.chunkManager then 7 | return 8 | end 9 | 10 | local player 11 | if world.getPlayer then 12 | player = world:getPlayer() 13 | end 14 | local position = player and player.position 15 | if not position then 16 | return 17 | end 18 | 19 | local chunkX, chunkY = ChunkManager.getChunkCoords(world.chunkManager, position.x, position.y) 20 | ChunkManager.ensureChunkLoaded(world.chunkManager, world, chunkX, chunkY) 21 | end 22 | 23 | ---Initialize deterministic chunk data around the player. 24 | ---@param world table 25 | function spawnSystem.spawnInitialGroups(world) 26 | ensurePlayerChunkLoaded(world) 27 | end 28 | 29 | ---Spawn system now delegates to chunk streaming; keep hook for future logic. 30 | ---@param world table 31 | ---@param _dt number 32 | function spawnSystem.update(world, _dt) 33 | ensurePlayerChunkLoaded(world) 34 | end 35 | 36 | return spawnSystem 37 | -------------------------------------------------------------------------------- /Diablo.love/systems/core/lifetime_stats.lua: -------------------------------------------------------------------------------- 1 | local LifetimeStats = require("modules.lifetime_stats") 2 | 3 | local lifetimeStatsSystem = {} 4 | 5 | ---Track lifetime stats for player combat events. 6 | ---@param world table 7 | ---@param dt number|nil 8 | function lifetimeStatsSystem.update(world, dt) -- luacheck: ignore 212/dt 9 | if not world or not world.pendingCombatEvents then 10 | return 11 | end 12 | 13 | LifetimeStats.ensure(world) 14 | 15 | local playerId = world.playerId 16 | if not playerId then 17 | return 18 | end 19 | 20 | for _, event in ipairs(world.pendingCombatEvents) do 21 | if event._lifetimeTracked then 22 | goto continue 23 | end 24 | 25 | if event.type == "damage" and event.sourceId == playerId then 26 | LifetimeStats.addDamage(world, event.amount or 0) 27 | elseif event.type == "death" and event.sourceId == playerId then 28 | LifetimeStats.addKill(world) 29 | end 30 | 31 | event._lifetimeTracked = true 32 | 33 | ::continue:: 34 | end 35 | end 36 | 37 | return lifetimeStatsSystem 38 | -------------------------------------------------------------------------------- /Diablo.love/systems/core/death_animation.lua: -------------------------------------------------------------------------------- 1 | local deathAnimationSystem = {} 2 | 3 | ---Update death animation timers and remove entities after animation completes 4 | ---@param world table World scene 5 | ---@param dt number Delta time 6 | function deathAnimationSystem.update(world, dt) 7 | local dyingEntities = world:queryEntities({ "deathAnimation", "dead" }) 8 | 9 | for _, entity in ipairs(dyingEntities) do 10 | if entity.inactive and entity.inactive.isInactive then 11 | goto continue 12 | end 13 | 14 | local deathAnim = entity.deathAnimation 15 | if not deathAnim then 16 | goto continue 17 | end 18 | 19 | -- Update timer 20 | deathAnim.timer = (deathAnim.timer or 0) + dt 21 | 22 | -- Check if animation is complete 23 | local totalDuration = deathAnim.animationDuration + deathAnim.holdDuration 24 | if deathAnim.timer >= totalDuration then 25 | -- Remove the entity after animation completes 26 | world:removeEntity(entity.id) 27 | end 28 | 29 | ::continue:: 30 | end 31 | end 32 | 33 | return deathAnimationSystem 34 | -------------------------------------------------------------------------------- /Diablo.love/components/notification.lua: -------------------------------------------------------------------------------- 1 | local function createNotificationComponent(opts) 2 | opts = opts or {} 3 | 4 | return { 5 | id = opts.id, 6 | category = opts.category, 7 | title = opts.title or "", 8 | bodyLines = opts.bodyLines or {}, 9 | iconPath = opts.iconPath, 10 | priority = opts.priority or 0, 11 | ttl = opts.ttl or 5, 12 | onClickAction = opts.onClickAction, 13 | timeElapsed = opts.timeElapsed or 0, 14 | state = opts.state or "enter", 15 | stateTime = opts.stateTime or 0, 16 | enterDuration = opts.enterDuration or 0.2, 17 | exitDuration = opts.exitDuration or 0.25, 18 | dismissRequested = opts.dismissRequested or false, 19 | allowDuplicates = opts.allowDuplicates or false, 20 | sequence = opts.sequence or 0, 21 | renderX = opts.renderX or 0, 22 | renderY = opts.renderY or 0, 23 | renderWidth = opts.renderWidth or 0, 24 | renderHeight = opts.renderHeight or 0, 25 | renderAlpha = opts.renderAlpha or 0, 26 | hovered = false, 27 | } 28 | end 29 | 30 | return createNotificationComponent 31 | -------------------------------------------------------------------------------- /Diablo.love/modules/world/boss_name_generator.lua: -------------------------------------------------------------------------------- 1 | local Items = require("data.items") 2 | 3 | local BossNameGenerator = {} 4 | 5 | local adjectivePool = {} 6 | local nounPool = {} 7 | 8 | for _, affix in pairs(Items.prefixes) do 9 | if affix.name then 10 | adjectivePool[#adjectivePool + 1] = affix.name 11 | end 12 | end 13 | 14 | for _, affix in pairs(Items.suffixes) do 15 | if affix.name then 16 | nounPool[#nounPool + 1] = affix.name 17 | end 18 | end 19 | 20 | local fallbackAdjectives = { "Ancient", "Savage", "Malevolent", "Tormented", "Ravenous", "Eternal" } 21 | local fallbackNouns = { "Gloom", "Ruin", "Ash", "Thorns", "Hunger", "Night" } 22 | 23 | local function pick(rng, list, fallback) 24 | if list and #list > 0 then 25 | return list[rng:random(1, #list)] 26 | end 27 | return fallback[rng:random(1, #fallback)] 28 | end 29 | 30 | function BossNameGenerator.generate(seed) 31 | local rng = love.math.newRandomGenerator(seed or love.math.random(1, 999999)) 32 | local adjective = pick(rng, adjectivePool, fallbackAdjectives) 33 | local noun = pick(rng, nounPool, fallbackNouns) 34 | return string.format("%s of %s", adjective, noun) 35 | end 36 | 37 | return BossNameGenerator 38 | -------------------------------------------------------------------------------- /Diablo.love/modules/stats_derivation.lua: -------------------------------------------------------------------------------- 1 | ---Stats derivation module: converts primary attributes to derived stats 2 | local StatsDerivation = {} 3 | 4 | ---Derive stats from primary attributes 5 | ---@param attributes table Table with strength, dexterity, vitality, intelligence 6 | ---@return table derivedStats Derived stats table compatible with stats structure 7 | function StatsDerivation.deriveStatsFromAttributes(attributes) 8 | attributes = attributes or {} 9 | 10 | local strength = attributes.strength or 0 11 | local dexterity = attributes.dexterity or 0 12 | local vitality = attributes.vitality or 0 13 | local intelligence = attributes.intelligence or 0 14 | 15 | -- Conversion formulas: 16 | -- Strength: 5 str = +1 min/max damage (1 str = 0.2 damage) 17 | -- Dexterity: 5 dex = +0.1% crit chance (1 dex = 0.02% = 0.0002 decimal) 18 | -- Vitality: 5 vit = +5 health (1 vit = 1 health) 19 | -- Intelligence: 5 int = +5 mana (1 int = 1 mana) 20 | 21 | local derivedStats = { 22 | damageMin = strength * 0.2, 23 | damageMax = strength * 0.2, 24 | critChance = dexterity * 0.0002, 25 | health = vitality, 26 | mana = intelligence, 27 | } 28 | 29 | return derivedStats 30 | end 31 | 32 | return StatsDerivation 33 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Beads Task Tracker - GitHub Pages 2 | 3 | This directory contains a static web page that displays all beads issues from the `.beads/issues.jsonl` file. 4 | 5 | ## Setup GitHub Pages 6 | 7 | 1. Go to your repository settings on GitHub 8 | 2. Navigate to **Pages** in the left sidebar 9 | 3. Under **Source**, select **Deploy from a branch** 10 | 4. Choose **main** (or your default branch) and **/docs** folder 11 | 5. Click **Save** 12 | 13 | The page will be available at: `https://.github.io//` 14 | 15 | ## How it works 16 | 17 | The `index.html` file fetches the `.beads/issues.jsonl` file from the repository root and displays all issues in a nice, filterable UI. 18 | 19 | ## Features 20 | 21 | - **Statistics Dashboard**: Shows total issues, open/closed counts, and epic count 22 | - **Filtering**: Filter by status, type, and priority 23 | - **Search**: Search issues by title, description, or ID 24 | - **Dependency Visualization**: Shows issue dependencies and relationships 25 | - **Responsive Design**: Works on desktop and mobile devices 26 | 27 | ## Updating 28 | 29 | The page automatically reads from `.beads/issues.jsonl`, so whenever you update your beads issues (via `bd` commands), the page will reflect those changes after the next GitHub Pages deployment. 30 | -------------------------------------------------------------------------------- /Diablo.love/components/wander.lua: -------------------------------------------------------------------------------- 1 | local ComponentDefaults = require("data.component_defaults") 2 | 3 | local function createWanderComponent(opts) 4 | opts = opts or {} 5 | 6 | return { 7 | interval = opts.interval or ComponentDefaults.WANDER_INTERVAL, 8 | variance = opts.variance or ComponentDefaults.WANDER_INTERVAL_VARIANCE, 9 | elapsed = 0, 10 | removeOnDeath = true, 11 | cohesionRange = opts.cohesionRange or ComponentDefaults.WANDER_COHESION_RANGE, 12 | cohesionStrength = opts.cohesionStrength or ComponentDefaults.WANDER_COHESION_STRENGTH, 13 | separationRange = opts.separationRange or ComponentDefaults.WANDER_SEPARATION_RANGE, 14 | separationStrength = opts.separationStrength or ComponentDefaults.WANDER_SEPARATION_STRENGTH, 15 | randomWeight = opts.randomWeight or ComponentDefaults.WANDER_RANDOM_WEIGHT, 16 | cohesionImpulseWeight = opts.cohesionImpulseWeight or ComponentDefaults.WANDER_COHESION_WEIGHT, 17 | separationImpulseWeight = opts.separationImpulseWeight or ComponentDefaults.WANDER_SEPARATION_WEIGHT, 18 | cohesionSteeringWeight = opts.cohesionSteeringWeight or ComponentDefaults.WANDER_COHESION_STEERING_WEIGHT, 19 | separationSteeringWeight = opts.separationSteeringWeight or ComponentDefaults.WANDER_SEPARATION_STEERING_WEIGHT, 20 | } 21 | end 22 | 23 | return createWanderComponent 24 | -------------------------------------------------------------------------------- /Diablo.love/entities/loot.lua: -------------------------------------------------------------------------------- 1 | local Loot = {} 2 | Loot.__index = Loot 3 | 4 | function Loot.new(opts) 5 | opts = opts or {} 6 | 7 | local createPosition = require("components.position") 8 | local createRenderable = require("components.renderable") 9 | local createLootable = require("components.lootable") 10 | local createSize = require("components.size") 11 | local createLootScatter = require("components.loot_scatter") 12 | local createInactive = require("components.inactive") 13 | 14 | local entity = { 15 | id = opts.id or ("loot_" .. math.random(10000, 99999)), 16 | position = createPosition({ 17 | x = opts.x or 0, 18 | y = opts.y or 0, 19 | }), 20 | size = createSize({ 21 | w = opts.width or 16, 22 | h = opts.height or 16, 23 | }), 24 | renderable = createRenderable(opts.renderable or { 25 | kind = "loot", 26 | color = { 0.9, 0.8, 0.2, 1 }, 27 | }), 28 | lootable = createLootable(opts.lootable), 29 | inactive = createInactive(), 30 | } 31 | 32 | if opts.hoverable then 33 | entity.hoverable = opts.hoverable 34 | end 35 | 36 | if opts.lootScatter then 37 | entity.lootScatter = createLootScatter(opts.lootScatter) 38 | end 39 | 40 | return setmetatable(entity, Loot) 41 | end 42 | 43 | return Loot 44 | -------------------------------------------------------------------------------- /Diablo.love/modules/stats.lua: -------------------------------------------------------------------------------- 1 | local statKeys = { 2 | -- Primary attributes 3 | "strength", 4 | "dexterity", 5 | "vitality", 6 | "intelligence", 7 | -- Derived stats (computed from attributes) 8 | "damageMin", 9 | "damageMax", 10 | "health", 11 | "mana", 12 | "critChance", 13 | -- Direct stats (not derived from attributes) 14 | "defense", 15 | "moveSpeed", 16 | "dodgeChance", 17 | "goldFind", 18 | "lifeSteal", 19 | "attackSpeed", 20 | "resistAll", 21 | "manaRegen", 22 | } 23 | 24 | local Stats = {} 25 | 26 | function Stats.newRecord() 27 | local record = {} 28 | for _, key in ipairs(statKeys) do 29 | record[key] = 0 30 | end 31 | return record 32 | end 33 | 34 | function Stats.clone(base) 35 | local stats = Stats.newRecord() 36 | if not base then 37 | return stats 38 | end 39 | 40 | for _, key in ipairs(statKeys) do 41 | stats[key] = base[key] or stats[key] 42 | end 43 | 44 | return stats 45 | end 46 | 47 | function Stats.add(target, source) 48 | if not source then 49 | return target 50 | end 51 | 52 | for _, key in ipairs(statKeys) do 53 | if source[key] then 54 | target[key] = (target[key] or 0) + source[key] 55 | end 56 | end 57 | 58 | return target 59 | end 60 | 61 | Stats.keys = statKeys 62 | 63 | return Stats 64 | -------------------------------------------------------------------------------- /Diablo.love/systems/core/foe_animation.lua: -------------------------------------------------------------------------------- 1 | local foeAnimationSystem = {} 2 | 3 | function foeAnimationSystem.update(world, dt) 4 | local entities = world:queryEntities({ "movement", "foe", "combat" }) 5 | 6 | for _, entity in ipairs(entities) do 7 | local movement = entity.movement 8 | local renderable = entity.renderable 9 | if not renderable or not renderable.spritePrefix then 10 | goto continue 11 | end 12 | 13 | local combat = entity.combat 14 | local isAttacking = combat and combat.swingTimer and combat.swingTimer > 0 15 | 16 | if isAttacking then 17 | if not combat.attackAnimationTime then 18 | combat.attackAnimationTime = 0 19 | end 20 | combat.attackAnimationTime = combat.attackAnimationTime + dt 21 | renderable.animationState = "attacking" 22 | else 23 | local isMoving = (movement.vx ~= 0) or (movement.vy ~= 0) 24 | 25 | if isMoving then 26 | movement.walkAnimationTime = (movement.walkAnimationTime or 0) + dt 27 | renderable.animationState = "walking" 28 | else 29 | movement.walkAnimationTime = (movement.walkAnimationTime or 0) + dt 30 | renderable.animationState = "idle" 31 | end 32 | end 33 | 34 | ::continue:: 35 | end 36 | end 37 | 38 | return foeAnimationSystem 39 | -------------------------------------------------------------------------------- /spec/spec_helper.lua: -------------------------------------------------------------------------------- 1 | -- Ensure the game's source directory is on the Lua search path for tests. 2 | local repoRoot = debug.getinfo(1, "S").source:sub(2):match("(.*/)") 3 | if repoRoot then 4 | local projectRoot = repoRoot:match("^(.*)/spec/") 5 | if projectRoot then 6 | local sourcePath = projectRoot .. "/Diablo.love/?.lua" 7 | local initPath = projectRoot .. "/Diablo.love/?/init.lua" 8 | local specPath = projectRoot .. "/spec/?.lua" 9 | local specInitPath = projectRoot .. "/spec/?/init.lua" 10 | package.path = string.format( 11 | "%s;%s;%s;%s;%s", 12 | sourcePath, 13 | initPath, 14 | specPath, 15 | specInitPath, 16 | package.path 17 | ) 18 | end 19 | end 20 | 21 | -- Helper to build basic entities for tests. 22 | local entityCounter = 0 23 | 24 | local function buildEntity(opts) 25 | opts = opts or {} 26 | entityCounter = entityCounter + 1 27 | local entity = { 28 | id = opts.id or ("entity_" .. tostring(entityCounter)), 29 | position = opts.position or { x = 0, y = 0 }, 30 | size = opts.size or { w = 20, h = 20 }, 31 | } 32 | 33 | for key, value in pairs(opts) do 34 | if key ~= "id" and key ~= "position" and key ~= "size" then 35 | entity[key] = value 36 | end 37 | end 38 | 39 | return entity 40 | end 41 | 42 | return { 43 | buildEntity = buildEntity, 44 | } 45 | -------------------------------------------------------------------------------- /Diablo.love/systems/input/mouse_input.lua: -------------------------------------------------------------------------------- 1 | local mouseInputSystem = {} 2 | 3 | local function ensurePrimary(scene) 4 | scene.input = scene.input or {} 5 | local input = scene.input 6 | 7 | input.mouse = input.mouse or {} 8 | local mouse = input.mouse 9 | 10 | mouse.primary = mouse.primary 11 | or { 12 | held = false, 13 | pressed = false, 14 | released = false, 15 | clickId = 0, 16 | consumedClickId = nil, 17 | _pressedFrame = false, 18 | _releasedFrame = false, 19 | } 20 | 21 | local primary = mouse.primary 22 | primary.clickId = primary.clickId or 0 23 | 24 | return primary 25 | end 26 | 27 | function mouseInputSystem.queuePress(scene) 28 | local primary = ensurePrimary(scene) 29 | 30 | primary._pressedFrame = true 31 | primary.held = true 32 | primary.clickId = (primary.clickId or 0) + 1 33 | primary.consumedClickId = nil 34 | end 35 | 36 | function mouseInputSystem.queueRelease(scene) 37 | local primary = ensurePrimary(scene) 38 | 39 | primary._releasedFrame = true 40 | primary.held = false 41 | end 42 | 43 | function mouseInputSystem.update(scene, _dt) 44 | local primary = ensurePrimary(scene) 45 | 46 | primary.pressed = primary._pressedFrame 47 | primary.released = primary._releasedFrame 48 | 49 | primary._pressedFrame = false 50 | primary._releasedFrame = false 51 | end 52 | 53 | return mouseInputSystem 54 | -------------------------------------------------------------------------------- /Diablo.love/components/floating_damage.lua: -------------------------------------------------------------------------------- 1 | local function createFloatingDamageComponent(opts) 2 | opts = opts or {} 3 | local position = opts.position or {} 4 | local velocity = opts.velocity or {} 5 | local color = opts.color or {} 6 | local shadowColor = opts.shadowColor or {} 7 | 8 | return { 9 | damage = opts.damage or 0, 10 | position = { 11 | x = position.x or 0, 12 | y = position.y or 0, 13 | }, 14 | velocity = { 15 | x = velocity.x or 0, 16 | y = velocity.y or 0, 17 | }, 18 | timer = opts.timer or 1, 19 | maxTimer = opts.maxTimer or opts.timer or 1, 20 | color = { 21 | color[1] or 1, 22 | color[2] or 1, 23 | color[3] or 1, 24 | color[4] or 1, 25 | }, 26 | shadowColor = { 27 | shadowColor[1] or 0, 28 | shadowColor[2] or 0, 29 | shadowColor[3] or 0, 30 | shadowColor[4] or 0.7, 31 | }, 32 | crit = opts.crit or false, 33 | scaleStart = opts.scaleStart or 1, 34 | scaleEnd = opts.scaleEnd or 0.9, 35 | wobbleAmplitude = opts.wobbleAmplitude or 0, 36 | wobbleFrequency = opts.wobbleFrequency or 0, 37 | wobbleOffset = opts.wobbleOffset or math.random() * math.pi * 2, 38 | flashDuration = opts.flashDuration or 0.12, 39 | elapsed = 0, 40 | } 41 | end 42 | 43 | return createFloatingDamageComponent 44 | -------------------------------------------------------------------------------- /Diablo.love/modules/input_actions.lua: -------------------------------------------------------------------------------- 1 | ---Input action name constants for centralized input management. 2 | ---Prevents typos and enables IDE autocomplete. 3 | local InputActions = {} 4 | 5 | -- Movement actions (continuous/held) 6 | InputActions.MOVE_LEFT = "move_left" 7 | InputActions.MOVE_RIGHT = "move_right" 8 | InputActions.MOVE_UP = "move_up" 9 | InputActions.MOVE_DOWN = "move_down" 10 | 11 | -- Skill actions (press events) 12 | InputActions.SKILL_1 = "skill_1" 13 | InputActions.SKILL_2 = "skill_2" 14 | InputActions.SKILL_3 = "skill_3" 15 | InputActions.SKILL_4 = "skill_4" 16 | 17 | -- Potion actions (press events) 18 | InputActions.POTION_HEALTH = "potion_health" 19 | InputActions.POTION_MANA = "potion_mana" 20 | 21 | -- UI actions (press events) 22 | InputActions.TOGGLE_INVENTORY = "toggle_inventory" 23 | InputActions.TOGGLE_SKILLS = "toggle_skills" 24 | InputActions.TOGGLE_WORLD_MAP = "toggle_world_map" 25 | InputActions.CLOSE_MODAL = "close_modal" 26 | 27 | -- Debug actions (press events) 28 | InputActions.DEBUG_TOGGLE = "debug_toggle" 29 | InputActions.DEBUG_CHUNKS = "debug_chunks" 30 | InputActions.RESET_WORLD = "reset_world" 31 | 32 | -- Minimap actions (press events) 33 | InputActions.MINIMAP_TOGGLE = "minimap_toggle" 34 | InputActions.MINIMAP_ZOOM_IN = "minimap_zoom_in" 35 | InputActions.MINIMAP_ZOOM_OUT = "minimap_zoom_out" 36 | 37 | -- Dev actions (press events) 38 | InputActions.INVENTORY_TEST_ITEM = "inventory_test_item" 39 | 40 | -- Mouse actions 41 | InputActions.MOUSE_PRIMARY = "mouse_primary" 42 | InputActions.MOUSE_SECONDARY = "mouse_secondary" 43 | 44 | return InputActions 45 | -------------------------------------------------------------------------------- /Diablo.love/systems/core/loot_scatter.lua: -------------------------------------------------------------------------------- 1 | local lootScatterSystem = {} 2 | 3 | function lootScatterSystem.update(world, dt) 4 | if not world or not dt then 5 | return 6 | end 7 | 8 | local lootEntities = world:queryEntities({ "lootScatter", "position" }) 9 | if not lootEntities or #lootEntities == 0 then 10 | return 11 | end 12 | 13 | for _, loot in ipairs(lootEntities) do 14 | if loot.inactive and loot.inactive.isInactive then 15 | world:removeComponent(loot.id, "lootScatter") 16 | goto continue 17 | end 18 | 19 | local scatter = loot.lootScatter 20 | if not scatter then 21 | goto continue 22 | end 23 | 24 | scatter.elapsed = (scatter.elapsed or 0) + dt 25 | 26 | local vx = scatter.vx or 0 27 | local vy = scatter.vy or 0 28 | 29 | loot.position.x = loot.position.x + vx * dt 30 | loot.position.y = loot.position.y + vy * dt 31 | 32 | local friction = scatter.friction or 8 33 | local decay = math.max(0, 1 - friction * dt) 34 | scatter.vx = vx * decay 35 | scatter.vy = vy * decay 36 | 37 | local threshold = scatter.stopThreshold or 8 38 | local speedSq = scatter.vx * scatter.vx + scatter.vy * scatter.vy 39 | local shouldStop = scatter.elapsed >= (scatter.maxDuration or 0.5) 40 | or speedSq <= (threshold * threshold) 41 | 42 | if shouldStop then 43 | world:removeComponent(loot.id, "lootScatter") 44 | end 45 | 46 | ::continue:: 47 | end 48 | end 49 | 50 | return lootScatterSystem 51 | -------------------------------------------------------------------------------- /Diablo.love/systems/input/mouse_look.lua: -------------------------------------------------------------------------------- 1 | local mouseLookSystem = {} 2 | 3 | function mouseLookSystem.update(scene, _dt) 4 | local entities = scene:queryEntities({ "movement", "playerControlled" }) 5 | 6 | for _, entity in ipairs(entities) do 7 | if not entity.position or not entity.movement then 8 | goto continue 9 | end 10 | 11 | -- Get mouse position in screen coordinates 12 | local screenX, screenY = love.mouse.getPosition() 13 | 14 | -- Convert to world coordinates 15 | local camera = scene.camera or { x = 0, y = 0 } 16 | local coordinates = scene.systemHelpers and scene.systemHelpers.coordinates 17 | if not coordinates then 18 | goto continue 19 | end 20 | 21 | local worldX, worldY = coordinates.toWorldFromScreen(camera, screenX, screenY) 22 | 23 | -- Compute direction vector from player position to mouse cursor 24 | local ndx, ndy = coordinates.directionFromEntityToWorld(entity, worldX, worldY) 25 | if not ndx or not ndy then 26 | goto continue 27 | end 28 | 29 | -- Ensure lookDirection exists (should always exist due to component default, but safety check) 30 | if not entity.movement.lookDirection then 31 | entity.movement.lookDirection = { x = 0, y = -1 } 32 | end 33 | 34 | -- Mutate existing lookDirection table to preserve ECS component reference 35 | entity.movement.lookDirection.x = ndx 36 | entity.movement.lookDirection.y = ndy 37 | 38 | ::continue:: 39 | end 40 | end 41 | 42 | return mouseLookSystem 43 | -------------------------------------------------------------------------------- /Diablo.love/systems/input/player_input.lua: -------------------------------------------------------------------------------- 1 | local vector = require("modules.vector") 2 | local InputManager = require("modules.input_manager") 3 | local InputActions = require("modules.input_actions") 4 | 5 | local playerInputSystem = {} 6 | 7 | function playerInputSystem.update(world, _dt) 8 | local entities = world:queryEntities({ "movement", "playerControlled" }) 9 | 10 | for _, entity in ipairs(entities) do 11 | local movement = entity.movement 12 | local dx, dy = 0, 0 13 | 14 | if InputManager.isActionDown(InputActions.MOVE_LEFT) then 15 | dx = dx - 1 16 | end 17 | if InputManager.isActionDown(InputActions.MOVE_RIGHT) then 18 | dx = dx + 1 19 | end 20 | if InputManager.isActionDown(InputActions.MOVE_UP) then 21 | dy = dy - 1 22 | end 23 | if InputManager.isActionDown(InputActions.MOVE_DOWN) then 24 | dy = dy + 1 25 | end 26 | 27 | movement.vx = dx 28 | movement.vy = dy 29 | 30 | -- Update look direction based on keyboard movement 31 | -- (mouse look will override this if mouse is moving) 32 | if dx ~= 0 or dy ~= 0 then 33 | local ndx, ndy = vector.normalize(dx, dy) 34 | if ndx ~= 0 or ndy ~= 0 then 35 | -- Ensure lookDirection exists 36 | if not movement.lookDirection then 37 | movement.lookDirection = { x = 0, y = -1 } 38 | end 39 | -- Update look direction based on movement 40 | movement.lookDirection.x = ndx 41 | movement.lookDirection.y = ndy 42 | end 43 | end 44 | end 45 | end 46 | 47 | return playerInputSystem 48 | -------------------------------------------------------------------------------- /spec/support/test_world.lua: -------------------------------------------------------------------------------- 1 | local TestWorld = {} 2 | TestWorld.__index = TestWorld 3 | 4 | ---Create a lightweight ECS-like world for unit testing systems. 5 | ---@return table 6 | function TestWorld.new() 7 | local world = { 8 | entities = {}, 9 | nextEntityIndex = 0, 10 | } 11 | 12 | return setmetatable(world, TestWorld) 13 | end 14 | 15 | function TestWorld:addEntity(entity) 16 | self.entities[entity.id] = entity 17 | self.nextEntityIndex = self.nextEntityIndex + 1 18 | end 19 | 20 | function TestWorld:removeEntity(entityId) 21 | self.entities[entityId] = nil 22 | end 23 | 24 | function TestWorld:getEntity(entityId) 25 | return self.entities[entityId] 26 | end 27 | 28 | function TestWorld:addComponent(entityId, componentName, component) 29 | local entity = self.entities[entityId] 30 | if not entity then 31 | -- luacheck: globals error 32 | error(("Unknown entity '%s'"):format(tostring(entityId))) 33 | end 34 | 35 | entity[componentName] = component 36 | end 37 | 38 | function TestWorld:removeComponent(entityId, componentName) 39 | local entity = self.entities[entityId] 40 | if entity then 41 | entity[componentName] = nil 42 | end 43 | end 44 | 45 | function TestWorld:queryEntities(componentNames) 46 | local results = {} 47 | 48 | for _, entity in pairs(self.entities) do 49 | local matches = true 50 | 51 | for _, name in ipairs(componentNames) do 52 | if not entity[name] then 53 | matches = false 54 | break 55 | end 56 | end 57 | 58 | if matches then 59 | results[#results + 1] = entity 60 | end 61 | end 62 | 63 | return results 64 | end 65 | 66 | return TestWorld 67 | -------------------------------------------------------------------------------- /Diablo.love/data/component_defaults.lua: -------------------------------------------------------------------------------- 1 | local ComponentDefaults = {} 2 | 3 | -- Inventory defaults 4 | ComponentDefaults.INVENTORY_CAPACITY = 80 5 | 6 | -- Potion defaults 7 | ComponentDefaults.HEALTH_POTION_STARTING_COUNT = 3 8 | ComponentDefaults.MAX_HEALTH_POTION_COUNT = 10 9 | ComponentDefaults.MANA_POTION_STARTING_COUNT = 2 10 | ComponentDefaults.MAX_MANA_POTION_COUNT = 10 11 | ComponentDefaults.POTION_COOLDOWN_DURATION = 0.5 12 | 13 | -- Visual feedback defaults 14 | ComponentDefaults.DAMAGE_FLASH_DURATION = 0.5 15 | 16 | -- Player starting values 17 | ComponentDefaults.PLAYER_STARTING_HEALTH = 50 18 | ComponentDefaults.PLAYER_STARTING_MANA = 25 19 | ComponentDefaults.BASE_MOVEMENT_SPEED = 140 20 | ComponentDefaults.PLAYER_COMBAT_RANGE = 100 21 | 22 | -- Combat defaults 23 | ComponentDefaults.COMBAT_SWING_DURATION = 0.35 24 | ComponentDefaults.DEFAULT_COMBAT_RANGE = 120 25 | ComponentDefaults.BASE_ATTACK_SPEED = 1.0 26 | ComponentDefaults.MIN_ATTACK_SPEED = 0.1 27 | ComponentDefaults.MIN_ATTACK_SPEED_MULTIPLIER = 0.1 28 | 29 | -- Entity state defaults 30 | ComponentDefaults.INACTIVE_STATE = false 31 | 32 | -- Targeting defaults 33 | ComponentDefaults.TARGET_KEEP_ALIVE = 1.5 34 | 35 | -- Wander behaviour defaults 36 | ComponentDefaults.WANDER_INTERVAL = 1.5 37 | ComponentDefaults.WANDER_INTERVAL_VARIANCE = 0.3 38 | ComponentDefaults.WANDER_COHESION_RANGE = 120 39 | ComponentDefaults.WANDER_COHESION_STRENGTH = 0.1 40 | ComponentDefaults.WANDER_SEPARATION_RANGE = 70 41 | ComponentDefaults.WANDER_SEPARATION_STRENGTH = 0.5 42 | ComponentDefaults.WANDER_RANDOM_WEIGHT = 1.0 43 | ComponentDefaults.WANDER_COHESION_WEIGHT = 1.0 44 | ComponentDefaults.WANDER_SEPARATION_WEIGHT = 1.5 45 | ComponentDefaults.WANDER_COHESION_STEERING_WEIGHT = 0.5 46 | ComponentDefaults.WANDER_SEPARATION_STEERING_WEIGHT = 1.2 47 | 48 | return ComponentDefaults 49 | -------------------------------------------------------------------------------- /Diablo.love/systems/helpers/coordinates.lua: -------------------------------------------------------------------------------- 1 | local vector = require("modules.vector") 2 | 3 | local coordinates = {} 4 | 5 | ---Convert screen coordinates to world coordinates 6 | ---@param camera table Camera object with x and y properties 7 | ---@param screenX number Screen X coordinate 8 | ---@param screenY number Screen Y coordinate 9 | ---@return number worldX, number worldY 10 | function coordinates.toWorldFromScreen(camera, screenX, screenY) 11 | local worldX = screenX + camera.x 12 | local worldY = screenY + camera.y 13 | return worldX, worldY 14 | end 15 | 16 | ---Calculate the center point for an entity using its position and size. 17 | ---@param entity table 18 | ---@return number|nil centerX, number|nil centerY 19 | function coordinates.getEntityCenter(entity) 20 | if not entity or not entity.position then 21 | return nil, nil 22 | end 23 | 24 | local size = entity.size 25 | local halfWidth = size and size.w and (size.w / 2) or 0 26 | local halfHeight = size and size.h and (size.h / 2) or 0 27 | 28 | return entity.position.x + halfWidth, entity.position.y + halfHeight 29 | end 30 | 31 | ---Compute the normalized direction vector and distance from an entity to a world point. 32 | ---@param entity table 33 | ---@param worldX number 34 | ---@param worldY number 35 | ---@return number|nil ndx, number|nil ndy, number|nil distance, number|nil centerX, number|nil centerY 36 | function coordinates.directionFromEntityToWorld(entity, worldX, worldY) 37 | local centerX, centerY = coordinates.getEntityCenter(entity) 38 | if not centerX or not centerY then 39 | return nil, nil, nil, nil, nil 40 | end 41 | 42 | local dx = worldX - centerX 43 | local dy = worldY - centerY 44 | local ndx, ndy, distance = vector.normalize(dx, dy) 45 | 46 | return ndx, ndy, distance, centerX, centerY 47 | end 48 | 49 | return coordinates 50 | -------------------------------------------------------------------------------- /Diablo.love/systems/render/blood_burst.lua: -------------------------------------------------------------------------------- 1 | local EmberEffect = require("effects.ember") 2 | local coordinates = require("systems.helpers.coordinates") 3 | 4 | local renderBloodBurstSystem = {} 5 | 6 | function renderBloodBurstSystem.draw(world) 7 | local dt = world.lastUpdateDt or 0 8 | if dt <= 0 then 9 | dt = 1 / 60 10 | end 11 | 12 | local entities = world:queryEntities({ "bloodBurst", "position" }) 13 | local toRemove = {} 14 | 15 | love.graphics.push("all") 16 | local camera = world.camera or { x = 0, y = 0 } 17 | love.graphics.translate(-camera.x, -camera.y) 18 | 19 | for _, entity in ipairs(entities) do 20 | local bloodBurst = entity.bloodBurst 21 | if not bloodBurst or not bloodBurst.emitter then 22 | goto continue 23 | end 24 | 25 | -- Update emitter anchor position based on entity's current position 26 | local centerX, centerY = coordinates.getEntityCenter(entity) 27 | if centerX and centerY then 28 | EmberEffect.setAnchor(bloodBurst.emitter, centerX, centerY) 29 | end 30 | 31 | -- Update and render particles 32 | EmberEffect.update(bloodBurst.emitter, dt) 33 | EmberEffect.drawParticles(bloodBurst.emitter) 34 | 35 | -- Decrement time to live 36 | bloodBurst.timeToLive = bloodBurst.timeToLive - dt 37 | 38 | -- Remove component when expired or particles are gone 39 | local particles = bloodBurst.emitter.particles or {} 40 | if bloodBurst.timeToLive <= 0 and #particles == 0 then 41 | toRemove[#toRemove + 1] = entity.id 42 | end 43 | 44 | ::continue:: 45 | end 46 | 47 | love.graphics.pop() 48 | 49 | -- Remove components from entities 50 | for _, id in ipairs(toRemove) do 51 | world:removeComponent(id, "bloodBurst") 52 | end 53 | end 54 | 55 | return renderBloodBurstSystem 56 | -------------------------------------------------------------------------------- /Diablo.love/systems/input/mouse_movement.lua: -------------------------------------------------------------------------------- 1 | local InputManager = require("modules.input_manager") 2 | local InputActions = require("modules.input_actions") 3 | 4 | local mouseMovementSystem = {} 5 | 6 | function mouseMovementSystem.update(scene, _dt) 7 | -- Only process if right mouse button is held 8 | if not InputManager.isActionDown(InputActions.MOUSE_SECONDARY) then 9 | return 10 | end 11 | 12 | local entities = scene:queryEntities({ "movement", "playerControlled" }) 13 | 14 | for _, entity in ipairs(entities) do 15 | if not entity.position or not entity.movement then 16 | goto continue 17 | end 18 | 19 | -- Get mouse position in screen coordinates 20 | local screenX, screenY = love.mouse.getPosition() 21 | 22 | -- Convert to world coordinates 23 | local camera = scene.camera or { x = 0, y = 0 } 24 | local coordinates = scene.systemHelpers and scene.systemHelpers.coordinates 25 | if not coordinates then 26 | goto continue 27 | end 28 | 29 | local worldX, worldY = coordinates.toWorldFromScreen(camera, screenX, screenY) 30 | 31 | -- Compute direction vector from player to mouse cursor 32 | local ndx, ndy, distance = coordinates.directionFromEntityToWorld(entity, worldX, worldY) 33 | if not ndx or not ndy or not distance then 34 | goto continue 35 | end 36 | 37 | -- Threshold for stopping movement (prevents jitter when reaching destination) 38 | local threshold = 8 39 | 40 | if distance <= threshold then 41 | -- Close enough: stop movement 42 | entity.movement.vx = 0 43 | entity.movement.vy = 0 44 | else 45 | entity.movement.vx = ndx 46 | entity.movement.vy = ndy 47 | end 48 | 49 | ::continue:: 50 | end 51 | end 52 | 53 | return mouseMovementSystem 54 | -------------------------------------------------------------------------------- /Diablo.love/data/foe_rarities.lua: -------------------------------------------------------------------------------- 1 | local rarities = { 2 | common = { 3 | id = "common", 4 | label = "Common", 5 | healthMultiplier = 1.0, 6 | damageMultiplier = 1.0, 7 | detectionMultiplier = 1.0, 8 | leashMultiplier = 1.0, 9 | scaleMultiplier = 1.0, 10 | forcePackAggro = false, 11 | tint = { 1, 1, 1, 1 }, 12 | experienceMultiplier = 1.0, 13 | itemDropChanceMultiplier = 1.0, 14 | itemDropCount = { min = 1, max = 1 }, 15 | goldChanceMultiplier = 1.0, 16 | goldAmountMultiplier = 1.0, 17 | }, 18 | elite = { 19 | id = "elite", 20 | label = "Elite", 21 | healthMultiplier = 2.1, 22 | damageMultiplier = 1.6, 23 | detectionMultiplier = 1.25, 24 | leashMultiplier = 1.35, 25 | scaleMultiplier = 1.12, 26 | forcePackAggro = true, 27 | tint = { 0.35, 0.65, 1.0, 1 }, 28 | experienceMultiplier = 1.35, 29 | itemDropChanceMultiplier = 1.35, 30 | itemDropCount = { min = 1, max = 1 }, 31 | goldChanceMultiplier = 1.25, 32 | goldAmountMultiplier = 1.35, 33 | }, 34 | boss = { 35 | id = "boss", 36 | label = "Boss", 37 | healthMultiplier = 4.0, 38 | damageMultiplier = 2.4, 39 | detectionMultiplier = 1.5, 40 | leashMultiplier = 1.6, 41 | scaleMultiplier = 1.25, 42 | forcePackAggro = true, 43 | tint = { 0.75, 0.35, 1.0, 1 }, 44 | experienceMultiplier = 1.9, 45 | itemDropChanceMultiplier = 1.0, -- Bosses always drop via item count below 46 | itemDropCount = { min = 2, max = 3 }, 47 | goldChanceMultiplier = 1.5, 48 | goldAmountMultiplier = 2.0, 49 | }, 50 | } 51 | 52 | local foeRarities = {} 53 | 54 | function foeRarities.getById(id) 55 | return rarities[id] or rarities.common 56 | end 57 | 58 | function foeRarities.getAll() 59 | return rarities 60 | end 61 | 62 | return foeRarities 63 | -------------------------------------------------------------------------------- /Diablo.love/modules/power_glow_shader.lua: -------------------------------------------------------------------------------- 1 | local PowerGlowShader = {} 2 | 3 | local shader = nil 4 | local shaderPath = "shaders/power_glow.glsl" 5 | 6 | local defaultSettings = { 7 | intensity = 0.5, 8 | pulseSpeed = 0.5, 9 | glowRadius = 0.6, 10 | distortionStrength = 1.5, 11 | } 12 | 13 | local function loadShader() 14 | if shader then 15 | return shader 16 | end 17 | 18 | local source, err = love.filesystem.read(shaderPath) 19 | if not source then 20 | print(string.format("Power glow shader missing (%s): %s", shaderPath, err)) 21 | return nil 22 | end 23 | 24 | local ok, compiledOrError = pcall(love.graphics.newShader, source) 25 | if not ok then 26 | print("Failed to compile power glow shader: " .. tostring(compiledOrError)) 27 | return nil 28 | end 29 | 30 | shader = compiledOrError 31 | return shader 32 | end 33 | 34 | function PowerGlowShader.getShader() 35 | return loadShader() 36 | end 37 | 38 | function PowerGlowShader.apply(entity, world, outlineColor, drawFunc) 39 | local rarityId = entity.foe and entity.foe.rarity 40 | local isPowerful = rarityId == "elite" or rarityId == "boss" 41 | 42 | if not isPowerful or not drawFunc then 43 | drawFunc() 44 | return 45 | end 46 | 47 | local shaderInstance = loadShader() 48 | if not shaderInstance then 49 | drawFunc() 50 | return 51 | end 52 | 53 | local time = world.time or 0 54 | local glowColor = outlineColor or { 1.0, 0.8, 0.2 } 55 | 56 | -- Send uniforms 57 | shaderInstance:send("time", time) 58 | shaderInstance:send("intensity", defaultSettings.intensity) 59 | shaderInstance:send("glowColor", glowColor) 60 | shaderInstance:send("pulseSpeed", defaultSettings.pulseSpeed) 61 | shaderInstance:send("glowRadius", defaultSettings.glowRadius) 62 | shaderInstance:send("distortionStrength", defaultSettings.distortionStrength) 63 | 64 | love.graphics.setShader(shaderInstance) 65 | drawFunc() 66 | love.graphics.setShader() 67 | end 68 | 69 | return PowerGlowShader 70 | -------------------------------------------------------------------------------- /Diablo.love/modules/world/biome_resolver.lua: -------------------------------------------------------------------------------- 1 | local biomes = require("data.biomes") 2 | 3 | local BiomeResolver = {} 4 | 5 | local NOISE_SCALE = 0.05 6 | 7 | local function remap(value, inMin, inMax) 8 | if inMax == inMin then 9 | return 0 10 | end 11 | return (value - inMin) / (inMax - inMin) 12 | end 13 | 14 | local function clamp(value, minValue, maxValue) 15 | if value < minValue then 16 | return minValue 17 | end 18 | if value > maxValue then 19 | return maxValue 20 | end 21 | return value 22 | end 23 | 24 | function BiomeResolver.resolveChunk(worldSeed, chunkX, chunkY) 25 | local seedOffset = worldSeed * 0.0001 26 | local nx = chunkX * NOISE_SCALE + seedOffset 27 | local ny = chunkY * NOISE_SCALE - seedOffset 28 | local noiseValue = love.math.noise(nx, ny) 29 | 30 | local biome = biomes.findByNoiseValue(noiseValue) 31 | local normalized = remap(noiseValue, biome.noise.min, biome.noise.max) 32 | normalized = clamp(normalized, 0, 1) 33 | local distanceToEdge = math.min(normalized, 1 - normalized) 34 | local transitionStrength = clamp(1 - distanceToEdge * 4, 0, 1) 35 | 36 | local neighborSamples = {} 37 | local directions = { 38 | { dx = 1, dy = 0 }, 39 | { dx = -1, dy = 0 }, 40 | { dx = 0, dy = 1 }, 41 | { dx = 0, dy = -1 }, 42 | } 43 | 44 | for _, dir in ipairs(directions) do 45 | local neighborNoiseX = (chunkX + dir.dx) * NOISE_SCALE + seedOffset 46 | local neighborNoiseY = (chunkY + dir.dy) * NOISE_SCALE - seedOffset 47 | local neighborValue = love.math.noise(neighborNoiseX, neighborNoiseY) 48 | neighborSamples[#neighborSamples + 1] = { 49 | dx = dir.dx, 50 | dy = dir.dy, 51 | biome = biomes.findByNoiseValue(neighborValue).id, 52 | noiseValue = neighborValue, 53 | } 54 | end 55 | 56 | return biome.id, { 57 | noiseValue = noiseValue, 58 | normalized = normalized, 59 | transitionStrength = transitionStrength, 60 | neighbors = neighborSamples, 61 | } 62 | end 63 | 64 | return BiomeResolver 65 | -------------------------------------------------------------------------------- /Diablo.love/modules/lifetime_stats.lua: -------------------------------------------------------------------------------- 1 | local LifetimeStats = {} 2 | 3 | local function clampNonNegative(value) 4 | value = value or 0 5 | if value < 0 then 6 | return 0 7 | end 8 | return value 9 | end 10 | 11 | ---Normalize a lifetime stats table into a safe structure. 12 | ---@param stats table|nil 13 | ---@return table 14 | function LifetimeStats.normalize(stats) 15 | return { 16 | foesKilled = clampNonNegative(stats and stats.foesKilled), 17 | damageDealt = clampNonNegative(stats and stats.damageDealt), 18 | experienceEarned = clampNonNegative(stats and stats.experienceEarned), 19 | levelsGained = clampNonNegative(stats and stats.levelsGained), 20 | } 21 | end 22 | 23 | ---Ensure lifetime stats exist on the world (or return a normalized copy for a snapshot). 24 | ---@param world table|nil 25 | ---@param initial table|nil 26 | ---@return table 27 | function LifetimeStats.ensure(world, initial) 28 | if not world then 29 | return LifetimeStats.normalize(initial) 30 | end 31 | 32 | if world.lifetimeStats then 33 | world.lifetimeStats = LifetimeStats.normalize(world.lifetimeStats) 34 | return world.lifetimeStats 35 | end 36 | 37 | world.lifetimeStats = LifetimeStats.normalize(initial) 38 | return world.lifetimeStats 39 | end 40 | 41 | function LifetimeStats.addDamage(world, amount) 42 | local stats = LifetimeStats.ensure(world) 43 | stats.damageDealt = clampNonNegative(stats.damageDealt) + clampNonNegative(amount) 44 | end 45 | 46 | function LifetimeStats.addKill(world) 47 | local stats = LifetimeStats.ensure(world) 48 | stats.foesKilled = clampNonNegative(stats.foesKilled) + 1 49 | end 50 | 51 | function LifetimeStats.addExperience(world, amount) 52 | local stats = LifetimeStats.ensure(world) 53 | stats.experienceEarned = clampNonNegative(stats.experienceEarned) + clampNonNegative(amount) 54 | end 55 | 56 | function LifetimeStats.addLevels(world, amount) 57 | local stats = LifetimeStats.ensure(world) 58 | stats.levelsGained = clampNonNegative(stats.levelsGained) + clampNonNegative(amount) 59 | end 60 | 61 | return LifetimeStats 62 | -------------------------------------------------------------------------------- /.beads/config.yaml: -------------------------------------------------------------------------------- 1 | # Beads Configuration File 2 | # This file configures default behavior for all bd commands in this repository 3 | # All settings can also be set via environment variables (BD_* prefix) 4 | # or overridden with command-line flags 5 | 6 | # Issue prefix for this repository (used by bd init) 7 | # If not set, bd init will auto-detect from directory name 8 | # Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc. 9 | # issue-prefix: "" 10 | 11 | # Use no-db mode: load from JSONL, no SQLite, write back after each command 12 | # When true, bd will use .beads/issues.jsonl as the source of truth 13 | # instead of SQLite database 14 | # no-db: false 15 | 16 | # Disable daemon for RPC communication (forces direct database access) 17 | # no-daemon: false 18 | 19 | # Disable auto-flush of database to JSONL after mutations 20 | # no-auto-flush: false 21 | 22 | # Disable auto-import from JSONL when it's newer than database 23 | # no-auto-import: false 24 | 25 | # Enable JSON output by default 26 | # json: false 27 | 28 | # Default actor for audit trails (overridden by BD_ACTOR or --actor) 29 | # actor: "" 30 | 31 | # Path to database (overridden by BEADS_DB or --db) 32 | # db: "" 33 | 34 | # Auto-start daemon if not running (can also use BEADS_AUTO_START_DAEMON) 35 | # auto-start-daemon: true 36 | 37 | # Debounce interval for auto-flush (can also use BEADS_FLUSH_DEBOUNCE) 38 | # flush-debounce: "5s" 39 | 40 | # Multi-repo configuration (experimental - bd-307) 41 | # Allows hydrating from multiple repositories and routing writes to the correct JSONL 42 | # repos: 43 | # primary: "." # Primary repo (where this database lives) 44 | # additional: # Additional repos to hydrate from (read-only) 45 | # - ~/beads-planning # Personal planning repo 46 | # - ~/work-planning # Work planning repo 47 | 48 | # Integration settings (access with 'bd config get/set') 49 | # These are stored in the database, not in this file: 50 | # - jira.url 51 | # - jira.project 52 | # - linear.url 53 | # - linear.api-key 54 | # - github.org 55 | # - github.repo 56 | # - sync.branch - Git branch for beads commits (use BEADS_SYNC_BRANCH env var or bd config set) 57 | -------------------------------------------------------------------------------- /Diablo.love/systems/core/starter_gear.lua: -------------------------------------------------------------------------------- 1 | local ItemGenerator = require("items.generator") 2 | local EquipmentHelper = require("systems.helpers.equipment") 3 | 4 | local starterGearSystem = {} 5 | 6 | ---Generate and equip starter gear for a new game. 7 | ---Runs once per world scene initialization. 8 | function starterGearSystem.update(world, _dt) 9 | -- Check if starter gear has already been generated 10 | if world.starterGearGenerated then 11 | return 12 | end 13 | 14 | local player = world:getPlayer() 15 | if not player then 16 | return 17 | end 18 | 19 | if not player.skills then 20 | local createSkills = require("components.skills") 21 | player.skills = createSkills() 22 | end 23 | 24 | if not player.skills.equipped[1] then 25 | player.skills.equipped[1] = "fireball" 26 | end 27 | 28 | if not player.skills.equipped[2] then 29 | player.skills.equipped[2] = "thunder" 30 | end 31 | 32 | local starterWeapon = ItemGenerator.roll({ 33 | rarity = "common", 34 | allowedTypes = { "sword", "axe" }, 35 | source = "starter", 36 | foeTier = 1, 37 | }) 38 | 39 | local starterHelmet = ItemGenerator.roll({ 40 | rarity = "common", 41 | itemType = "helmet", 42 | source = "starter", 43 | foeTier = 1, 44 | }) 45 | 46 | local starterChest = ItemGenerator.roll({ 47 | rarity = "common", 48 | itemType = "chest", 49 | source = "starter", 50 | foeTier = 1, 51 | }) 52 | 53 | local starterGloves = ItemGenerator.roll({ 54 | rarity = "common", 55 | itemType = "gloves", 56 | source = "starter", 57 | foeTier = 1, 58 | }) 59 | 60 | local starterBoots = ItemGenerator.roll({ 61 | rarity = "common", 62 | itemType = "boots", 63 | source = "starter", 64 | foeTier = 1, 65 | }) 66 | 67 | EquipmentHelper.equip(player, starterWeapon) 68 | EquipmentHelper.equip(player, starterHelmet) 69 | EquipmentHelper.equip(player, starterChest) 70 | EquipmentHelper.equip(player, starterGloves) 71 | EquipmentHelper.equip(player, starterBoots) 72 | 73 | world.starterGearGenerated = true 74 | end 75 | 76 | return starterGearSystem 77 | -------------------------------------------------------------------------------- /Diablo.love/systems/core/apply_stats.lua: -------------------------------------------------------------------------------- 1 | local EquipmentHelper = require("systems.helpers.equipment") 2 | local ComponentDefaults = require("data.component_defaults") 3 | 4 | local applyStatsSystem = {} 5 | 6 | local function recalculateResource(component, newMaxValue, fallback) 7 | if not component then 8 | return 9 | end 10 | 11 | local oldMax = component.max or fallback 12 | local newMax = newMaxValue or fallback 13 | 14 | component.max = newMax 15 | 16 | local current = component.current or newMax 17 | 18 | if newMax > oldMax then 19 | local increase = newMax - oldMax 20 | current = math.min(current + increase, newMax) 21 | elseif newMax < oldMax then 22 | if oldMax > 0 then 23 | local ratio = current / oldMax 24 | current = math.min(current, newMax * ratio) 25 | else 26 | current = math.min(current, newMax) 27 | end 28 | else 29 | current = math.min(current, newMax) 30 | end 31 | 32 | component.current = current 33 | end 34 | 35 | ---Apply computed stats (base + equipment) to player entity components 36 | ---Updates movement speed, health max, etc. based on total stats 37 | function applyStatsSystem.update(world, _dt) 38 | local player = world:getPlayer() 39 | if not player then 40 | return 41 | end 42 | 43 | local totalStats = EquipmentHelper.computeTotalStats(player) 44 | 45 | player.stats = player.stats or {} 46 | player.stats.total = totalStats 47 | player.stats.base = player.baseStats 48 | 49 | -- Apply movement speed: base speed * (1 + moveSpeed percentage bonuses) 50 | if player.movement then 51 | local baseSpeed = ComponentDefaults.BASE_MOVEMENT_SPEED 52 | 53 | -- Apply moveSpeed bonuses from stats (both base and equipment) 54 | local speedMultiplier = 1 + (totalStats.moveSpeed or 0) 55 | player.movement.speed = baseSpeed * speedMultiplier 56 | end 57 | 58 | -- Apply health max: update max health based on stats 59 | -- Apply health and mana caps using shared helper 60 | recalculateResource(player.health, totalStats.health, ComponentDefaults.PLAYER_STARTING_HEALTH) 61 | recalculateResource(player.mana, totalStats.mana, ComponentDefaults.PLAYER_STARTING_MANA) 62 | end 63 | 64 | return applyStatsSystem 65 | -------------------------------------------------------------------------------- /Diablo.love/systems/combat/foe_attack.lua: -------------------------------------------------------------------------------- 1 | local vector = require("modules.vector") 2 | local coordinates = require("systems.helpers.coordinates") 3 | local combatTiming = require("systems.helpers.combat_timing") 4 | 5 | local foeAttackSystem = {} 6 | 7 | function foeAttackSystem.update(world, dt) 8 | -- Query entities with foe, chase, combat, position, and health components 9 | local entities = world:queryEntities({ "foe", "chase", "combat", "position" }) 10 | 11 | for _, foe in ipairs(entities) do 12 | -- Skip inactive entities 13 | if foe.inactive and foe.inactive.isInactive then 14 | goto continue 15 | end 16 | 17 | local combat = foe.combat 18 | if not combat then 19 | goto continue 20 | end 21 | 22 | -- Decrement cooldown and swing timer each frame 23 | combatTiming.updateTimers(combat, dt, { clearAttackAnimationTime = true }) 24 | 25 | -- Skip if already has a queued attack 26 | if combat.queuedAttack then 27 | goto continue 28 | end 29 | 30 | -- Skip if cooldown is not ready 31 | if combat.cooldown > 0 then 32 | goto continue 33 | end 34 | 35 | -- Get player entity 36 | local player = world:getPlayer() 37 | if not player or not player.health or player.health.current <= 0 or player.dead then 38 | goto continue 39 | end 40 | 41 | -- Calculate distance between foe center and player center 42 | local foeX, foeY = coordinates.getEntityCenter(foe) 43 | local playerX, playerY = coordinates.getEntityCenter(player) 44 | if not foeX or not playerX then 45 | goto continue 46 | end 47 | 48 | local distance = vector.distance(foeX, foeY, playerX, playerY) 49 | local attackRange = combatTiming.getRange(combat) 50 | 51 | -- Check if foe is within attack range 52 | if distance <= attackRange then 53 | -- Set queued attack 54 | combat.queuedAttack = { 55 | targetId = player.id, 56 | range = attackRange, 57 | time = world.time or 0, 58 | } 59 | 60 | combatTiming.beginSwing(combat, { timeStamp = world.time or 0 }) 61 | end 62 | 63 | ::continue:: 64 | end 65 | end 66 | 67 | return foeAttackSystem 68 | -------------------------------------------------------------------------------- /Diablo.love/entities/projectile.lua: -------------------------------------------------------------------------------- 1 | local Projectile = {} 2 | Projectile.__index = Projectile 3 | 4 | ---Create a projectile entity representing a spell projectile. 5 | ---@param opts table|nil 6 | ---@return table 7 | function Projectile.new(opts) 8 | opts = opts or {} 9 | 10 | local createPosition = require("components.position") 11 | local createSize = require("components.size") 12 | local createMovement = require("components.movement") 13 | local createRenderable = require("components.renderable") 14 | local createProjectile = require("components.projectile") 15 | local createInactive = require("components.inactive") 16 | 17 | local size = opts.size or 12 18 | 19 | local primaryColor = opts.color or { 1.0, 0.4, 0.1, 1 } 20 | local secondaryColor = opts.secondaryColor or { 1.0, 0.6, 0.2, 0.9 } 21 | local coreColor = opts.coreColor or { 1.0, 0.9, 0.7, 1 } 22 | 23 | local entity = { 24 | id = opts.id or ("projectile_" .. math.random(10000, 99999)), 25 | position = createPosition({ 26 | x = opts.x or 0, 27 | y = opts.y or 0, 28 | }), 29 | size = createSize({ 30 | w = size, 31 | h = size, 32 | }), 33 | movement = createMovement({ 34 | speed = opts.speed or 300, 35 | vx = opts.vx or 0, 36 | vy = opts.vy or 0, 37 | }), 38 | renderable = createRenderable({ 39 | kind = opts.renderKind or "circle", 40 | color = primaryColor, 41 | secondaryColor = secondaryColor, 42 | coreColor = coreColor, 43 | sparkleSeed = opts.sparkleSeed or math.random(), 44 | }), 45 | projectile = createProjectile({ 46 | spellId = opts.spellId, 47 | targetId = opts.targetId, 48 | targetX = opts.targetX, 49 | targetY = opts.targetY, 50 | damage = opts.damage, 51 | ownerId = opts.ownerId, 52 | lifetime = opts.lifetime, 53 | maxLifetime = opts.lifetime, 54 | speed = opts.speed or 300, 55 | impactDuration = opts.impactDuration, 56 | directionX = opts.vx or 0, 57 | directionY = opts.vy or 0, 58 | }), 59 | inactive = createInactive(), 60 | } 61 | 62 | return setmetatable(entity, Projectile) 63 | end 64 | 65 | return Projectile 66 | -------------------------------------------------------------------------------- /Diablo.love/systems/ui/experience_bar.lua: -------------------------------------------------------------------------------- 1 | local Leveling = require("modules.leveling") 2 | local UIConfig = require("systems.ui.config") 3 | 4 | local uiExperienceBar = {} 5 | 6 | function uiExperienceBar.draw(world) 7 | local player = world:getPlayer() 8 | if not player or not player.experience then 9 | return 10 | end 11 | 12 | local exp = player.experience 13 | local level = exp.level or 1 14 | local currentXP = exp.currentXP or 0 15 | 16 | local totalXPForCurrentLevel = Leveling.getXPForLevel(level) 17 | local totalXPForNextLevel = Leveling.getXPForLevel(level + 1) 18 | local xpProgress = currentXP - totalXPForCurrentLevel 19 | local xpRequired = totalXPForNextLevel - totalXPForCurrentLevel 20 | 21 | xpProgress = math.max(0, xpProgress) 22 | xpRequired = math.max(0, xpRequired) 23 | 24 | local ratio = xpRequired > 0 and math.min(1, xpProgress / xpRequired) or 0 25 | if ratio < 0.001 then 26 | ratio = 0 27 | end 28 | 29 | local screenWidth = love.graphics.getWidth() 30 | local screenHeight = love.graphics.getHeight() 31 | local pos = UIConfig.getExperienceBarPosition(screenWidth, screenHeight) 32 | local barX = pos.barX 33 | local barY = pos.barY 34 | local barWidth = pos.barWidth 35 | local barHeight = pos.barHeight 36 | 37 | love.graphics.push("all") 38 | 39 | love.graphics.setColor(0.1, 0.1, 0.1, 0.9) 40 | love.graphics.rectangle("fill", barX, barY, barWidth, barHeight, 4, 4) 41 | 42 | if ratio > 0 then 43 | local fillWidth = math.min(barWidth * ratio, barWidth) 44 | love.graphics.setScissor(barX, barY, barWidth, barHeight) 45 | love.graphics.setColor(0.2, 0.6, 1.0, 1) 46 | love.graphics.rectangle("fill", barX, barY, fillWidth, barHeight, 4, 4) 47 | love.graphics.setScissor() 48 | end 49 | 50 | love.graphics.setColor(0.9, 0.85, 0.65, 1) 51 | love.graphics.setLineWidth(2) 52 | love.graphics.rectangle("line", barX, barY, barWidth, barHeight, 4, 4) 53 | 54 | love.graphics.setColor(1, 1, 1, 1) 55 | local displayProgress = math.floor(xpProgress + 0.5) 56 | local displayRequired = xpRequired > 0 and math.floor(xpRequired + 0.5) or 0 57 | local text = string.format("Level %d | %d / %d XP", level, displayProgress, displayRequired) 58 | love.graphics.printf(text, barX, barY + (barHeight / 2) - 6, barWidth, "center") 59 | 60 | love.graphics.pop() 61 | end 62 | 63 | return uiExperienceBar 64 | -------------------------------------------------------------------------------- /Diablo.love/systems/render/window/scrollbar.lua: -------------------------------------------------------------------------------- 1 | ---Scrollbar renderer for scrollable content areas. 2 | ---Draws a visual scrollbar indicator on the right edge of content areas. 3 | local ScrollableContent = require("systems.helpers.scrollable_content") 4 | 5 | local renderScrollbar = {} 6 | 7 | local SCROLLBAR_WIDTH = 8 8 | local SCROLLBAR_MARGIN = 12 9 | local SCROLLBAR_BG_COLOR = { 0.2, 0.2, 0.2, 0.5 } 10 | local SCROLLBAR_THUMB_COLOR = { 0.6, 0.55, 0.45, 0.9 } 11 | 12 | ---Draw scrollbar for a scrollable content area. 13 | ---@param scene table Scene with scrollState and windowLayout 14 | function renderScrollbar.draw(scene) 15 | local scrollState = scene.scrollState 16 | local layout = scene.windowLayout 17 | 18 | if not scrollState or not layout or not layout.content then 19 | return 20 | end 21 | 22 | -- Only show scrollbar if scrolling is needed 23 | if not ScrollableContent.needsScrolling(scrollState) then 24 | return 25 | end 26 | 27 | local content = layout.content 28 | local contentX = content.x 29 | local contentY = content.y 30 | local contentWidth = content.width 31 | local contentHeight = content.height 32 | 33 | -- Calculate scrollbar position (right edge of content area) 34 | local scrollbarX = contentX + contentWidth - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN 35 | local scrollbarY = contentY 36 | local scrollbarHeight = contentHeight 37 | 38 | -- Calculate thumb size (proportional to visible area) 39 | local thumbHeight = scrollbarHeight * (scrollState.viewportHeight / scrollState.contentHeight) 40 | thumbHeight = math.max(12, thumbHeight) -- Minimum thumb height for visibility 41 | 42 | -- Calculate thumb position based on scroll offset 43 | local scrollRatio = 0 44 | if scrollState.maxScrollY > 0 then 45 | scrollRatio = scrollState.scrollY / scrollState.maxScrollY 46 | end 47 | local availableHeight = scrollbarHeight - thumbHeight 48 | local thumbY = scrollbarY + scrollRatio * availableHeight 49 | 50 | -- Draw scrollbar background 51 | love.graphics.setColor(SCROLLBAR_BG_COLOR) 52 | love.graphics.rectangle("fill", scrollbarX, scrollbarY, SCROLLBAR_WIDTH, scrollbarHeight, 2, 2) 53 | 54 | -- Draw scrollbar thumb 55 | love.graphics.setColor(SCROLLBAR_THUMB_COLOR) 56 | love.graphics.rectangle("fill", scrollbarX, thumbY, SCROLLBAR_WIDTH, thumbHeight, 2, 2) 57 | 58 | -- Reset color 59 | love.graphics.setColor(1, 1, 1, 1) 60 | end 61 | 62 | return renderScrollbar 63 | -------------------------------------------------------------------------------- /spec/systems/ui/target_spec.lua: -------------------------------------------------------------------------------- 1 | require("spec.spec_helper") 2 | 3 | -- luacheck: globals love rawset rawget _G 4 | 5 | local function buildLoveStub() 6 | return { 7 | graphics = { 8 | push = function() end, 9 | pop = function() end, 10 | setColor = function() end, 11 | rectangle = function() end, 12 | setLineWidth = function() end, 13 | print = function() end, 14 | printf = function() end, 15 | getWidth = function() 16 | return 800 17 | end, 18 | getFont = function() 19 | return { 20 | getHeight = function() 21 | return 14 22 | end, 23 | } 24 | end, 25 | }, 26 | } 27 | end 28 | 29 | rawset(_G, "love", buildLoveStub()) 30 | 31 | local uiTargetSystem = require("systems.ui.target") 32 | local TestWorld = require("spec.support.test_world") 33 | 34 | describe("systems.ui.target", function() 35 | local world 36 | local originalLove 37 | 38 | before_each(function() 39 | originalLove = rawget(_G, "love") 40 | rawset(_G, "love", buildLoveStub()) 41 | 42 | world = TestWorld.new() 43 | world.camera = { x = 0, y = 0 } 44 | function world:getPlayer() -- luacheck: ignore 212/self 45 | return self.player 46 | end 47 | local player = { 48 | id = "player", 49 | targeting = {}, 50 | } 51 | world.player = player 52 | world:addEntity(player) 53 | end) 54 | 55 | local function addTarget(rarityId) 56 | local entity = { 57 | id = "foe_1", 58 | foe = { rarity = rarityId }, 59 | health = { current = 50, max = 100 }, 60 | position = { x = 0, y = 0 }, 61 | size = { w = 20, h = 20 }, 62 | name = "Test Foe", 63 | } 64 | world:addEntity(entity) 65 | local player = world:getPlayer() 66 | player.targeting = { currentTargetId = entity.id, keepAlive = 1.5 } 67 | return entity 68 | end 69 | 70 | after_each(function() 71 | rawset(_G, "love", originalLove) 72 | end) 73 | 74 | it("draws boss frame styling without error", function() 75 | addTarget("boss") 76 | uiTargetSystem.draw(world) 77 | end) 78 | 79 | it("draws elite frame styling without error", function() 80 | addTarget("elite") 81 | uiTargetSystem.draw(world) 82 | end) 83 | end) 84 | -------------------------------------------------------------------------------- /Diablo.love/systems/ai/detection.lua: -------------------------------------------------------------------------------- 1 | local vector = require("modules.vector") 2 | local createChase = require("components.chase") 3 | 4 | local detectionSystem = {} 5 | 6 | local function ensureChase(world, foe, targetId) 7 | if foe.chase then 8 | foe.chase.targetId = targetId 9 | return 10 | end 11 | 12 | world:addComponent(foe.id, "chase", createChase({ targetId = targetId })) 13 | end 14 | 15 | function detectionSystem.update(world, _dt) 16 | local player = world:getPlayer() 17 | if not player or not player.position then 18 | return 19 | end 20 | 21 | local foes = world:queryEntities({ "detection", "position" }) 22 | 23 | for _, foe in ipairs(foes) do 24 | -- Skip inactive entities (too far from player) 25 | if foe.inactive and foe.inactive.isInactive then 26 | goto continue 27 | end 28 | 29 | local detection = foe.detection 30 | local foePos = foe.position 31 | local playerPos = player.position 32 | 33 | local distSquared = vector.distanceSquared(foePos.x, foePos.y, playerPos.x, playerPos.y) 34 | local range = detection.range or 0 35 | local rangeSquared = range * range 36 | local detected = false 37 | local hasForcedAggro = detection.forceAggro and detection.detectedTargetId == player.id 38 | 39 | if hasForcedAggro then 40 | local leashRange = detection.leashRange 41 | if not leashRange then 42 | local extension = detection.leashExtension or 0 43 | leashRange = math.max(range, range + extension) 44 | detection.leashRange = leashRange 45 | end 46 | 47 | local leashSquared = (detection.leashRange or range) * (detection.leashRange or range) 48 | if distSquared <= leashSquared then 49 | detected = true 50 | else 51 | detection.forceAggro = false 52 | detection.leashRange = nil 53 | end 54 | elseif distSquared <= rangeSquared then 55 | detected = true 56 | detection.leashRange = nil 57 | end 58 | 59 | if detected then 60 | detection.detectedTargetId = player.id 61 | ensureChase(world, foe, player.id) 62 | else 63 | detection.detectedTargetId = nil 64 | if foe.chase then 65 | world:removeComponent(foe.id, "chase") 66 | end 67 | end 68 | 69 | ::continue:: 70 | end 71 | end 72 | 73 | return detectionSystem 74 | -------------------------------------------------------------------------------- /.beads/README.md: -------------------------------------------------------------------------------- 1 | # Beads - AI-Native Issue Tracking 2 | 3 | Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code. 4 | 5 | ## What is Beads? 6 | 7 | Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git. 8 | 9 | **Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads) 10 | 11 | ## Quick Start 12 | 13 | ### Essential Commands 14 | 15 | ```bash 16 | # Create new issues 17 | bd create "Add user authentication" 18 | 19 | # View all issues 20 | bd list 21 | 22 | # View issue details 23 | bd show 24 | 25 | # Update issue status 26 | bd update --status in-progress 27 | bd update --status done 28 | 29 | # Sync with git remote 30 | bd sync 31 | ``` 32 | 33 | ### Working with Issues 34 | 35 | Issues in Beads are: 36 | - **Git-native**: Stored in `.beads/issues.jsonl` and synced like code 37 | - **AI-friendly**: CLI-first design works perfectly with AI coding agents 38 | - **Branch-aware**: Issues can follow your branch workflow 39 | - **Always in sync**: Auto-syncs with your commits 40 | 41 | ## Why Beads? 42 | 43 | ✨ **AI-Native Design** 44 | - Built specifically for AI-assisted development workflows 45 | - CLI-first interface works seamlessly with AI coding agents 46 | - No context switching to web UIs 47 | 48 | 🚀 **Developer Focused** 49 | - Issues live in your repo, right next to your code 50 | - Works offline, syncs when you push 51 | - Fast, lightweight, and stays out of your way 52 | 53 | 🔧 **Git Integration** 54 | - Automatic sync with git commits 55 | - Branch-aware issue tracking 56 | - Intelligent JSONL merge resolution 57 | 58 | ## Get Started with Beads 59 | 60 | Try Beads in your own projects: 61 | 62 | ```bash 63 | # Install Beads 64 | curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash 65 | 66 | # Initialize in your repo 67 | bd init 68 | 69 | # Create your first issue 70 | bd create "Try out Beads" 71 | ``` 72 | 73 | ## Learn More 74 | 75 | - **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs) 76 | - **Quick Start Guide**: Run `bd quickstart` 77 | - **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples) 78 | 79 | --- 80 | 81 | *Beads: Issue tracking that moves at the speed of thought* ⚡ 82 | -------------------------------------------------------------------------------- /Diablo.love/systems/render/loot.lua: -------------------------------------------------------------------------------- 1 | local Resources = require("modules.resources") 2 | 3 | local renderLootSystem = {} 4 | 5 | function renderLootSystem.draw(world) 6 | local camera = world.camera or { x = 0, y = 0 } 7 | 8 | love.graphics.push("all") 9 | love.graphics.translate(-camera.x, -camera.y) 10 | 11 | local entities = world:queryEntities({ "renderable", "lootable", "position", "size" }) 12 | 13 | for _, entity in ipairs(entities) do 14 | if entity.inactive and entity.inactive.isInactive then 15 | goto continue 16 | end 17 | 18 | local renderable = entity.renderable 19 | if not renderable or renderable.kind ~= "loot" then 20 | goto continue 21 | end 22 | 23 | local lootable = entity.lootable 24 | if not lootable then 25 | goto continue 26 | end 27 | 28 | local hasItem = lootable.item ~= nil 29 | local hasGold = lootable.gold and lootable.gold > 0 30 | if not hasItem and not hasGold then 31 | goto continue 32 | end 33 | 34 | local pos = entity.position 35 | local size = entity.size 36 | local x = pos.x 37 | local y = pos.y 38 | local w = size.w 39 | local h = size.h 40 | 41 | local color = renderable.color or { 1, 1, 1, 1 } 42 | 43 | love.graphics.push("all") 44 | 45 | love.graphics.setColor(0, 0, 0, 0.7) 46 | love.graphics.rectangle("fill", x, y, w, h, 4, 4) 47 | 48 | love.graphics.setColor(color) 49 | love.graphics.setLineWidth(3) 50 | love.graphics.rectangle("line", x, y, w, h, 4, 4) 51 | 52 | local item = lootable.item 53 | local spritePath = lootable.iconPath or (item and item.spritePath) 54 | 55 | if spritePath then 56 | local sprite = Resources.loadImageSafe(spritePath) 57 | if sprite then 58 | local spriteWidth = sprite:getWidth() 59 | local spriteHeight = sprite:getHeight() 60 | 61 | local innerPadding = 8 62 | local scale = math.min((w - innerPadding) / spriteWidth, (h - innerPadding) / spriteHeight) 63 | local drawX = x + (w - spriteWidth * scale) / 2 64 | local drawY = y + (h - spriteHeight * scale) / 2 65 | 66 | love.graphics.setColor(1, 1, 1, 1) 67 | love.graphics.draw(sprite, drawX, drawY, 0, scale, scale) 68 | end 69 | end 70 | 71 | love.graphics.pop() 72 | 73 | ::continue:: 74 | end 75 | 76 | love.graphics.pop() 77 | end 78 | 79 | return renderLootSystem 80 | -------------------------------------------------------------------------------- /Diablo.love/systems/helpers/combat_timing.lua: -------------------------------------------------------------------------------- 1 | local ComponentDefaults = require("data.component_defaults") 2 | 3 | local combatTiming = {} 4 | 5 | ---Update combat cooldown and swing timer for an entity. 6 | ---@param combat table|nil 7 | ---@param dt number 8 | ---@param opts table|nil 9 | function combatTiming.updateTimers(combat, dt, opts) 10 | if not combat then 11 | return 12 | end 13 | 14 | opts = opts or {} 15 | 16 | combat.cooldown = math.max((combat.cooldown or 0) - dt, 0) 17 | 18 | if combat.swingTimer and combat.swingTimer > 0 then 19 | combat.swingTimer = math.max(combat.swingTimer - dt, 0) 20 | if combat.swingTimer <= 0 and opts.clearAttackAnimationTime then 21 | combat.attackAnimationTime = nil 22 | end 23 | end 24 | end 25 | 26 | ---Compute the effective attack speed for an entity, including stat modifiers. 27 | ---@param entity table 28 | ---@return number 29 | function combatTiming.computeEffectiveAttackSpeed(entity) 30 | if not entity or not entity.combat then 31 | return ComponentDefaults.BASE_ATTACK_SPEED 32 | end 33 | 34 | local combat = entity.combat 35 | local baseSpeed = combat.attackSpeed or ComponentDefaults.BASE_ATTACK_SPEED 36 | local multiplier = 1 + ((entity.stats and entity.stats.total and entity.stats.total.attackSpeed) or 0) 37 | multiplier = math.max(multiplier, ComponentDefaults.MIN_ATTACK_SPEED_MULTIPLIER) 38 | 39 | local effective = baseSpeed * multiplier 40 | return math.max(effective, ComponentDefaults.MIN_ATTACK_SPEED) 41 | end 42 | 43 | ---Start a new swing, applying cooldown and swing timer. 44 | ---@param combat table|nil 45 | ---@param opts table|nil 46 | function combatTiming.beginSwing(combat, opts) 47 | if not combat then 48 | return 49 | end 50 | 51 | opts = opts or {} 52 | 53 | local attackSpeed = opts.attackSpeed or combat.attackSpeed or ComponentDefaults.BASE_ATTACK_SPEED 54 | attackSpeed = math.max(attackSpeed, ComponentDefaults.MIN_ATTACK_SPEED) 55 | 56 | combat.cooldown = 1 / attackSpeed 57 | combat.swingTimer = opts.swingDuration or combat.swingDuration or ComponentDefaults.COMBAT_SWING_DURATION 58 | 59 | if opts.timeStamp then 60 | combat.lastAttackTime = opts.timeStamp 61 | end 62 | end 63 | 64 | ---Get the combat range with a fallback to defaults. 65 | ---@param combat table|nil 66 | ---@return number 67 | function combatTiming.getRange(combat) 68 | if not combat then 69 | return ComponentDefaults.DEFAULT_COMBAT_RANGE 70 | end 71 | 72 | return combat.range or ComponentDefaults.DEFAULT_COMBAT_RANGE 73 | end 74 | 75 | return combatTiming 76 | -------------------------------------------------------------------------------- /Diablo.love/systems/helpers/sprite_renderer.lua: -------------------------------------------------------------------------------- 1 | local Resources = require("modules.resources") 2 | 3 | local spriteRenderer = {} 4 | 5 | local spriteSheetCache = {} 6 | 7 | local function createSpriteGrid(image, gridCols, gridRows) 8 | local imageW, imageH = image:getDimensions() 9 | local cellW = math.floor(imageW / gridCols) 10 | local cellH = math.floor(imageH / gridRows) 11 | 12 | local spriteGrid = {} 13 | for row = 0, gridRows - 1 do 14 | spriteGrid[row] = {} 15 | for col = 0, gridCols - 1 do 16 | spriteGrid[row][col] = love.graphics.newQuad( 17 | col * cellW, 18 | row * cellH, 19 | cellW, 20 | cellH, 21 | imageW, 22 | imageH 23 | ) 24 | end 25 | end 26 | 27 | return spriteGrid 28 | end 29 | 30 | function spriteRenderer.getSpriteQuad(spriteSheetPath, row, col, gridCols) 31 | gridCols = gridCols or 8 32 | 33 | if not spriteSheetCache[spriteSheetPath] then 34 | local image = Resources.loadImage(spriteSheetPath) 35 | if not image then 36 | return nil, nil 37 | end 38 | 39 | local gridRows = 4 40 | local spriteGrid = createSpriteGrid(image, gridCols, gridRows) 41 | 42 | spriteSheetCache[spriteSheetPath] = { 43 | image = image, 44 | grid = spriteGrid, 45 | gridCols = gridCols, 46 | } 47 | end 48 | 49 | local cached = spriteSheetCache[spriteSheetPath] 50 | if not cached or not cached.grid[row] or not cached.grid[row][col] then 51 | return nil, nil 52 | end 53 | 54 | return cached.image, cached.grid[row][col] 55 | end 56 | 57 | function spriteRenderer.getAnimationFrame(animationState, walkTime, attackTime, swingDuration, totalFrames) 58 | walkTime = walkTime or 0 59 | attackTime = attackTime or 0 60 | swingDuration = swingDuration or 0.3 61 | totalFrames = totalFrames or 8 62 | 63 | if animationState == "attacking" then 64 | local attackProgress = math.min(attackTime / swingDuration, 1.0) 65 | local frameIndex = math.floor(attackProgress * totalFrames) 66 | return math.min(frameIndex, totalFrames - 1) 67 | elseif animationState == "walking" then 68 | local frameIndex = math.floor((walkTime * 8) % totalFrames) 69 | return frameIndex 70 | else 71 | local frameIndex = math.floor((walkTime * 2) % totalFrames) 72 | return frameIndex 73 | end 74 | end 75 | 76 | function spriteRenderer.clearCache(spriteSheetPath) 77 | if spriteSheetPath then 78 | spriteSheetCache[spriteSheetPath] = nil 79 | else 80 | spriteSheetCache = {} 81 | end 82 | end 83 | 84 | return spriteRenderer 85 | -------------------------------------------------------------------------------- /Diablo.love/entities/structures/factory.lua: -------------------------------------------------------------------------------- 1 | local createPosition = require("components.position") 2 | local createSize = require("components.size") 3 | local createRenderable = require("components.renderable") 4 | local createStructure = require("components.structure") 5 | local createPhysicsBody = require("components.physics_body") 6 | local createInactive = require("components.inactive") 7 | 8 | local StructureFactory = {} 9 | StructureFactory.__index = StructureFactory 10 | 11 | local STRUCTURE_TEMPLATES = { 12 | tree_cluster = { 13 | size = { w = 48, h = 64 }, 14 | renderable = { kind = "structure", shape = "tree_cluster", color = { 0.1, 0.4, 0.18, 1 } }, 15 | }, 16 | forest_hut = { 17 | size = { w = 70, h = 50 }, 18 | renderable = { kind = "structure", shape = "forest_hut", color = { 0.45, 0.27, 0.11, 1 } }, 19 | }, 20 | desert_rock = { 21 | size = { w = 60, h = 40 }, 22 | renderable = { kind = "structure", shape = "desert_rock", color = { 0.65, 0.54, 0.3, 1 } }, 23 | }, 24 | ruined_obelisk = { 25 | size = { w = 40, h = 90 }, 26 | renderable = { kind = "structure", shape = "ruined_obelisk", color = { 0.55, 0.5, 0.42, 1 } }, 27 | }, 28 | ice_spike = { 29 | size = { w = 36, h = 80 }, 30 | renderable = { kind = "structure", shape = "ice_spike", color = { 0.72, 0.85, 0.95, 1 } }, 31 | }, 32 | frozen_ruin = { 33 | size = { w = 80, h = 60 }, 34 | renderable = { kind = "structure", shape = "frozen_ruin", color = { 0.68, 0.77, 0.88, 1 } }, 35 | }, 36 | } 37 | 38 | local function resolveTemplate(structureId) 39 | return STRUCTURE_TEMPLATES[structureId] or STRUCTURE_TEMPLATES.tree_cluster 40 | end 41 | 42 | function StructureFactory.build(opts) 43 | opts = opts or {} 44 | local template = resolveTemplate(opts.structureId) 45 | 46 | local entity = { 47 | id = opts.id or (opts.structureId .. "_" .. tostring(math.floor(math.random() * 100000))), 48 | position = createPosition({ x = opts.x or 0, y = opts.y or 0 }), 49 | size = createSize({ w = template.size.w, h = template.size.h }), 50 | renderable = createRenderable({ kind = template.renderable.kind, color = template.renderable.color }), 51 | structure = createStructure({ id = opts.id, structureId = opts.structureId, lootable = template.lootable }), 52 | physicsBody = createPhysicsBody({ 53 | bodyType = "static", 54 | fixedRotation = true, 55 | friction = 0, 56 | }), 57 | inactive = createInactive(), 58 | } 59 | 60 | entity.renderable.shape = template.renderable.shape 61 | entity.renderable.rotation = opts.rotation or 0 62 | 63 | return entity 64 | end 65 | 66 | return StructureFactory 67 | -------------------------------------------------------------------------------- /Diablo.love/systems/render/mouse_look.lua: -------------------------------------------------------------------------------- 1 | local renderMouseLookSystem = {} 2 | 3 | ---Draw a simple arrow shape 4 | ---@param x number Center X position 5 | ---@param y number Center Y position 6 | ---@param angle number Rotation angle in radians 7 | ---@param size number Arrow size 8 | local function drawArrow(x, y, angle, size) 9 | love.graphics.push("all") 10 | love.graphics.translate(x, y) 11 | love.graphics.rotate(angle) 12 | 13 | -- Draw arrow as a triangle pointing right (will be rotated) 14 | local arrowSize = size or 20 15 | local halfSize = arrowSize / 2 16 | 17 | -- Triangle points: tip pointing right, base on left 18 | love.graphics.polygon("fill", 19 | 0, 0, -- Tip (right) 20 | -halfSize, -halfSize * 0.6, -- Top base (left) 21 | -halfSize * 0.5, 0, -- Middle base (left) 22 | -halfSize, halfSize * 0.6 -- Bottom base (left) 23 | ) 24 | 25 | love.graphics.pop() 26 | end 27 | 28 | function renderMouseLookSystem.draw(scene) 29 | local player = scene:getPlayer() 30 | if not player or not player.position or not player.movement or not player.movement.lookDirection then 31 | return 32 | end 33 | 34 | local camera = scene.camera or { x = 0, y = 0 } 35 | local coordinates = scene.systemHelpers and scene.systemHelpers.coordinates 36 | if not coordinates then 37 | return 38 | end 39 | 40 | love.graphics.push("all") 41 | love.graphics.translate(-camera.x, -camera.y) 42 | 43 | local size = player.size or { w = 32, h = 32 } 44 | local centerX, centerY = coordinates.getEntityCenter(player) 45 | if not centerX or not centerY then 46 | love.graphics.pop() 47 | return 48 | end 49 | 50 | -- Position arrow on a fixed-radius circle around the player center 51 | -- The arrow moves along this invisible circle based on look direction 52 | local lookDir = player.movement.lookDirection 53 | local playerRadius = math.max(size.w, size.h) / 2 -- Half the player size 54 | local circleRadius = playerRadius + 20 -- Fixed circle radius (ensures arrow doesn't overlap player) 55 | local arrowX = centerX + lookDir.x * circleRadius 56 | local arrowY = centerY + lookDir.y * circleRadius 57 | 58 | -- Calculate rotation angle from look direction 59 | local angle = math.atan2(lookDir.y, lookDir.x) 60 | 61 | -- Draw arrow with distinct color and opacity 62 | love.graphics.setColor(1, 1, 0.7, 0.7) -- Light yellow with moderate opacity 63 | local arrowSize = math.max(size.w, size.h) * 1.2 -- Slightly larger than player sprite 64 | drawArrow(arrowX, arrowY, angle, arrowSize) 65 | 66 | love.graphics.setColor(1, 1, 1, 1) 67 | love.graphics.pop() 68 | end 69 | 70 | return renderMouseLookSystem 71 | -------------------------------------------------------------------------------- /Diablo.love/shaders/power_glow.glsl: -------------------------------------------------------------------------------- 1 | extern number time; 2 | extern number intensity; 3 | extern vec3 glowColor; 4 | extern number pulseSpeed; 5 | extern number glowRadius; 6 | extern number distortionStrength; 7 | 8 | vec4 effect(vec4 color, Image texture, vec2 textureCoords, vec2 screenCoords) { 9 | vec4 texColor = Texel(texture, textureCoords); 10 | 11 | // Skip transparent pixels 12 | if (texColor.a < 0.01) { 13 | return texColor * color; 14 | } 15 | 16 | // Smooth progressive pulse - gradual up and down with easing (not blinking) 17 | // abs(sin) creates natural smooth up/down from 0 to 1 18 | float rawPulse = abs(sin(time * pulseSpeed)); 19 | // Apply power curve for progressive easing - smoother transitions 20 | float pulse = pow(rawPulse, 0.7); // Easing curve for smooth progressive feel 21 | 22 | // More intense pulse range - from 0.4 to 1.2 for stronger effect 23 | float pulseIntensity = 0.4 + pulse * 0.8; 24 | 25 | // Calculate distance from center for radial glow 26 | vec2 center = vec2(0.5, 0.5); 27 | vec2 uv = textureCoords; 28 | float dist = distance(uv, center); 29 | 30 | // Create radial glow mask - extend further out for more visible glow 31 | float glowMask = 1.0 - smoothstep(0.0, glowRadius, dist); 32 | glowMask = pow(glowMask, 0.8); // Softer falloff for wider glow 33 | 34 | // Much stronger animated glow 35 | float animatedGlow = glowMask * pulseIntensity * intensity; 36 | 37 | // More noticeable radial distortion for power effect 38 | vec2 distortion = (uv - center) * distortionStrength * pulse * 0.03; 39 | vec4 distortedColor = Texel(texture, uv + distortion); 40 | 41 | // More visible warping 42 | vec4 baseColor = mix(texColor, distortedColor, pulse * 0.25); 43 | 44 | // Glow color overlay - use screen blend to preserve color saturation 45 | vec3 glowOverlay = glowColor * animatedGlow * (1.0 + pulse * 0.4); 46 | // Screen blend: 1 - (1 - a) * (1 - b) preserves colors better than additive 47 | vec3 screenBlend = 1.0 - (1.0 - baseColor.rgb) * (1.0 - glowOverlay); 48 | // Mix between original and screen blend to control intensity 49 | vec3 finalColor = mix(baseColor.rgb, screenBlend, 0.7); 50 | 51 | // Reduced brightness boost to avoid white washout 52 | finalColor = finalColor * (1.0 + pulse * 0.1); 53 | 54 | // Add colored edge glow with screen blend for color preservation 55 | float edgeGlow = smoothstep(glowRadius * 0.7, glowRadius, dist); 56 | vec3 edgeGlowColor = glowColor * edgeGlow * pulse * intensity * 0.3; 57 | vec3 edgeScreenBlend = 1.0 - (1.0 - finalColor) * (1.0 - edgeGlowColor); 58 | finalColor = mix(finalColor, edgeScreenBlend, 0.5); 59 | 60 | // Preserve alpha 61 | return vec4(finalColor, texColor.a) * color; 62 | } 63 | -------------------------------------------------------------------------------- /Diablo.love/systems/helpers/aggro.lua: -------------------------------------------------------------------------------- 1 | local vector = require("modules.vector") 2 | local coordinates = require("systems.helpers.coordinates") 3 | local createChase = require("components.chase") 4 | 5 | local Aggro = {} 6 | 7 | local function addChaseComponent(world, foe, targetId) 8 | if foe.chase then 9 | foe.chase.targetId = targetId 10 | return 11 | end 12 | 13 | world:addComponent(foe.id, "chase", createChase({ targetId = targetId })) 14 | end 15 | 16 | local function updateDetectionForAggro(foe, target) 17 | local detection = foe.detection 18 | if not detection then 19 | return 20 | end 21 | 22 | detection.detectedTargetId = target.id 23 | detection.forceAggro = true 24 | 25 | local baseRange = detection.range or 0 26 | local extension = detection.leashExtension or 0 27 | local leashRange = math.max(baseRange, baseRange + extension) 28 | 29 | local foeCenterX, foeCenterY = coordinates.getEntityCenter(foe) 30 | local targetCenterX, targetCenterY = coordinates.getEntityCenter(target) 31 | 32 | if foeCenterX and targetCenterX then 33 | local distance = vector.distance(foeCenterX, foeCenterY, targetCenterX, targetCenterY) 34 | leashRange = math.max(leashRange, distance + extension) 35 | end 36 | 37 | detection.leashRange = leashRange 38 | end 39 | 40 | ---Force a foe to aggro onto a specific target (typically the player). 41 | ---@param world table 42 | ---@param foe table 43 | ---@param targetId string 44 | ---@param opts table|nil 45 | function Aggro.ensureAggro(world, foe, targetId, opts) 46 | opts = opts or {} 47 | 48 | if not world or not foe or foe.dead then 49 | return 50 | end 51 | 52 | local target = opts.target or world:getEntity(targetId) 53 | if not target or not target.playerControlled then 54 | return 55 | end 56 | 57 | if foe.inactive then 58 | foe.inactive.isInactive = false 59 | end 60 | addChaseComponent(world, foe, target.id) 61 | updateDetectionForAggro(foe, target) 62 | 63 | if opts.propagatePack == false then 64 | return 65 | end 66 | 67 | local foeInfo = foe.foe 68 | if not foeInfo or not foeInfo.packAggro or not foeInfo.packId then 69 | return 70 | end 71 | 72 | local packMembers = world:queryEntities({ "foe" }) 73 | for _, other in ipairs(packMembers) do 74 | if other.id ~= foe.id and not other.dead then 75 | local otherInfo = other.foe 76 | if otherInfo and otherInfo.packId == foeInfo.packId then 77 | Aggro.ensureAggro(world, other, target.id, { 78 | target = target, 79 | propagatePack = false, 80 | }) 81 | end 82 | end 83 | end 84 | end 85 | 86 | return Aggro 87 | -------------------------------------------------------------------------------- /Diablo.love/systems/ui/config.lua: -------------------------------------------------------------------------------- 1 | local UIConfig = {} 2 | 3 | -- Shared UI positioning constants 4 | UIConfig.barX = 32 -- Left margin for all bars 5 | UIConfig.barHeight = 20 -- Height of all bars 6 | UIConfig.buttonSize = 48 -- Size of potion/bag buttons 7 | UIConfig.bottomOffset = 44 -- 32 + 12, spacing from bottom for bottom bar elements 8 | UIConfig.buttonSpacing = 8 -- Spacing between buttons 9 | UIConfig.sideMargin = 32 -- Left/right margin for experience bar 10 | UIConfig.iconPadding = { 11 | small = 3, 12 | large = 8, 13 | } 14 | UIConfig.defaultBadgeSize = 14 15 | UIConfig.potionBadgeSize = 16 16 | 17 | UIConfig.iconBox = { 18 | shadowColor = { 0, 0, 0, 0.5 }, 19 | shadowOffset = 2, 20 | backgroundColor = { 0.1, 0.1, 0.1, 0.9 }, 21 | borderColor = { 0.9, 0.85, 0.65, 1 }, 22 | badgeBackgroundColor = { 0.1, 0.1, 0.1, 0.9 }, 23 | badgeTextColor = { 0.95, 0.9, 0.7, 1 }, 24 | disabledOverlayColor = { 0, 0, 0, 0.55 }, 25 | cooldownOverlayColor = { 0, 0, 0, 0.45 }, 26 | highlightLineWidth = 2, 27 | borderLineWidth = 2, 28 | smallCornerRadius = 3, 29 | largeCornerRadius = 4, 30 | } 31 | 32 | ---Calculate spacing between health/mana bars 33 | ---@return number spacing The spacing between bars 34 | function UIConfig.getBarSpacing() 35 | return UIConfig.buttonSize - (UIConfig.barHeight * 2) 36 | end 37 | 38 | ---Get health bar width based on screen width 39 | ---@param screenWidth number Screen width 40 | ---@return number width The health bar width 41 | function UIConfig.getHealthBarWidth(screenWidth) 42 | return math.min(screenWidth * 0.3, 240) 43 | end 44 | 45 | ---Get bottom bar positions (mana bar, health bar, button Y positions) 46 | ---@param screenHeight number Screen height 47 | ---@return table positions Table with manaBarY, healthBarY, buttonY 48 | function UIConfig.getBottomBarPositions(screenHeight) 49 | local spacing = UIConfig.getBarSpacing() 50 | local manaBarY = screenHeight - UIConfig.barHeight - UIConfig.bottomOffset 51 | local healthBarY = manaBarY - UIConfig.barHeight - spacing 52 | local buttonY = healthBarY 53 | 54 | return { 55 | manaBarY = manaBarY, 56 | healthBarY = healthBarY, 57 | buttonY = buttonY, 58 | } 59 | end 60 | 61 | ---Get experience bar position and dimensions 62 | ---@param screenWidth number Screen width 63 | ---@param screenHeight number Screen height 64 | ---@return table position Table with barX, barY, barWidth, barHeight 65 | function UIConfig.getExperienceBarPosition(screenWidth, screenHeight) 66 | local barX = UIConfig.barX 67 | local barY = screenHeight - UIConfig.barHeight - 8 68 | local barWidth = screenWidth - barX - UIConfig.sideMargin 69 | 70 | return { 71 | barX = barX, 72 | barY = barY, 73 | barWidth = barWidth, 74 | barHeight = UIConfig.barHeight, 75 | } 76 | end 77 | 78 | return UIConfig 79 | -------------------------------------------------------------------------------- /Diablo.love/data/biomes.lua: -------------------------------------------------------------------------------- 1 | local biomes = {} 2 | 3 | local biomeList = { 4 | { 5 | id = "forest", 6 | label = "Verdant Forest", 7 | noise = { min = 0.0, max = 0.45 }, 8 | tileColors = { 9 | primary = { 0.09, 0.26, 0.12, 1 }, 10 | secondary = { 0.13, 0.32, 0.16, 1 }, 11 | accent = { 0.2, 0.4, 0.22, 1 }, 12 | }, 13 | propWeights = { 14 | { id = "shrub", weight = 3 }, 15 | { id = "stone", weight = 1 }, 16 | }, 17 | structureWeights = { 18 | { id = "tree_cluster", weight = 4 }, 19 | { id = "forest_hut", weight = 0.6 }, 20 | }, 21 | foeWeights = { 22 | { id = "goblin1", weight = 3 }, 23 | { id = "orc1", weight = 3 }, 24 | }, 25 | }, 26 | { 27 | id = "desert", 28 | label = "Scorched Expanse", 29 | noise = { min = 0.45, max = 0.75 }, 30 | tileColors = { 31 | primary = { 0.58, 0.45, 0.24, 1 }, 32 | secondary = { 0.64, 0.5, 0.28, 1 }, 33 | accent = { 0.74, 0.58, 0.32, 1 }, 34 | }, 35 | propWeights = { 36 | { id = "dune", weight = 3 }, 37 | { id = "dry_brush", weight = 1 }, 38 | }, 39 | structureWeights = { 40 | { id = "desert_rock", weight = 5 }, 41 | { id = "ruined_obelisk", weight = 0.8 }, 42 | }, 43 | foeWeights = { 44 | { id = "goblin2", weight = 2 }, 45 | { id = "orc2", weight = 2 }, 46 | }, 47 | }, 48 | { 49 | id = "tundra", 50 | label = "Frozen Tundra", 51 | noise = { min = 0.75, max = 1.0 }, 52 | tileColors = { 53 | primary = { 0.78, 0.84, 0.89, 1 }, 54 | secondary = { 0.86, 0.91, 0.95, 1 }, 55 | accent = { 0.7, 0.77, 0.84, 1 }, 56 | }, 57 | propWeights = { 58 | { id = "snow_drift", weight = 2 }, 59 | { id = "ice_rock", weight = 1 }, 60 | }, 61 | structureWeights = { 62 | { id = "ice_spike", weight = 4 }, 63 | { id = "frozen_ruin", weight = 0.4 }, 64 | }, 65 | foeWeights = { 66 | { id = "goblin3", weight = 2 }, 67 | { id = "orc3", weight = 2 }, 68 | }, 69 | }, 70 | } 71 | 72 | local biomeIndex = {} 73 | for _, biome in ipairs(biomeList) do 74 | biomeIndex[biome.id] = biome 75 | end 76 | 77 | function biomes.getAll() 78 | return biomeList 79 | end 80 | 81 | function biomes.getById(id) 82 | return biomeIndex[id] 83 | end 84 | 85 | function biomes.findByNoiseValue(value) 86 | for _, biome in ipairs(biomeList) do 87 | if value >= biome.noise.min and value < biome.noise.max then 88 | return biome 89 | end 90 | end 91 | return biomeList[#biomeList] 92 | end 93 | 94 | return biomes 95 | -------------------------------------------------------------------------------- /Diablo.love/systems/helpers/scrollable_content.lua: -------------------------------------------------------------------------------- 1 | ---Generic scrollable content helper for managing scroll state in scenes. 2 | ---Provides scroll position tracking, bounds clamping, and viewport calculations. 3 | local ScrollableContent = {} 4 | 5 | ---Initialize scroll state for a scene. 6 | ---@param scene table Scene object to attach scroll state to 7 | ---@param viewportHeight number Height of visible area 8 | ---@param contentHeight number Total height of scrollable content 9 | ---@return table scrollState Initialized scroll state 10 | function ScrollableContent.init(scene, viewportHeight, contentHeight) 11 | local scrollState = { 12 | scrollY = 0, 13 | viewportHeight = viewportHeight, 14 | contentHeight = contentHeight, 15 | } 16 | scene.scrollState = scrollState 17 | ScrollableContent.updateBounds(scrollState) 18 | return scrollState 19 | end 20 | 21 | ---Update scroll bounds based on current content and viewport heights. 22 | ---@param scrollState table Scroll state to update 23 | function ScrollableContent.updateBounds(scrollState) 24 | scrollState.maxScrollY = math.max(0, scrollState.contentHeight - scrollState.viewportHeight) 25 | -- Clamp current scroll position to new bounds 26 | if scrollState.scrollY > scrollState.maxScrollY then 27 | scrollState.scrollY = scrollState.maxScrollY 28 | end 29 | if scrollState.scrollY < 0 then 30 | scrollState.scrollY = 0 31 | end 32 | end 33 | 34 | ---Update scroll position by delta amount. 35 | ---@param scrollState table Scroll state to update 36 | ---@param dy number Delta Y (positive = scroll down, negative = scroll up) 37 | ---@param scrollSpeed number|nil Optional scroll speed multiplier (default: 30) 38 | function ScrollableContent.updateScroll(scrollState, dy, scrollSpeed) 39 | scrollSpeed = scrollSpeed or 30 40 | local delta = dy * scrollSpeed 41 | 42 | scrollState.scrollY = scrollState.scrollY + delta 43 | scrollState.scrollY = math.max(0, math.min(scrollState.scrollY, scrollState.maxScrollY)) 44 | end 45 | 46 | ---Get the visible area rectangle for clipping. 47 | ---@param scrollState table Scroll state 48 | ---@param contentX number X position of content area 49 | ---@param contentY number Y position of content area 50 | ---@param contentWidth number Width of content area 51 | ---@return number x, number y, number width, number height Visible area coordinates 52 | function ScrollableContent.getVisibleArea(scrollState, contentX, contentY, contentWidth) 53 | return contentX, contentY, contentWidth, scrollState.viewportHeight 54 | end 55 | 56 | ---Check if scrolling is needed (content exceeds viewport). 57 | ---@param scrollState table Scroll state 58 | ---@return boolean True if scrolling is needed 59 | function ScrollableContent.needsScrolling(scrollState) 60 | return scrollState.contentHeight > scrollState.viewportHeight 61 | end 62 | 63 | return ScrollableContent 64 | -------------------------------------------------------------------------------- /Diablo.love/entities/player.lua: -------------------------------------------------------------------------------- 1 | local Player = {} 2 | Player.__index = Player 3 | 4 | ---Create a player entity with position and size defaults. 5 | ---@param opts table|nil 6 | ---@return Player 7 | function Player.new(opts) 8 | opts = opts or {} 9 | 10 | local createInventory = require("components.inventory") 11 | local createEquipment = require("components.equipment") 12 | local createBaseStats = require("components.base_stats") 13 | local createPosition = require("components.position") 14 | local createSize = require("components.size") 15 | local createMovement = require("components.movement") 16 | local createRenderable = require("components.renderable") 17 | local createPlayerControlled = require("components.player_controlled") 18 | local createHealth = require("components.health") 19 | local createMana = require("components.mana") 20 | local createCombat = require("components.combat") 21 | local createPotions = require("components.potions") 22 | local createSkills = require("components.skills") 23 | local createExperience = require("components.experience") 24 | local createPhysicsBody = require("components.physics_body") 25 | local createInactive = require("components.inactive") 26 | local createTargeting = require("components.targeting") 27 | 28 | local entity = { 29 | id = opts.id or "player", 30 | position = createPosition({ 31 | x = opts.x or 0, 32 | y = opts.y or 0, 33 | }), 34 | size = createSize({ 35 | w = opts.width, 36 | h = opts.height, 37 | }), 38 | inventory = createInventory(), 39 | equipment = createEquipment(), 40 | baseStats = createBaseStats(opts.baseStats), 41 | movement = createMovement(opts.movement), 42 | renderable = createRenderable(opts.renderable), 43 | playerControlled = createPlayerControlled(), 44 | health = createHealth(opts.health), 45 | mana = createMana(opts.mana), 46 | combat = createCombat(opts.combat), 47 | potions = createPotions(), 48 | skills = createSkills(), 49 | experience = createExperience(opts.experience or { 50 | level = 1, 51 | currentXP = 0, 52 | }), 53 | physicsBody = createPhysicsBody({ 54 | bodyType = "dynamic", 55 | fixedRotation = (opts.physicsBody and opts.physicsBody.fixedRotation) ~= false, 56 | linearDamping = (opts.physicsBody and opts.physicsBody.linearDamping) or 20, 57 | friction = opts.physicsBody and opts.physicsBody.friction, 58 | density = opts.physicsBody and opts.physicsBody.density, 59 | userData = opts.physicsBody and opts.physicsBody.userData, 60 | }), 61 | inactive = createInactive(), 62 | targeting = createTargeting(), 63 | } 64 | 65 | return setmetatable(entity, Player) 66 | end 67 | 68 | return Player 69 | -------------------------------------------------------------------------------- /Diablo.love/systems/combat/player_attack.lua: -------------------------------------------------------------------------------- 1 | local vector = require("modules.vector") 2 | local Targeting = require("systems.helpers.targeting") 3 | local coordinates = require("systems.helpers.coordinates") 4 | local combatTiming = require("systems.helpers.combat_timing") 5 | local soundHelper = require("systems.helpers.sound") 6 | 7 | local playerAttackSystem = {} 8 | 9 | function playerAttackSystem.update(world, dt) 10 | Targeting.tick(world, dt) 11 | 12 | local player = world:getPlayer() 13 | if not player then 14 | return 15 | end 16 | 17 | local combat = player.combat 18 | if not combat then 19 | return 20 | end 21 | 22 | combatTiming.updateTimers(combat, dt) 23 | 24 | local input = 25 | world.input and world.input.mouse and world.input.mouse.primary 26 | if not input then 27 | return 28 | end 29 | 30 | local wantsAttack = input.held or input.pressed 31 | if not wantsAttack then 32 | return 33 | end 34 | 35 | if input.consumedClickId == input.clickId then 36 | return 37 | end 38 | 39 | local target = Targeting.getCurrentTarget(world) 40 | if wantsAttack then 41 | local desiredRange = combatTiming.getRange(combat) 42 | target = Targeting.resolveMouseTarget(world, { range = desiredRange }) or target 43 | end 44 | 45 | if combat.cooldown > 0 then 46 | return 47 | end 48 | 49 | if combat.queuedAttack then 50 | return 51 | end 52 | 53 | -- Always trigger swing animation and cooldown for visual feedback 54 | local effectiveAttackSpeed = combatTiming.computeEffectiveAttackSpeed(player) 55 | combatTiming.beginSwing(combat, { 56 | attackSpeed = effectiveAttackSpeed, 57 | timeStamp = world.time or 0, 58 | }) 59 | 60 | -- Only queue damage if valid target exists and is in range 61 | if not target then 62 | soundHelper.playMissSound() 63 | return 64 | end 65 | 66 | local targetHealth = target.health 67 | if not targetHealth or targetHealth.current <= 0 then 68 | soundHelper.playMissSound() 69 | return 70 | end 71 | 72 | local playerX, playerY = coordinates.getEntityCenter(player) 73 | local targetX, targetY = coordinates.getEntityCenter(target) 74 | if not playerX or not targetX then 75 | soundHelper.playMissSound() 76 | return 77 | end 78 | 79 | local range = combatTiming.getRange(combat) 80 | local distance = vector.distance(playerX, playerY, targetX, targetY) 81 | if distance > range then 82 | soundHelper.playMissSound() 83 | return 84 | end 85 | 86 | -- Queue the attack for damage computation 87 | combat.queuedAttack = { 88 | targetId = target.id, 89 | time = world.time or 0, 90 | range = range, 91 | } 92 | 93 | -- Play attack sound effect 94 | soundHelper.playAttackSound() 95 | end 96 | 97 | return playerAttackSystem 98 | -------------------------------------------------------------------------------- /Diablo.love/systems/render/health.lua: -------------------------------------------------------------------------------- 1 | local FoeRarities = require("data.foe_rarities") 2 | 3 | local renderHealthSystem = {} 4 | 5 | function renderHealthSystem.draw(world) 6 | local camera = world.camera or { x = 0, y = 0 } 7 | 8 | love.graphics.push("all") 9 | love.graphics.translate(-camera.x, -camera.y) 10 | 11 | -- Query foes with health (only show health bars for foes, not player) 12 | local entities = world:queryEntities({ "foe", "health", "position" }) 13 | 14 | for _, entity in ipairs(entities) do 15 | if (entity.inactive and entity.inactive.isInactive) or entity.dead then 16 | goto continue 17 | end 18 | 19 | local health = entity.health 20 | local pos = entity.position 21 | local size = entity.size or { w = 20, h = 20 } 22 | local rarityId = entity.foe and entity.foe.rarity 23 | local rarity = rarityId and FoeRarities.getById(rarityId) or nil 24 | 25 | local maxHealth = health.max or 1 26 | local current = math.max(0, math.min(health.current or 0, maxHealth)) 27 | 28 | -- Always show bar for elites/bosses; otherwise only when damaged 29 | local shouldShow = rarityId == "elite" or rarityId == "boss" or current < maxHealth 30 | if not shouldShow then 31 | goto continue 32 | end 33 | 34 | local ratio = maxHealth > 0 and (current / maxHealth) or 0 35 | 36 | local barWidth = math.max(size.w, 36) 37 | if rarityId == "boss" then 38 | barWidth = math.max(size.w * 1.1, 44) 39 | elseif rarityId == "elite" then 40 | barWidth = math.max(size.w * 1.05, 40) 41 | end 42 | local barHeight = 6 43 | local barX = pos.x + (size.w - barWidth) / 2 44 | local barY = pos.y - 26 45 | 46 | love.graphics.push("all") 47 | 48 | love.graphics.setColor(0, 0, 0, 0.6) 49 | love.graphics.rectangle("fill", barX - 2, barY - 2, barWidth + 4, barHeight + 4, 3, 3) 50 | 51 | if rarityId == "boss" then 52 | love.graphics.setColor(0.2, 0.12, 0.25, 0.95) 53 | elseif rarityId == "elite" then 54 | love.graphics.setColor(0.12, 0.16, 0.25, 0.95) 55 | else 56 | love.graphics.setColor(0.15, 0.15, 0.18, 0.9) 57 | end 58 | love.graphics.rectangle("fill", barX, barY, barWidth, barHeight, 3, 3) 59 | 60 | love.graphics.setColor(0.85, 0.2, 0.2, 1) 61 | love.graphics.rectangle("fill", barX, barY, barWidth * ratio, barHeight, 3, 3) 62 | 63 | local frameColor = { 0.9, 0.85, 0.65, 1 } 64 | if rarity and rarity.tint then 65 | frameColor = { rarity.tint[1], rarity.tint[2], rarity.tint[3], 1 } 66 | end 67 | 68 | love.graphics.setColor(frameColor) 69 | love.graphics.setLineWidth(1.2) 70 | love.graphics.rectangle("line", barX, barY, barWidth, barHeight, 3, 3) 71 | 72 | love.graphics.pop() 73 | 74 | ::continue:: 75 | end 76 | 77 | love.graphics.pop() 78 | end 79 | 80 | return renderHealthSystem 81 | -------------------------------------------------------------------------------- /spec/systems/ai/chase_spec.lua: -------------------------------------------------------------------------------- 1 | local helper = require("spec.spec_helper") 2 | local TestWorld = require("spec.support.test_world") 3 | 4 | local chaseSystem = require("systems.ai.chase") 5 | 6 | describe("systems.ai.chase", function() 7 | local world 8 | local player 9 | 10 | local function addFoe(opts) 11 | opts = opts or {} 12 | local foe = helper.buildEntity({ 13 | id = opts.id or ("foe_" .. tostring(math.random(1000, 9999))), 14 | position = opts.position or { x = 0, y = 0 }, 15 | size = opts.size or { w = 32, h = 32 }, 16 | movement = { 17 | speed = opts.speed or 120, 18 | vx = 0, 19 | vy = 0, 20 | }, 21 | chase = { 22 | targetId = player.id, 23 | separationBuffer = opts.separationBuffer, 24 | }, 25 | }) 26 | 27 | world:addEntity(foe) 28 | return foe 29 | end 30 | 31 | before_each(function() 32 | world = TestWorld.new() 33 | 34 | function world:getEntity(id) 35 | return self.entities[id] 36 | end 37 | 38 | player = helper.buildEntity({ 39 | id = "player_1", 40 | position = { x = 200, y = 0 }, 41 | size = { w = 32, h = 32 }, 42 | movement = { speed = 0 }, 43 | }) 44 | 45 | world:addEntity(player) 46 | end) 47 | 48 | it("sets movement toward target while respecting separation buffer", function() 49 | local foe = addFoe({ position = { x = 0, y = 0 } }) 50 | 51 | chaseSystem.update(world, 0.016) 52 | 53 | assert.is_true(foe.movement.vx > 0) 54 | assert.is_true(math.abs(foe.movement.vy) < 0.01) 55 | assert.is_true((foe.movement.maxDistance or 0) > 0) 56 | end) 57 | 58 | it("stops movement when within stop distance", function() 59 | -- Place foe near player within combined radius + buffer 60 | local foe = addFoe({ position = { x = 160, y = 0 } }) 61 | 62 | chaseSystem.update(world, 0.016) 63 | 64 | assert.equal(0, foe.movement.vx) 65 | assert.equal(0, foe.movement.vy) 66 | assert.equal(0, foe.movement.maxDistance) 67 | end) 68 | 69 | it("applies separation between multiple foes chasing same target", function() 70 | local upperFoe = addFoe({ id = "foe_upper", position = { x = 0, y = -30 } }) 71 | local lowerFoe = addFoe({ id = "foe_lower", position = { x = 0, y = 30 } }) 72 | 73 | chaseSystem.update(world, 0.016) 74 | 75 | assert.is_true( 76 | upperFoe.movement.vy * lowerFoe.movement.vy < 0, 77 | "foes should diverge vertically due to separation" 78 | ) 79 | end) 80 | 81 | it("ignores inactive foes", function() 82 | local foe = addFoe({ position = { x = 0, y = 0 } }) 83 | foe.inactive = { isInactive = true } 84 | 85 | chaseSystem.update(world, 0.016) 86 | 87 | assert.equal(0, foe.movement.vx) 88 | assert.equal(0, foe.movement.vy) 89 | end) 90 | end) 91 | -------------------------------------------------------------------------------- /Diablo.love/systems/render/inventory/tooltip.lua: -------------------------------------------------------------------------------- 1 | ---Render system for inventory tooltips 2 | local Tooltips = require("systems.helpers.tooltips") 3 | local EquipmentHelper = require("systems.helpers.equipment") 4 | 5 | local renderInventoryTooltip = {} 6 | 7 | ---Draw tooltip for hovered item 8 | ---@param scene table Inventory scene 9 | function renderInventoryTooltip.draw(scene) 10 | local mx, my = love.mouse.getPosition() 11 | local hovered 12 | local isInventoryItem = false 13 | 14 | -- Check inventory items (only show tooltip if item exists) 15 | for _, rect in ipairs(scene.itemRects or {}) do 16 | if mx >= rect.x and mx <= rect.x + rect.w and my >= rect.y and my <= rect.y + rect.h then 17 | if rect.item then 18 | hovered = rect.item 19 | isInventoryItem = true 20 | break 21 | end 22 | end 23 | end 24 | 25 | -- Check equipment slots 26 | if not hovered then 27 | for _, rect in ipairs(scene.equipmentRects or {}) do 28 | if mx >= rect.x and mx <= rect.x + rect.w and my >= rect.y and my <= rect.y + rect.h then 29 | hovered = rect.item 30 | break 31 | end 32 | end 33 | end 34 | 35 | -- Check attribute hovers (only if no item is hovered) 36 | local hoveredAttribute = nil 37 | if not hovered then 38 | for _, rect in ipairs(scene.attributeRects or {}) do 39 | if mx >= rect.x and mx <= rect.x + rect.w and my >= rect.y and my <= rect.y + rect.h then 40 | hoveredAttribute = rect 41 | break 42 | end 43 | end 44 | end 45 | 46 | -- Show attribute tooltip 47 | if hoveredAttribute then 48 | local attributeName = hoveredAttribute.attributeKey:gsub("^%l", string.upper) 49 | local lines = Tooltips.buildAttributeTooltipLines( 50 | hoveredAttribute.attributeKey, 51 | hoveredAttribute.attributeValue 52 | ) 53 | Tooltips.drawSimpleTooltip( 54 | attributeName, 55 | lines, 56 | mx, 57 | my, 58 | { 59 | offsetX = 16, 60 | offsetY = 16, 61 | clamp = true, 62 | } 63 | ) 64 | return 65 | end 66 | 67 | if not hovered then 68 | return 69 | end 70 | 71 | -- Only show comparison for inventory items (not already-equipped items) 72 | local isEquippedItem = not isInventoryItem 73 | local equippedItems = {} 74 | if isInventoryItem and hovered.slot then 75 | local player = scene.world:getPlayer() 76 | if player then 77 | equippedItems = EquipmentHelper.getEquippedItemsForComparison(player, hovered.slot) 78 | end 79 | end 80 | 81 | Tooltips.drawItemTooltip(hovered, mx, my, { 82 | offsetX = 16, 83 | offsetY = 16, 84 | clamp = true, 85 | equippedItems = equippedItems, 86 | isEquippedItem = isEquippedItem, 87 | }) 88 | end 89 | 90 | return renderInventoryTooltip 91 | -------------------------------------------------------------------------------- /Diablo.love/shaders/crt.glsl: -------------------------------------------------------------------------------- 1 | extern vec2 resolution; 2 | extern number time; 3 | extern number curvature; 4 | extern number scanStrength; 5 | extern number vignetteStrength; 6 | extern number noiseStrength; 7 | extern number rgbOffset; 8 | extern number sharpStrength; 9 | extern number glowStrength; 10 | extern number glowThreshold; 11 | extern number glowRadius; 12 | 13 | float hash(vec2 p) { 14 | return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); 15 | } 16 | 17 | vec2 curveCoords(vec2 uv) { 18 | uv = uv * 2.0 - 1.0; 19 | uv.x *= 1.0 + (uv.y * uv.y) * curvature; 20 | uv.y *= 1.0 + (uv.x * uv.x) * curvature; 21 | uv = uv * 0.5 + 0.5; 22 | return uv; 23 | } 24 | 25 | vec4 effect(vec4 color, Image texture, vec2 textureCoords, vec2 screenCoords) { 26 | vec2 uv = curveCoords(textureCoords); 27 | 28 | if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { 29 | return vec4(0.0, 0.0, 0.0, 1.0); 30 | } 31 | 32 | vec2 texel = 1.0 / resolution; 33 | 34 | float scan = sin((screenCoords.y / resolution.y + time * 0.5) * resolution.y * 1.2); 35 | float scanMix = mix(1.0 - scanStrength, 1.0, scan * 0.5 + 0.5); 36 | 37 | float grain = (hash(screenCoords + time) - 0.5) * noiseStrength; 38 | 39 | vec4 centerSample = Texel(texture, uv); 40 | float r = Texel(texture, uv + vec2(rgbOffset, 0.0)).r; 41 | float b = Texel(texture, uv - vec2(rgbOffset, 0.0)).b; 42 | 43 | vec4 colorSample = vec4(r, centerSample.g, b, centerSample.a); 44 | 45 | vec3 baseColor = colorSample.rgb; 46 | 47 | vec3 north = Texel(texture, uv + vec2(0.0, texel.y)).rgb; 48 | vec3 south = Texel(texture, uv - vec2(0.0, texel.y)).rgb; 49 | vec3 east = Texel(texture, uv + vec2(texel.x, 0.0)).rgb; 50 | vec3 west = Texel(texture, uv - vec2(texel.x, 0.0)).rgb; 51 | 52 | vec3 blurSample = (north + south + east + west) * 0.25; 53 | vec3 sharpened = baseColor + (baseColor - blurSample) * sharpStrength; 54 | 55 | vec2 glowStep = texel * glowRadius; 56 | vec3 glowAccum = vec3(0.0); 57 | glowAccum += Texel(texture, uv + vec2(glowStep.x, 0.0)).rgb; 58 | glowAccum += Texel(texture, uv - vec2(glowStep.x, 0.0)).rgb; 59 | glowAccum += Texel(texture, uv + vec2(0.0, glowStep.y)).rgb; 60 | glowAccum += Texel(texture, uv - vec2(0.0, glowStep.y)).rgb; 61 | glowAccum += Texel(texture, uv + glowStep).rgb; 62 | glowAccum += Texel(texture, uv - glowStep).rgb; 63 | glowAccum += Texel(texture, uv + vec2(glowStep.x, -glowStep.y)).rgb; 64 | glowAccum += Texel(texture, uv - vec2(glowStep.x, -glowStep.y)).rgb; 65 | vec3 glowColor = glowAccum * 0.125; 66 | float glowMask = max(max(glowColor.r, max(glowColor.g, glowColor.b)) - glowThreshold, 0.0); 67 | glowColor *= glowMask * glowStrength; 68 | 69 | float dist = distance(uv, vec2(0.5)); 70 | float vignette = 1.0 - smoothstep(0.3, 0.72, dist); 71 | vignette = mix(1.0 - vignetteStrength, 1.0, vignette); 72 | 73 | vec3 finalColor = (sharpened + glowColor) * scanMix; 74 | finalColor += grain; 75 | finalColor *= vignette; 76 | 77 | return vec4(finalColor, colorSample.a) * color; 78 | } 79 | -------------------------------------------------------------------------------- /spec/systems/core/movement_spec.lua: -------------------------------------------------------------------------------- 1 | local helper = require("spec.spec_helper") 2 | local TestWorld = require("spec.support.test_world") 3 | 4 | local movementSystem = require("systems.core.movement") 5 | 6 | describe("systems.core.movement", function() 7 | local world 8 | 9 | before_each(function() 10 | world = TestWorld.new() 11 | end) 12 | 13 | local function addEntity(opts) 14 | opts = opts or {} 15 | local entity = helper.buildEntity({ 16 | id = opts.id or ("entity_" .. tostring(math.random(1000, 9999))), 17 | position = opts.position or { x = 0, y = 0 }, 18 | movement = { 19 | speed = opts.speed or 100, 20 | vx = opts.vx or 1, 21 | vy = opts.vy or 0, 22 | maxDistance = opts.maxDistance, 23 | }, 24 | }) 25 | 26 | if opts.knockback then 27 | entity.knockback = opts.knockback 28 | end 29 | 30 | world:addEntity(entity) 31 | return entity 32 | end 33 | 34 | it("moves entity along normalized velocity vector", function() 35 | local entity = addEntity({ 36 | position = { x = 0, y = 0 }, 37 | speed = 120, 38 | vx = 1, 39 | vy = 0, 40 | }) 41 | 42 | movementSystem.update(world, 0.5) 43 | 44 | assert.is_true(entity.position.x > 0) 45 | assert.equal(0, entity.position.y) 46 | end) 47 | 48 | it("clamps distance when maxDistance is set", function() 49 | local entity = addEntity({ 50 | position = { x = 0, y = 0 }, 51 | speed = 200, 52 | vx = 1, 53 | vy = 0, 54 | maxDistance = 5, 55 | }) 56 | 57 | movementSystem.update(world, 0.5) 58 | 59 | assert.is_true(entity.position.x <= 5 + 1e-6) 60 | end) 61 | 62 | it("applies and then removes knockback after timer expires", function() 63 | local entity = addEntity({ 64 | position = { x = 0, y = 0 }, 65 | speed = 0, 66 | vx = 0, 67 | vy = 0, 68 | knockback = { 69 | x = 1, 70 | y = 0, 71 | timer = 0.1, 72 | maxTimer = 0.1, 73 | strength = 50, 74 | }, 75 | }) 76 | 77 | movementSystem.update(world, 0.05) 78 | assert.is_true(entity.position.x > 0) 79 | assert.is_not_nil(entity.knockback) 80 | 81 | movementSystem.update(world, 0.1) 82 | assert.is_nil(entity.knockback) 83 | end) 84 | 85 | it("skips inactive entities", function() 86 | local entity = addEntity({ 87 | position = { x = 0, y = 0 }, 88 | speed = 120, 89 | vx = 1, 90 | vy = 0, 91 | }) 92 | entity.inactive = { isInactive = true } 93 | 94 | movementSystem.update(world, 0.5) 95 | 96 | assert.equal(0, entity.position.x) 97 | assert.equal(0, entity.position.y) 98 | end) 99 | end) 100 | -------------------------------------------------------------------------------- /Diablo.love/scenes/controls.lua: -------------------------------------------------------------------------------- 1 | local renderWindowChrome = require("systems.render.window.chrome") 2 | local renderControlsList = require("systems.render.controls.list") 3 | local renderScrollbar = require("systems.render.window.scrollbar") 4 | local InputManager = require("modules.input_manager") 5 | local InputActions = require("modules.input_actions") 6 | local SceneKinds = require("modules.scene_kinds") 7 | 8 | local ControlsScene = {} 9 | ControlsScene.__index = ControlsScene 10 | 11 | function ControlsScene.new(opts) 12 | opts = opts or {} 13 | local world = assert(opts.world, "ControlsScene requires world reference") 14 | 15 | local scene = { 16 | world = world, 17 | kind = SceneKinds.CONTROLS, 18 | title = "Controls", 19 | windowLayoutOptions = { 20 | widthRatio = 0.6, 21 | heightRatio = 0.80, 22 | headerHeight = 72, 23 | padding = 28, 24 | }, 25 | systems = { 26 | draw = { 27 | renderWindowChrome.draw, 28 | renderControlsList.draw, 29 | renderScrollbar.draw, 30 | }, 31 | }, 32 | } 33 | 34 | scene.windowChromeConfig = { 35 | title = scene.title, 36 | icon = "book", 37 | } 38 | 39 | return setmetatable(scene, ControlsScene) 40 | end 41 | 42 | function ControlsScene:enter() 43 | self.windowRects = {} 44 | self.windowLayout = nil 45 | end 46 | 47 | -- luacheck: ignore 212/self 48 | function ControlsScene:exit() 49 | end 50 | 51 | -- luacheck: ignore 212/self 52 | function ControlsScene:update(_dt) 53 | end 54 | 55 | function ControlsScene:draw() 56 | love.graphics.push("all") 57 | 58 | -- Reset rects for click detection 59 | self.windowRects = {} 60 | 61 | -- Iterate through all render systems 62 | for _, system in ipairs(self.systems.draw) do 63 | system(self) 64 | end 65 | 66 | love.graphics.pop() 67 | end 68 | 69 | function ControlsScene:keypressed(key) 70 | local action = InputManager.getActionForKey(key) 71 | if action == InputActions.CLOSE_MODAL then 72 | if self.world and self.world.sceneManager then 73 | self.world.sceneManager:pop() 74 | end 75 | end 76 | end 77 | 78 | function ControlsScene:mousepressed(x, y, button) 79 | if button ~= 1 then 80 | return 81 | end 82 | 83 | local closeRect = self.windowRects and self.windowRects.close 84 | if closeRect 85 | and x >= closeRect.x 86 | and x <= closeRect.x + closeRect.w 87 | and y >= closeRect.y 88 | and y <= closeRect.y + closeRect.h 89 | then 90 | if self.world and self.world.sceneManager then 91 | self.world.sceneManager:pop() 92 | end 93 | return 94 | end 95 | end 96 | 97 | function ControlsScene:wheelmoved(_x, y) 98 | if self.scrollState then 99 | local ScrollableContent = require("systems.helpers.scrollable_content") 100 | ScrollableContent.updateScroll(self.scrollState, y) 101 | end 102 | end 103 | 104 | return ControlsScene 105 | -------------------------------------------------------------------------------- /Diablo.love/systems/core/loot_tooltip.lua: -------------------------------------------------------------------------------- 1 | local Tooltips = require("systems.helpers.tooltips") 2 | local EquipmentHelper = require("systems.helpers.equipment") 3 | 4 | local lootTooltipSystem = {} 5 | 6 | function lootTooltipSystem.draw(world) 7 | local coordsHelper = world.systemHelpers and world.systemHelpers.coordinates 8 | if not coordsHelper or not coordsHelper.toWorldFromScreen then 9 | return 10 | end 11 | 12 | local camera = world.camera or { x = 0, y = 0 } 13 | local pointerX, pointerY = love.mouse.getPosition() 14 | local worldX, worldY = coordsHelper.toWorldFromScreen(camera, pointerX, pointerY) 15 | 16 | local loots = world:queryEntities({ "lootable", "position" }) 17 | local hovered = nil 18 | 19 | for _, loot in ipairs(loots) do 20 | if loot.inactive and loot.inactive.isInactive then 21 | goto continue 22 | end 23 | 24 | local pos = loot.position 25 | local size = loot.size or { w = 16, h = 16 } 26 | 27 | if worldX >= pos.x and worldX <= pos.x + size.w and worldY >= pos.y and worldY <= pos.y + size.h then 28 | hovered = loot 29 | break 30 | end 31 | 32 | ::continue:: 33 | end 34 | 35 | if not hovered or not hovered.lootable then 36 | return 37 | end 38 | 39 | local lootable = hovered.lootable 40 | 41 | if lootable.item then 42 | -- Collect equipped items for comparison - only compare against the same slot 43 | local player = world:getPlayer() 44 | local equippedItems = {} 45 | if player and lootable.item.slot then 46 | equippedItems = EquipmentHelper.getEquippedItemsForComparison(player, lootable.item.slot) 47 | end 48 | 49 | Tooltips.drawItemTooltip(lootable.item, pointerX, pointerY, { 50 | offsetX = 18, 51 | offsetY = 18, 52 | clamp = true, 53 | equippedItems = equippedItems, 54 | isEquippedItem = false, 55 | }) 56 | return 57 | end 58 | 59 | if lootable.gold and lootable.gold > 0 then 60 | local padding = 8 61 | local label = string.format("%d Gold", lootable.gold) 62 | local font = love.graphics.getFont() 63 | local width = font:getWidth(label) + padding * 2 64 | local height = font:getHeight() + padding * 2 65 | local tooltipX = pointerX + 18 66 | local tooltipY = pointerY + 18 67 | 68 | local screenWidth, screenHeight = love.graphics.getDimensions() 69 | if tooltipX + width > screenWidth then 70 | tooltipX = screenWidth - width - 8 71 | end 72 | if tooltipY + height > screenHeight then 73 | tooltipY = screenHeight - height - 8 74 | end 75 | 76 | love.graphics.push("all") 77 | 78 | love.graphics.setColor(0, 0, 0, 0.85) 79 | love.graphics.rectangle("fill", tooltipX, tooltipY, width, height, 6, 6) 80 | 81 | love.graphics.setColor(1, 0.9, 0.4, 1) 82 | love.graphics.setLineWidth(2) 83 | love.graphics.rectangle("line", tooltipX, tooltipY, width, height, 6, 6) 84 | 85 | love.graphics.print(label, tooltipX + padding, tooltipY + padding) 86 | 87 | love.graphics.pop() 88 | end 89 | end 90 | 91 | return lootTooltipSystem 92 | --------------------------------------------------------------------------------