diff --git a/data/content/attacks.json b/data/content/attacks.json new file mode 100644 index 0000000..d1d0a2b --- /dev/null +++ b/data/content/attacks.json @@ -0,0 +1,22 @@ +{ + "slash_quick": {"name":"Quick Slash","mode":"melee_arc","motion":"slash","reach":72,"arc_degrees":100,"duration":0.22,"active_start":0.05,"active_end":0.15,"hitbox_radius":18,"damage_mult":1.0,"knockback_mult":1.0,"visual_id":"fx_slash"}, + "slash_heavy": {"name":"Heavy Cleave","mode":"melee_arc","motion":"slash","reach":86,"arc_degrees":128,"duration":0.42,"active_start":0.12,"active_end":0.28,"hitbox_radius":23,"damage_mult":1.45,"knockback_mult":1.55,"visual_id":"fx_slash"}, + "axe_cleave": {"name":"Axe Cleave","mode":"melee_arc","motion":"slash","reach":80,"arc_degrees":145,"duration":0.38,"active_start":0.10,"active_end":0.26,"hitbox_radius":24,"damage_mult":1.28,"knockback_mult":1.7,"visual_id":"fx_slash"}, + "dagger_stab": {"name":"Dagger Flurry","mode":"melee_thrust","motion":"thrust","reach":58,"width":18,"duration":0.16,"active_start":0.03,"active_end":0.10,"damage_mult":0.72,"knockback_mult":0.45,"visual_id":"fx_slash"}, + "spear_thrust": {"name":"Spear Thrust","mode":"melee_thrust","motion":"thrust","reach":112,"width":22,"duration":0.34,"active_start":0.10,"active_end":0.23,"damage_mult":1.16,"knockback_mult":1.35,"visual_id":"fx_slash"}, + "hammer_slam": {"name":"Hammer Slam","mode":"melee_slam","motion":"slam","reach":66,"radius":54,"duration":0.58,"active_start":0.25,"active_end":0.36,"damage_mult":1.75,"knockback_mult":2.25,"visual_id":"fx_explosion"}, + "bow_shot": {"name":"Bow Shot","mode":"ranged","motion":"draw","duration":0.48,"release_time":0.27,"damage_mult":1.0,"knockback_mult":0.72,"default_projectile":"arrow_wood","visual_id":"fx_arrow"}, + "crossbow_shot": {"name":"Crossbow Bolt","mode":"ranged","motion":"recoil","duration":0.62,"release_time":0.18,"damage_mult":1.42,"knockback_mult":1.25,"default_projectile":"bolt_iron","visual_id":"fx_arrow"}, + "staff_cast": {"name":"Staff Cast","mode":"ranged","motion":"thrust","duration":0.50,"release_time":0.22,"damage_mult":1.12,"knockback_mult":0.65,"default_projectile":"arcane_bolt","visual_id":"fx_arcane"}, + "wand_cast": {"name":"Wand Flick","mode":"ranged","motion":"flick","duration":0.30,"release_time":0.12,"damage_mult":0.78,"knockback_mult":0.35,"default_projectile":"arcane_bolt","visual_id":"fx_arcane"}, + "ember_lance": {"name":"Ember Lance","mode":"ranged","motion":"thrust","duration":0.52,"release_time":0.23,"damage_mult":1.22,"knockback_mult":0.95,"default_projectile":"ember_orb","visual_id":"fx_fireball"}, + "frost_shard": {"name":"Frost Shard","mode":"ranged","motion":"thrust","duration":0.46,"release_time":0.20,"damage_mult":1.05,"knockback_mult":0.78,"default_projectile":"frost_shard","visual_id":"fx_arcane"}, + "void_orb": {"name":"Void Orb","mode":"ranged","motion":"cast","duration":0.66,"release_time":0.34,"damage_mult":1.5,"knockback_mult":1.1,"default_projectile":"void_orb","visual_id":"fx_void"}, + "sun_spear": {"name":"Sun Spear","mode":"ranged","motion":"thrust","duration":0.58,"release_time":0.24,"damage_mult":1.3,"knockback_mult":1.05,"default_projectile":"holy_spark","visual_id":"fx_holy"}, + "thorn_burst": {"name":"Thorn Burst","mode":"ranged","motion":"cast","duration":0.55,"release_time":0.28,"damage_mult":1.0,"knockback_mult":0.9,"default_projectile":"thorn_spine","visual_id":"fx_arcane"}, + "enemy_bite": {"name":"Bite","mode":"melee_arc","motion":"bite","reach":42,"arc_degrees":70,"duration":0.48,"damage_mult":1.0,"knockback_mult":0.8}, + "enemy_claw": {"name":"Claw","mode":"melee_arc","motion":"slash","reach":52,"arc_degrees":95,"duration":0.55,"damage_mult":1.05,"knockback_mult":0.85}, + "enemy_slam": {"name":"Slam","mode":"melee_slam","motion":"slam","reach":48,"radius":50,"duration":0.9,"damage_mult":1.4,"knockback_mult":1.8}, + "enemy_spit": {"name":"Spit","mode":"ranged","motion":"cast","duration":1.1,"release_time":0.55,"damage_mult":0.9,"knockback_mult":0.55,"default_projectile":"acid_spit"}, + "enemy_bolt": {"name":"Hex Bolt","mode":"ranged","motion":"cast","duration":1.0,"release_time":0.48,"damage_mult":1.0,"knockback_mult":0.6,"default_projectile":"enemy_hex"} +} \ No newline at end of file diff --git a/data/content/items.json b/data/content/items.json new file mode 100644 index 0000000..7f25fe9 --- /dev/null +++ b/data/content/items.json @@ -0,0 +1,28 @@ +{ + "training_sword": {"name":"Training Sword","kind":"weapon","slot":"weapon","weapon_class":"sword","tier":1,"power":5,"attack_id":"slash_quick","projectile_id":"","attack_speed":1.0,"damage_mult":1.0,"knockback":0.8,"inventory_sprite":"item_sword_training","equipped_sheet":"weapon_sword_training","attack_sprite":"weapon_sword_training","biomes":["Greenlands"]}, + "frontier_sabre": {"name":"Frontier Sabre","kind":"weapon","slot":"weapon","weapon_class":"sword","tier":2,"power":12,"attack_id":"slash_quick","projectile_id":"","attack_speed":1.16,"damage_mult":1.08,"knockback":0.9,"inventory_sprite":"item_sword_frontier","equipped_sheet":"weapon_sword_frontier","attack_sprite":"weapon_sword_frontier","biomes":["Greenlands","Ancient Forest"]}, + "ruin_greatsword": {"name":"Ruin Greatsword","kind":"weapon","slot":"weapon","weapon_class":"greatsword","tier":4,"power":31,"attack_id":"slash_heavy","projectile_id":"","attack_speed":0.72,"damage_mult":1.38,"knockback":1.8,"inventory_sprite":"item_greatsword_ruin","equipped_sheet":"weapon_greatsword_ruin","attack_sprite":"weapon_greatsword_ruin","biomes":["Iron Hills","Ash Wastes"]}, + "woodsman_axe": {"name":"Woodsman's Axe","kind":"weapon","slot":"weapon","weapon_class":"axe","tier":1,"power":8,"attack_id":"axe_cleave","projectile_id":"","attack_speed":0.88,"damage_mult":1.12,"knockback":1.35,"inventory_sprite":"item_axe_woodsman","equipped_sheet":"weapon_axe_woodsman","attack_sprite":"weapon_axe_woodsman","biomes":["Greenlands","Ancient Forest"]}, + "cinder_executioner": {"name":"Cinder Executioner","kind":"weapon","slot":"weapon","weapon_class":"axe","tier":5,"power":42,"attack_id":"axe_cleave","projectile_id":"","attack_speed":0.82,"damage_mult":1.48,"knockback":2.0,"inventory_sprite":"item_axe_cinder","equipped_sheet":"weapon_axe_cinder","attack_sprite":"weapon_axe_cinder","biomes":["Ash Wastes"]}, + "militia_spear": {"name":"Militia Spear","kind":"weapon","slot":"weapon","weapon_class":"spear","tier":1,"power":7,"attack_id":"spear_thrust","projectile_id":"","attack_speed":0.94,"damage_mult":1.0,"knockback":1.15,"inventory_sprite":"item_spear_militia","equipped_sheet":"weapon_spear_militia","attack_sprite":"weapon_spear_militia","biomes":["Greenlands"]}, + "frost_pike": {"name":"Frost Pike","kind":"weapon","slot":"weapon","weapon_class":"spear","tier":4,"power":34,"attack_id":"spear_thrust","projectile_id":"","attack_speed":1.02,"damage_mult":1.28,"knockback":1.55,"inventory_sprite":"item_spear_frost","equipped_sheet":"weapon_spear_frost","attack_sprite":"weapon_spear_frost","biomes":["Frostwild"]}, + "mire_dagger": {"name":"Mirefang Dagger","kind":"weapon","slot":"weapon","weapon_class":"dagger","tier":2,"power":13,"attack_id":"dagger_stab","projectile_id":"","attack_speed":1.65,"damage_mult":0.86,"knockback":0.42,"inventory_sprite":"item_dagger_mire","equipped_sheet":"weapon_dagger_mire","attack_sprite":"weapon_dagger_mire","biomes":["Mistfen"]}, + "quarry_maul": {"name":"Quarry Maul","kind":"weapon","slot":"weapon","weapon_class":"hammer","tier":3,"power":24,"attack_id":"hammer_slam","projectile_id":"","attack_speed":0.58,"damage_mult":1.46,"knockback":2.35,"inventory_sprite":"item_hammer_quarry","equipped_sheet":"weapon_hammer_quarry","attack_sprite":"weapon_hammer_quarry","biomes":["Iron Hills"]}, + "hunter_bow": {"name":"Hunter Bow","kind":"weapon","slot":"weapon","weapon_class":"bow","tier":1,"power":8,"attack_id":"bow_shot","projectile_id":"arrow_wood","attack_speed":1.0,"damage_mult":0.98,"knockback":0.62,"inventory_sprite":"item_bow_hunter","equipped_sheet":"weapon_bow_hunter","attack_sprite":"weapon_bow_hunter","biomes":["Greenlands","Ancient Forest"]}, + "ironwood_bow": {"name":"Ironwood Bow","kind":"weapon","slot":"weapon","weapon_class":"bow","tier":3,"power":23,"attack_id":"bow_shot","projectile_id":"arrow_iron","attack_speed":1.12,"damage_mult":1.12,"knockback":0.82,"inventory_sprite":"item_bow_ironwood","equipped_sheet":"weapon_bow_ironwood","attack_sprite":"weapon_bow_ironwood","biomes":["Ancient Forest","Iron Hills"]}, + "ember_bow": {"name":"Emberstring Bow","kind":"weapon","slot":"weapon","weapon_class":"bow","tier":4,"power":33,"attack_id":"bow_shot","projectile_id":"arrow_fire","attack_speed":1.06,"damage_mult":1.18,"knockback":0.7,"inventory_sprite":"item_bow_ember","equipped_sheet":"weapon_bow_ember","attack_sprite":"weapon_bow_ember","biomes":["Ash Wastes"]}, + "rime_bow": {"name":"Rimeglass Bow","kind":"weapon","slot":"weapon","weapon_class":"bow","tier":4,"power":33,"attack_id":"bow_shot","projectile_id":"arrow_frost","attack_speed":1.08,"damage_mult":1.1,"knockback":0.68,"inventory_sprite":"item_bow_rime","equipped_sheet":"weapon_bow_rime","attack_sprite":"weapon_bow_rime","biomes":["Frostwild"]}, + "iron_crossbow": {"name":"Iron Crossbow","kind":"weapon","slot":"weapon","weapon_class":"crossbow","tier":3,"power":27,"attack_id":"crossbow_shot","projectile_id":"bolt_iron","attack_speed":0.7,"damage_mult":1.42,"knockback":1.25,"inventory_sprite":"item_crossbow_iron","equipped_sheet":"weapon_crossbow_iron","attack_sprite":"weapon_crossbow_iron","biomes":["Iron Hills"]}, + "apprentice_staff": {"name":"Apprentice Staff","kind":"weapon","slot":"weapon","weapon_class":"staff","tier":1,"power":9,"attack_id":"staff_cast","projectile_id":"arcane_bolt","attack_speed":0.9,"damage_mult":1.0,"knockback":0.55,"inventory_sprite":"item_staff_apprentice","equipped_sheet":"weapon_staff_apprentice","attack_sprite":"weapon_staff_apprentice","biomes":["Greenlands"]}, + "ember_staff": {"name":"Cinderwake Staff","kind":"weapon","slot":"weapon","weapon_class":"staff","tier":4,"power":36,"attack_id":"ember_lance","projectile_id":"ember_orb","attack_speed":0.82,"damage_mult":1.34,"knockback":0.9,"inventory_sprite":"item_staff_ember","equipped_sheet":"weapon_staff_ember","attack_sprite":"weapon_staff_ember","biomes":["Ash Wastes"]}, + "frost_staff": {"name":"Rimecaller Staff","kind":"weapon","slot":"weapon","weapon_class":"staff","tier":4,"power":35,"attack_id":"frost_shard","projectile_id":"frost_shard","attack_speed":0.92,"damage_mult":1.22,"knockback":0.72,"inventory_sprite":"item_staff_frost","equipped_sheet":"weapon_staff_frost","attack_sprite":"weapon_staff_frost","biomes":["Frostwild"]}, + "void_staff": {"name":"Voidglass Staff","kind":"weapon","slot":"weapon","weapon_class":"staff","tier":5,"power":48,"attack_id":"void_orb","projectile_id":"void_orb","attack_speed":0.68,"damage_mult":1.58,"knockback":1.15,"inventory_sprite":"item_staff_void","equipped_sheet":"weapon_staff_void","attack_sprite":"weapon_staff_void","biomes":["Mistfen","Ash Wastes"]}, + "sun_staff": {"name":"Sunward Crozier","kind":"weapon","slot":"weapon","weapon_class":"staff","tier":5,"power":46,"attack_id":"sun_spear","projectile_id":"holy_spark","attack_speed":0.76,"damage_mult":1.5,"knockback":1.0,"inventory_sprite":"item_staff_sun","equipped_sheet":"weapon_staff_sun","attack_sprite":"weapon_staff_sun","biomes":["Greenlands","Ancient Forest"]}, + "thorn_wand": {"name":"Thorn Wand","kind":"weapon","slot":"weapon","weapon_class":"wand","tier":2,"power":16,"attack_id":"thorn_burst","projectile_id":"thorn_spine","attack_speed":1.28,"damage_mult":0.92,"knockback":0.48,"inventory_sprite":"item_wand_thorn","equipped_sheet":"weapon_wand_thorn","attack_sprite":"weapon_wand_thorn","biomes":["Ancient Forest"]}, + "frontier_helm": {"name":"Frontier Helm","kind":"armor","slot":"helm","tier":1,"power":5,"inventory_sprite":"armor_helm_frontier","equipped_sheet":"armor_helm_frontier","biomes":["Greenlands"]}, + "iron_cuirass": {"name":"Deepforge Cuirass","kind":"armor","slot":"chest","tier":3,"power":24,"inventory_sprite":"armor_chest_deepforge","equipped_sheet":"armor_chest_deepforge","biomes":["Iron Hills"]}, + "briar_gloves": {"name":"Briarbound Gloves","kind":"armor","slot":"gloves","tier":2,"power":15,"inventory_sprite":"armor_gloves_briar","equipped_sheet":"armor_gloves_briar","biomes":["Ancient Forest"]}, + "mire_boots": {"name":"Mireglass Boots","kind":"armor","slot":"boots","tier":3,"power":22,"inventory_sprite":"armor_boots_mire","equipped_sheet":"armor_boots_mire","biomes":["Mistfen"]}, + "cinder_belt": {"name":"Cinderborn Warbelt","kind":"armor","slot":"belt","tier":4,"power":31,"inventory_sprite":"armor_belt_cinder","equipped_sheet":"armor_belt_cinder","biomes":["Ash Wastes"]}, + "rime_mantle": {"name":"Rimebound Mantle","kind":"armor","slot":"cape","tier":4,"power":32,"inventory_sprite":"armor_cape_rime","equipped_sheet":"armor_cape_rime","biomes":["Frostwild"]} +} \ No newline at end of file diff --git a/data/content/monsters.json b/data/content/monsters.json new file mode 100644 index 0000000..3a760ed --- /dev/null +++ b/data/content/monsters.json @@ -0,0 +1,43 @@ +{ + "raider": {"name":"Frontier Raider","biomes":["Greenlands","Iron Hills","Ash Wastes"],"behavior":"melee","attack_id":"enemy_claw","hp_mult":1.0,"speed_mult":1.0,"damage_mult":1.0,"sprite_id":"enemy_goblin","drops":["training_sword","frontier_helm"]}, + "wolf": {"name":"Grey Wolf","biomes":["Greenlands","Ancient Forest"],"behavior":"rush","attack_id":"enemy_bite","hp_mult":0.78,"speed_mult":1.38,"damage_mult":0.9,"sprite_id":"wild_ridgeback","drops":["hunter_bow"]}, + "slime": {"name":"Meadow Slime","biomes":["Greenlands"],"behavior":"melee","attack_id":"enemy_slam","hp_mult":1.15,"speed_mult":0.68,"damage_mult":0.82,"sprite_id":"wild_marsh","drops":[]}, + "boar": {"name":"Tusk Boar","biomes":["Greenlands"],"behavior":"rush","attack_id":"enemy_bite","hp_mult":1.25,"speed_mult":1.18,"damage_mult":1.18,"sprite_id":"wild_ridgeback","drops":["militia_spear"]}, + "bandit_archer": {"name":"Bandit Archer","biomes":["Greenlands"],"behavior":"ranged","attack_id":"enemy_bolt","projectile_id":"arrow_wood","hp_mult":0.8,"speed_mult":0.9,"damage_mult":0.9,"sprite_id":"enemy_goblin_archer","drops":["hunter_bow"]}, + "meadow_shaman": {"name":"Meadow Shaman","biomes":["Greenlands"],"behavior":"ranged","attack_id":"enemy_bolt","projectile_id":"enemy_hex","hp_mult":0.88,"speed_mult":0.82,"damage_mult":1.06,"sprite_id":"enemy_cultist","drops":["apprentice_staff"]}, + + "bramble": {"name":"Bramble Guard","biomes":["Ancient Forest"],"behavior":"tank","attack_id":"enemy_slam","hp_mult":1.7,"speed_mult":0.62,"damage_mult":1.18,"sprite_id":"wild_thornkin","drops":["briar_gloves"]}, + "thorn_hound": {"name":"Thorn Hound","biomes":["Ancient Forest"],"behavior":"rush","attack_id":"enemy_bite","hp_mult":0.92,"speed_mult":1.34,"damage_mult":1.02,"sprite_id":"wild_ridgeback","drops":["thorn_wand"]}, + "wisp": {"name":"Forest Wisp","biomes":["Ancient Forest","Iron Hills","Mistfen"],"behavior":"ranged","attack_id":"enemy_bolt","projectile_id":"enemy_hex","hp_mult":0.76,"speed_mult":0.9,"damage_mult":1.0,"sprite_id":"wild_wisp","drops":["thorn_wand"]}, + "bark_guard": {"name":"Bark Guard","biomes":["Ancient Forest"],"behavior":"tank","attack_id":"enemy_claw","hp_mult":1.9,"speed_mult":0.58,"damage_mult":1.22,"sprite_id":"wild_thornkin","drops":["briar_gloves"]}, + "vine_spitter": {"name":"Vine Spitter","biomes":["Ancient Forest"],"behavior":"ranged","attack_id":"enemy_spit","projectile_id":"thorn_spine","hp_mult":0.9,"speed_mult":0.78,"damage_mult":1.02,"sprite_id":"wild_thornkin","drops":["thorn_wand"]}, + "dryad": {"name":"Lost Dryad","biomes":["Ancient Forest"],"behavior":"ranged","attack_id":"thorn_burst","projectile_id":"thorn_spine","hp_mult":1.12,"speed_mult":0.94,"damage_mult":1.15,"sprite_id":"enemy_cultist","drops":["ironwood_bow","thorn_wand"]}, + + "golem": {"name":"Stone Golem","biomes":["Iron Hills"],"behavior":"tank","attack_id":"enemy_slam","hp_mult":2.0,"speed_mult":0.52,"damage_mult":1.35,"sprite_id":"wild_golem","drops":["quarry_maul","iron_cuirass"]}, + "cave_bat": {"name":"Cave Bat","biomes":["Iron Hills"],"behavior":"rush","attack_id":"enemy_bite","hp_mult":0.58,"speed_mult":1.55,"damage_mult":0.72,"sprite_id":"wild_wisp","drops":[]}, + "ironclad": {"name":"Ironclad Deserter","biomes":["Iron Hills"],"behavior":"tank","attack_id":"enemy_claw","hp_mult":1.65,"speed_mult":0.72,"damage_mult":1.28,"sprite_id":"enemy_dark_knight","drops":["iron_crossbow","iron_cuirass"]}, + "quarry_rat": {"name":"Quarry Rat","biomes":["Iron Hills"],"behavior":"rush","attack_id":"enemy_bite","hp_mult":0.72,"speed_mult":1.42,"damage_mult":0.82,"sprite_id":"wild_ridgeback","drops":[]}, + "crystal_shardling": {"name":"Crystal Shardling","biomes":["Iron Hills"],"behavior":"ranged","attack_id":"enemy_spit","projectile_id":"arcane_bolt","hp_mult":0.95,"speed_mult":0.82,"damage_mult":1.15,"sprite_id":"wild_golem","drops":["iron_crossbow"]}, + "miner_wraith": {"name":"Miner Wraith","biomes":["Iron Hills"],"behavior":"ranged","attack_id":"enemy_bolt","projectile_id":"enemy_hex","hp_mult":1.0,"speed_mult":0.96,"damage_mult":1.18,"sprite_id":"enemy_wraith","drops":["quarry_maul"]}, + + "leech": {"name":"Mire Leech","biomes":["Mistfen"],"behavior":"rush","attack_id":"enemy_bite","hp_mult":0.86,"speed_mult":1.25,"damage_mult":0.92,"sprite_id":"wild_leech","drops":["mire_dagger"]}, + "bog_slime": {"name":"Bog Slime","biomes":["Mistfen"],"behavior":"tank","attack_id":"enemy_slam","hp_mult":1.55,"speed_mult":0.58,"damage_mult":0.95,"sprite_id":"wild_marsh","drops":["mire_boots"]}, + "fen_crawler": {"name":"Fen Crawler","biomes":["Mistfen"],"behavior":"rush","attack_id":"enemy_claw","hp_mult":1.02,"speed_mult":1.3,"damage_mult":1.06,"sprite_id":"wild_leech","drops":["mire_dagger"]}, + "mire_spitter": {"name":"Mire Spitter","biomes":["Mistfen"],"behavior":"ranged","attack_id":"enemy_spit","projectile_id":"acid_spit","hp_mult":0.88,"speed_mult":0.76,"damage_mult":1.04,"sprite_id":"wild_marsh","drops":["mire_boots"]}, + "swamp_witch": {"name":"Swamp Witch","biomes":["Mistfen"],"behavior":"ranged","attack_id":"void_orb","projectile_id":"void_orb","hp_mult":1.12,"speed_mult":0.84,"damage_mult":1.28,"sprite_id":"enemy_cultist","drops":["void_staff"]}, + "lantern_wisp": {"name":"Lantern Wisp","biomes":["Mistfen"],"behavior":"ranged","attack_id":"enemy_bolt","projectile_id":"enemy_hex","hp_mult":0.72,"speed_mult":1.04,"damage_mult":0.92,"sprite_id":"wild_wisp","drops":[]}, + + "imp": {"name":"Ember Imp","biomes":["Ash Wastes"],"behavior":"ranged","attack_id":"enemy_bolt","projectile_id":"ember_orb","hp_mult":0.82,"speed_mult":0.95,"damage_mult":1.08,"sprite_id":"wild_imp","drops":["ember_staff"]}, + "cinder_hound": {"name":"Cinder Hound","biomes":["Ash Wastes"],"behavior":"rush","attack_id":"enemy_bite","hp_mult":0.96,"speed_mult":1.4,"damage_mult":1.15,"sprite_id":"wild_ridgeback","drops":["cinder_executioner"]}, + "ash_raider": {"name":"Ash Raider","biomes":["Ash Wastes"],"behavior":"melee","attack_id":"axe_cleave","hp_mult":1.18,"speed_mult":1.0,"damage_mult":1.18,"sprite_id":"enemy_goblin","drops":["cinder_executioner","cinder_belt"]}, + "magma_beetle": {"name":"Magma Beetle","biomes":["Ash Wastes"],"behavior":"tank","attack_id":"enemy_slam","hp_mult":1.8,"speed_mult":0.64,"damage_mult":1.25,"sprite_id":"wild_beetle","drops":["cinder_belt"]}, + "ember_cultist": {"name":"Ember Cultist","biomes":["Ash Wastes"],"behavior":"ranged","attack_id":"ember_lance","projectile_id":"ember_orb","hp_mult":1.02,"speed_mult":0.84,"damage_mult":1.32,"sprite_id":"enemy_cultist","drops":["ember_staff","ember_bow"]}, + "charred_golem": {"name":"Charred Golem","biomes":["Ash Wastes"],"behavior":"tank","attack_id":"enemy_slam","hp_mult":2.15,"speed_mult":0.48,"damage_mult":1.48,"sprite_id":"wild_golem","drops":["cinder_executioner"]}, + + "frostling": {"name":"Frostling","biomes":["Frostwild"],"behavior":"rush","attack_id":"enemy_claw","hp_mult":0.92,"speed_mult":1.28,"damage_mult":1.0,"sprite_id":"wild_ridgeback","drops":["rime_bow"]}, + "ice_wolf": {"name":"Ice Wolf","biomes":["Frostwild"],"behavior":"rush","attack_id":"enemy_bite","hp_mult":1.0,"speed_mult":1.45,"damage_mult":1.08,"sprite_id":"wild_ridgeback","drops":["frost_pike"]}, + "snow_wisp": {"name":"Snow Wisp","biomes":["Frostwild"],"behavior":"ranged","attack_id":"frost_shard","projectile_id":"frost_shard","hp_mult":0.75,"speed_mult":0.98,"damage_mult":1.05,"sprite_id":"wild_wisp","drops":["frost_staff"]}, + "rime_archer": {"name":"Rime Archer","biomes":["Frostwild"],"behavior":"ranged","attack_id":"bow_shot","projectile_id":"arrow_frost","hp_mult":0.88,"speed_mult":0.88,"damage_mult":1.08,"sprite_id":"enemy_skeleton_archer","drops":["rime_bow"]}, + "crystal_yeti": {"name":"Crystal Yeti","biomes":["Frostwild"],"behavior":"tank","attack_id":"enemy_slam","hp_mult":2.1,"speed_mult":0.56,"damage_mult":1.42,"sprite_id":"wild_golem","drops":["frost_pike","rime_mantle"]}, + "frost_seer": {"name":"Frost Seer","biomes":["Frostwild"],"behavior":"ranged","attack_id":"frost_shard","projectile_id":"frost_shard","hp_mult":1.12,"speed_mult":0.82,"damage_mult":1.32,"sprite_id":"enemy_wraith","drops":["frost_staff","rime_mantle"]} +} \ No newline at end of file diff --git a/data/content/projectiles.json b/data/content/projectiles.json new file mode 100644 index 0000000..795f022 --- /dev/null +++ b/data/content/projectiles.json @@ -0,0 +1,15 @@ +{ + "arrow_wood": {"name":"Wooden Arrow","speed":620,"life":1.8,"radius":5,"pierce":0,"damage_mult":1.0,"knockback_mult":1.0,"visual_id":"fx_arrow","color":"#d8c49a"}, + "arrow_iron": {"name":"Iron Arrow","speed":650,"life":1.9,"radius":5,"pierce":1,"damage_mult":1.12,"knockback_mult":1.15,"visual_id":"fx_arrow","color":"#c9d1da"}, + "arrow_fire": {"name":"Ember Arrow","speed":610,"life":1.8,"radius":6,"pierce":0,"damage_mult":1.18,"knockback_mult":0.9,"splash_radius":38,"visual_id":"fx_fireball","color":"#ff9b55"}, + "arrow_frost": {"name":"Rime Arrow","speed":600,"life":2.0,"radius":6,"pierce":0,"damage_mult":1.08,"knockback_mult":0.85,"slow":0.28,"visual_id":"fx_arcane","color":"#9ee8ff"}, + "bolt_iron": {"name":"Iron Bolt","speed":760,"life":1.7,"radius":5,"pierce":2,"damage_mult":1.22,"knockback_mult":1.35,"visual_id":"fx_arrow","color":"#d8dde6"}, + "arcane_bolt": {"name":"Arcane Bolt","speed":520,"life":2.2,"radius":7,"pierce":0,"damage_mult":1.0,"knockback_mult":0.7,"visual_id":"fx_arcane","color":"#7fcfff"}, + "ember_orb": {"name":"Ember Orb","speed":440,"life":2.3,"radius":9,"pierce":0,"damage_mult":1.14,"knockback_mult":0.9,"splash_radius":52,"visual_id":"fx_fireball","color":"#ff7f4f"}, + "frost_shard": {"name":"Frost Shard","speed":560,"life":2.0,"radius":7,"pierce":1,"damage_mult":1.02,"knockback_mult":0.75,"slow":0.35,"visual_id":"fx_arcane","color":"#a7efff"}, + "void_orb": {"name":"Void Orb","speed":360,"life":2.8,"radius":12,"pierce":1,"damage_mult":1.3,"knockback_mult":1.15,"splash_radius":64,"visual_id":"fx_void","color":"#b16de6"}, + "holy_spark": {"name":"Holy Spark","speed":500,"life":2.1,"radius":8,"pierce":1,"damage_mult":1.12,"knockback_mult":0.9,"visual_id":"fx_holy","color":"#ffe58b"}, + "thorn_spine": {"name":"Thorn Spine","speed":540,"life":2.0,"radius":6,"pierce":1,"damage_mult":1.04,"knockback_mult":1.0,"visual_id":"fx_arrow","color":"#74b85a"}, + "acid_spit": {"name":"Acid Glob","speed":320,"life":2.5,"radius":8,"pierce":0,"damage_mult":1.0,"knockback_mult":0.45,"splash_radius":36,"visual_id":"fx_arcane","color":"#8dcc62"}, + "enemy_hex": {"name":"Hex Bolt","speed":285,"life":3.0,"radius":7,"pierce":0,"damage_mult":1.0,"knockback_mult":0.5,"visual_id":"fx_void","color":"#c68ae8"} +} \ No newline at end of file diff --git a/data/content/tiles.json b/data/content/tiles.json new file mode 100644 index 0000000..89fab60 --- /dev/null +++ b/data/content/tiles.json @@ -0,0 +1,31 @@ +{ + "green_grass": {"name":"Meadow Grass","biome":"Greenlands","kind":"ground","weight":7,"color":"#456c3b","accent":"#4f7c43","walk_cost":1.0,"tags":["grass"]}, + "green_flowers": {"name":"Flower Grass","biome":"Greenlands","kind":"ground","weight":3,"color":"#4d7440","accent":"#d7c56a","walk_cost":1.0,"tags":["grass","flowers"]}, + "green_dirt": {"name":"Frontier Dirt","biome":"Greenlands","kind":"ground","weight":3,"color":"#9b8754","accent":"#aa9763","walk_cost":1.0,"tags":["dirt"]}, + "green_ruin": {"name":"Old Foundation","biome":"Greenlands","kind":"ground","weight":1,"color":"#77766e","accent":"#929189","walk_cost":1.0,"tags":["stone","ruin"]}, + + "forest_moss": {"name":"Ancient Moss","biome":"Ancient Forest","kind":"ground","weight":7,"color":"#294d37","accent":"#345c40","walk_cost":1.0,"tags":["moss"]}, + "forest_roots": {"name":"Rootbound Soil","biome":"Ancient Forest","kind":"ground","weight":4,"color":"#3b5034","accent":"#66543b","walk_cost":1.0,"tags":["roots"]}, + "forest_glade": {"name":"Glade","biome":"Ancient Forest","kind":"ground","weight":2,"color":"#466a43","accent":"#82905c","walk_cost":1.0,"tags":["grass","flowers"]}, + "forest_ruin": {"name":"Moss Ruin","biome":"Ancient Forest","kind":"ground","weight":1,"color":"#626b5d","accent":"#7e8873","walk_cost":1.0,"tags":["ruin","moss"]}, + + "iron_gravel": {"name":"Iron Gravel","biome":"Iron Hills","kind":"ground","weight":7,"color":"#55595b","accent":"#676b6e","walk_cost":1.0,"tags":["rock"]}, + "iron_slate": {"name":"Slate Shelf","biome":"Iron Hills","kind":"ground","weight":4,"color":"#454a4f","accent":"#747a80","walk_cost":1.05,"tags":["rock","slate"]}, + "iron_vein": {"name":"Exposed Vein","biome":"Iron Hills","kind":"ground","weight":2,"color":"#4c5258","accent":"#708d9d","walk_cost":1.0,"tags":["ore"]}, + "iron_quarry": {"name":"Quarry Floor","biome":"Iron Hills","kind":"ground","weight":1,"color":"#696662","accent":"#8a8378","walk_cost":1.0,"tags":["worked","stone"]}, + + "mire_mud": {"name":"Fen Mud","biome":"Mistfen","kind":"ground","weight":6,"color":"#43554a","accent":"#58685a","walk_cost":1.08,"tags":["mud"]}, + "mire_moss": {"name":"Wet Moss","biome":"Mistfen","kind":"ground","weight":5,"color":"#354f47","accent":"#47645a","walk_cost":1.0,"tags":["moss"]}, + "mire_water": {"name":"Shallow Mire","biome":"Mistfen","kind":"ground","weight":2,"color":"#3e6161","accent":"#547878","walk_cost":1.22,"tags":["water","shallow"]}, + "mire_bog": {"name":"Black Bog","biome":"Mistfen","kind":"ground","weight":1,"color":"#303f3c","accent":"#6b7251","walk_cost":1.15,"tags":["bog"]}, + + "ash_dust": {"name":"Ash Dust","biome":"Ash Wastes","kind":"ground","weight":6,"color":"#5b443e","accent":"#6d5046","walk_cost":1.0,"tags":["ash"]}, + "ash_crack": {"name":"Ember Cracks","biome":"Ash Wastes","kind":"ground","weight":4,"color":"#49332f","accent":"#a75d3f","walk_cost":1.0,"tags":["ash","ember"]}, + "ash_obsidian": {"name":"Obsidian Shelf","biome":"Ash Wastes","kind":"ground","weight":2,"color":"#373138","accent":"#5c4856","walk_cost":1.05,"tags":["obsidian"]}, + "ash_ruin": {"name":"Charred Ruin","biome":"Ash Wastes","kind":"ground","weight":1,"color":"#57443e","accent":"#88604e","walk_cost":1.0,"tags":["ruin","ash"]}, + + "frost_snow": {"name":"Packed Snow","biome":"Frostwild","kind":"ground","weight":7,"color":"#7f9eb2","accent":"#9eb9c8","walk_cost":1.0,"tags":["snow"]}, + "frost_ice": {"name":"Blue Ice","biome":"Frostwild","kind":"ground","weight":4,"color":"#6d91ad","accent":"#a4d3e6","walk_cost":1.04,"tags":["ice"]}, + "frost_rock": {"name":"Rime Rock","biome":"Frostwild","kind":"ground","weight":2,"color":"#657584","accent":"#8ca1af","walk_cost":1.0,"tags":["rock","snow"]}, + "frost_crystal": {"name":"Crystal Shelf","biome":"Frostwild","kind":"ground","weight":1,"color":"#708da6","accent":"#a8e7f2","walk_cost":1.0,"tags":["crystal"]} +} \ No newline at end of file diff --git a/docs/CONTENT_SYSTEM.md b/docs/CONTENT_SYSTEM.md new file mode 100644 index 0000000..307c31f --- /dev/null +++ b/docs/CONTENT_SYSTEM.md @@ -0,0 +1,145 @@ +# Marchbound Content System + +This pass moves combat/content identity out of one giant hardcoded combat script and into editable catalogs. + +## Shipped catalogs + +- `data/content/items.json` +- `data/content/monsters.json` +- `data/content/attacks.json` +- `data/content/projectiles.json` +- `data/content/tiles.json` + +`ContentDB` loads these files and then applies optional browser/local overrides from `user://marchbound_content_overrides.json`. + +## Content Lab + +Open **CONTENT LAB** from the bottom navigation. + +The editor is scene-driven (`scenes/ui/ContentEditor.tscn`) and supports: +- category browsing; +- raw structured JSON editing; +- preview for IDs that already exist in `VisualAtlas`; +- relationship summary; +- saving runtime overrides; +- resetting an entry to shipped data; +- validating item -> attack/projectile and monster -> attack references. + +Runtime overrides deliberately do not mutate `res://` because the Web build cannot safely write back into the shipped project. Once an override is approved, copy the JSON entry into the shipped catalog. + +## Weapon schema + +A weapon entry can define: + +```json +{ + "name": "Rimeglass Bow", + "kind": "weapon", + "slot": "weapon", + "weapon_class": "bow", + "tier": 4, + "power": 33, + "attack_id": "bow_shot", + "projectile_id": "arrow_frost", + "attack_speed": 1.08, + "damage_mult": 1.1, + "knockback": 0.68, + "inventory_sprite": "item_bow_rime", + "equipped_sheet": "weapon_bow_rime", + "attack_sprite": "weapon_bow_rime", + "biomes": ["Frostwild"] +} +``` + +### Visual contract + +- `inventory_sprite`: icon/art shown in inventory and loot UI. +- `equipped_sheet`: world-character overlay/spritesheet while equipped. +- `attack_sprite`: temporary weapon sprite shown during the attack motion. +- `attack_id`: collision/motion behavior. +- `projectile_id`: payload for ranged attacks; can vary independently of the bow/staff itself. + +The current visual pass has fallbacks when a named visual asset is not yet present. Gameplay data should not wait for final art. + +## Attack modes + +### `melee_arc` +Sword/axe style swing. Uses reach + angular arc collision. + +### `melee_thrust` +Spear/dagger style attack. Uses a forward corridor collision volume with configurable reach/width. + +### `melee_slam` +Hammer/heavy attack. Uses an impact point and radial collision. + +### `ranged` +Bow/crossbow/staff/wand/spell. Releases the selected projectile at the attack's release point. + +Attack definitions also carry motion, duration, damage multiplier, knockback multiplier and a provisional FX visual ID. + +## Projectile schema + +Projectiles can independently define: +- speed; +- lifetime; +- collision radius; +- pierce count; +- damage multiplier; +- knockback multiplier; +- splash radius; +- future slow/status metadata; +- provisional visual ID and color. + +This means one bow can fire Wooden, Iron, Ember or Rime arrows without needing four different attack implementations. + +## Monster schema + +Monsters define: +- display name; +- biome membership; +- behavior (`melee`, `rush`, `tank`, `ranged`); +- attack ID; +- optional projectile ID; +- HP/speed/damage multipliers; +- sprite ID; +- authored drop candidates. + +The first expanded catalog contains 36 normal monster identities across the six current biomes. + +## Tile schema + +Local ground variation is also data-driven. Each definition provides: +- biome; +- ground kind; +- relative weight metadata; +- base color; +- accent color; +- movement-cost hook; +- tags. + +The current pass includes 24 local ground identities. It is intentionally lightweight and browser-minded; it does not instantiate 36,864 tile nodes. + +## Current authored attack families + +Player-facing foundations include: +- Quick Slash +- Heavy Cleave +- Axe Cleave +- Dagger Flurry +- Spear Thrust +- Hammer Slam +- Bow Shot +- Crossbow Bolt +- Staff Cast +- Wand Flick +- Ember Lance +- Frost Shard +- Void Orb +- Sun Spear +- Thorn Burst + +Enemy foundations include Bite, Claw, Slam, Spit and Hex Bolt plus existing named-boss patterns. + +## Design rule + +New content should prefer **composing existing attack/projectile primitives** before adding one-off combat code. A new spear should usually be data. A genuinely new mechanic, such as a returning boomerang or chained beam, earns a new reusable attack mode. diff --git a/docs/CURRENT_STATE.md b/docs/CURRENT_STATE.md index d824d0a..a327f75 100644 --- a/docs/CURRENT_STATE.md +++ b/docs/CURRENT_STATE.md @@ -1,277 +1,235 @@ -# MARCHBOUND - Current State +# MARCHBOUND — Current State -> Canonical short status ledger. The full design canon remains in `MARCHBOUND_MASTER.md`; focused implementation notes and audits live beside this file. +> Canonical short status ledger. Full design canon lives in `MARCHBOUND_MASTER.md`; implementation-specific schemas live beside this file. -**Last updated:** 2026-08-22 -**Stage:** Pre-Alpha / M1 loop, scale and visual-production expansion -**Engine:** Godot 4.7.2, GDScript, Compatibility renderer, browser-first -**Repository:** `ramonsamagaio/marchbound` -**Visual pass:** `nyra/visual-atlas-pass-v2` passed strict CI and is ready for merge +**Last updated:** 2026-08-23 +**Stage:** Pre-Alpha / M1 depth + content expansion +**Engine:** Godot 4.7.2 · GDScript · Compatibility renderer · browser-first +**Repository:** `ramonsamagaio/marchbound` -## Playable loop +## Current playable loop -`First March -> Dawnkeep -> Warden / equipment / Warband -> Contracts -> visual World Map -> choose reachable territory + risk -> enter giant local territory -> explore / harvest / fight / build temporary defenses -> guardian / regional boss -> loot / claim / Wild Bond chance -> equip / forge / research / recruit / train -> push farther` +`First March → Dawnkeep → equipment / Warband / Contracts → World Map → choose reachable territory + risk → giant local territory → explore / harvest / fight / build field defenses → guardian / regional boss → loot / claim / Wild Bond → equip / forge / research / recruit / train → push farther` -The long-term rule is unchanged: **the player should keep inventing a reason to play one more expedition.** +Core long-game rule: **the player should keep inventing a reason to play one more expedition.** -## Current visual production pipeline +## Strategic World Map -The approved provisional atlas uploaded by the user is now a real project asset: +- effectively unbounded deterministic macro coordinates; +- Dawnkeep at `[0,0]`; +- adjacency / supply-line conquest; +- six MVP biomes; +- Threat, resource richness, boss territories and future PvP metadata; +- Frontier Claim / Monster Hunt / Resource Sweep / Ruin Siege objectives; +- Standard / Prospector / Blood Oath risk stances; +- deterministic Frontier Mutations; +- regional loot families and Wild Bond targeting; +- atlas-driven territory art. -`res://assets/marchbound_assetsprov.png` +Every macro territory currently opens to a **192×192 local-tile target**: 36,864 local tiles / 12,288×12,288 world-space. The architecture reserves a future target capacity of 3 Wardens/players per macro territory. -All atlas coordinates/semantic IDs are centralized in: +## Local territory / Necesse layer -`res://scripts/core/VisualAtlas.gd` +Current prototype: +- lightweight player-following camera rather than 36,864 scene nodes; +- physical outpost with buildings and roads; +- biome-weighted resource harvesting; +- local props; +- field construction; +- camera-relative enemy spawning; +- deterministic local ground variation. -The atlas currently supplies provisional: -- resource and system icons; -- human troops; -- enemies; -- Wild Bond creatures; -- buildings; -- battle effects / pickups; -- World Map territory art. +First field structure is the **Field Watchtower**, built from Wood harvested in the current run. It snaps to the local grid and automatically attacks enemies. -The atlas is a production bridge, not final art. Final character/equipment work should follow the approved mannequin + modular armor + assembled Warden references. +The new content pass adds **24 local ground identities** across the six current biomes. They are data-driven rather than individual hardcoded arena cases. -## Resolution / layout safety +## Data-driven Content Spine -Native design resolution is now **1920×1080**, increased from 1280×720 after the first playtest exposed crowded HUD/result panels. +A new `ContentDB` autoload moves content identity out of giant combat scripts. -Minimum practical window remains **1280×720** and canvas stretch uses `expand`. +Shipped catalogs: +- `data/content/items.json` +- `data/content/monsters.json` +- `data/content/attacks.json` +- `data/content/projectiles.json` +- `data/content/tiles.json` -The expedition result is now a centered, scroll-safe scene whose Continue button remains outside the variable-length loot body, fixing the case where a completed mission could become impossible to exit because the button fell outside the viewport. +Current first catalog size: +- **20 reusable attack definitions**; +- **13 projectile definitions**; +- **26 authored weapon/armor definitions**; +- **36 normal monster identities**; +- **24 local ground variants**. -## Scene-driven UI status +Runtime editor overrides save to `user://marchbound_content_overrides.json`, which is browser-safe and does not mutate the shipped project. -Important art-direction surfaces are being moved out of procedural GDScript. +Schema / authoring guide: `docs/CONTENT_SYSTEM.md`. -### Converted to `.tscn` -- `scenes/ui/ExpeditionLayout.tscn` -- `scenes/ui/ExpeditionResult.tscn` -- `scenes/ui/PaperDoll.tscn` +## Content Lab -The Paper Doll no longer depends on its old procedural `_draw()` body composition. Its visible stage, character art, aura, equipment slots and labels are editor-positionable nodes. +`CONTENT LAB` is now a routable scene-driven tool inside the game shell. -### Partially converted / still code-driven -- Main shell/navigation: resource icons are visual, shell still code-built. -- Inventory: Paper Doll is scene-driven, outer 3-column layout still code-built. -- World Map: territory art is atlas-driven and information panel is scroll-safe, shell still code-built. -- Dawnkeep: building art is atlas-driven, draggable world canvas remains custom/code-driven. -- Warband: atlas unit cards + qualities are visible, outer card/layout system still code-built. -- Contracts / Marketplace: still code-built and lower visual priority. +It supports: +- Items / Monsters / Attacks / Projectiles / Tiles categories; +- browse/select entries; +- editable structured JSON; +- atlas preview when an existing visual ID is available; +- relationship summary; +- persistent local/runtime overrides; +- reset-to-shipped-entry; +- attack/projectile reference validation. -Full audit: `docs/VISUAL_UI_AUDIT.md`. +Its UI lives in `scenes/ui/ContentEditor.tscn`, so the editor itself is visually adjustable in Godot. -## Strategic World Map +## Weapon / attack architecture -- effectively unbounded deterministic macro coordinates; -- Dawnkeep anchored at `[0,0]`; -- adjacency/supply-line conquest; -- pannable map window; -- six MVP biomes; -- Threat increases with distance / Frontier Season; -- resource richness; -- boss territories; -- future optional-PvP metadata; -- four objectives: Frontier Claim, Monster Hunt, Resource Sweep, Ruin Siege; -- three risk stances: Standard, Prospector, Blood Oath; -- deterministic Frontier Mutations; -- regional equipment targeting; -- regional Wild Bond targeting; -- macro tiles now use clear atlas territory art rather than primarily text/color buttons. - -Every macro World Map territory now advertises a **192×192 local-map target** and a future target capacity of **3 players/Wardens** in that territory. - -## Giant local territories - -The local expedition prototype is no longer a 900×560 one-screen arena. +Weapons now separate **content logic from art**. -Current local map target: -- **192 × 192 local tiles**; -- **64 px tile scale**; -- **36,864 tiles**; -- **12,288 × 12,288 px world-space**. +A weapon can independently define: +- `inventory_sprite` — inventory / loot visual; +- `equipped_sheet` — world-character equipped overlay / sheet; +- `attack_sprite` — temporary visible weapon used during the attack motion; +- `attack_id` — collision + timing behavior; +- `projectile_id` — ranged payload; +- attack speed; +- damage multiplier; +- knockback; +- biome / drop identity. -The current prototype renders only the visible grid/content around the Warden and uses a lightweight camera model, rather than creating 36,864 scene nodes. +Current reusable attack modes: +- **melee arc** — sword / axe style sweep; +- **melee thrust** — spear / dagger forward collision corridor; +- **melee slam** — hammer impact radius; +- **ranged** — bow / crossbow / staff / wand / spell projectile release. -The local territory already contains a physical atlas-driven frontier outpost with buildings, roads, resource nodes and environmental props. - -This is the base for the intended Necesse + Vampire Survivors local layer. - -## Local building during expeditions - -Building is now gameplay, not only scenery. - -First field-building prototype: -- harvest Wood during the expedition; -- press **B** on a free local grid position; -- spend **6 Wood harvested in that run**; -- build a Field Watchtower; -- watchtowers automatically attack enemies in range; -- placement snaps to the 64 px local grid; -- blocked/overlapping placement is rejected; -- current per-expedition limit is 12 towers. - -This is the first step toward local walls, traps, camps, turrets, repair/logistics and resource structures. - -## Action combat - -- WASD movement; -- auto attack; -- dash with invulnerability; -- Rally and Shockwave; -- Field Watchtower building on B; -- Command-limited army followers; -- biome-specific enemy rosters; -- melee / rush / tank / ranged enemy behaviors; -- hostile projectiles; -- enemy Elites; -- Momentum kill chains; -- harvest nodes; -- 12 stackable Field Doctrines; -- Warden talent hooks; -- unit evolution behavior; -- Wild Bond behavior; -- regional equipment set bonuses; -- Frontier Mutation rules; -- impact feedback / floating text / shake; -- browser-minded entity caps; -- player/allies/enemies/resources/buildings now begin using real atlas sprites instead of only programmer primitives. - -## Warband progression - -### Founding families -- Militia -- Archer -- War Wolf -- Mage - -Each has Rank progression and Rank-3 permanent evolution choices: -- Vanguard / Shieldwall -- Ranger / Longbow -- Dire Wolf / Pack Alpha -- Stormcaller / Lifebinder - -### Wild Bonds -- Ridgeback -- Thornkin -- Stone Golem -- Mire Leech -- Ember Imp -- Frost Wisp - -Wild Bonds are regional discoveries. Named regional bosses guarantee an undiscovered local bond. Monster Hunt / Elite-heavy territory improves ordinary discovery odds. - -## Individual unit quality - -Army families now have a persistent individual-unit layer in `player.unit_roster`. - -Old saves migrate existing troops into Standard individual records. New recruits can roll a prefix and/or rare Elite classification. - -Current prefix chance: **42%**. - -Prefixes: -- **Swift** - faster cadence / slightly more reach; -- **Ironhide** - stronger frontline damage; -- **Blessed** - attacks restore some Warden HP; -- **Vicious** - stronger execution against wounded enemies; -- **Ancient** - stronger attacks + extended reach; -- **Stormtouched** - attacks can chain damage. - -### Elite -Elite is independent of prefix and fills the emotional/collection role of a shiny-quality unit. - -Current provisional roll: approximately **1 / 96** for a newly generated individual. - -Elite units currently gain combat multipliers and a distinct visual aura/card treatment. A unit may be both prefixed and Elite. - -The Warband screen surfaces individual quality cards using atlas art. - -## Warden / equipment progression - -- Warden XP and levels; -- six permanent Talent branches; -- Command progression; -- procedural equipment power/rarity; -- slot-specific affixes; -- six biome-bound equipment families; -- 54 authored regional item identities; -- 2pc / 4pc set bonuses; -- named boss provenance on qualifying drops; -- equipment comparison; -- Forge upgrades; -- scene-driven Paper Doll foundation. +All player weapons now have a knockback value, even when small. -The Paper Doll scene is structurally ready for true modular body/armor layers, but final production quality still requires the isolated approved body and modular armor pieces as RGBA/Spine-ready assets rather than relying forever on a cropped combined atlas unit. +When final weapon art is absent, the combat layer can draw deterministic class-shaped placeholders so attack behavior remains readable: sword, axe, spear, dagger, hammer, bow, crossbow, staff and wand. -## Dawnkeep / economy +## Ranged payloads / spells -- draggable visual settlement; -- atlas-driven building art in the current visual pass; -- Town Hall, Lumberyard, Quarry, Farm, Barracks, Forge, Arcane Lab, Market; -- building upgrades; -- six research branches; -- passive production; -- offline production; -- local layout persistence; -- Renown / Frontier Season scaffold; -- NPC Marketplace proof; -- Frontier Contracts; -- First March onboarding. +Ranged weapon behavior is not welded to one projectile. -## Frontier Mutations +Current projectile foundations include: +- Wooden / Iron / Ember / Rime arrows; +- Iron bolt; +- Arcane Bolt; +- Ember Orb; +- Frost Shard; +- Void Orb; +- Holy Spark; +- Thorn Spine; +- Acid Glob; +- hostile Hex Bolt. -- Swarming Brood -- Frenzied Hunt -- Ironhide Territory -- Marked by Elites -- Rich Veins -- Arcane Storm +Projectile data can independently control speed, life, collision radius, pierce, damage multiplier, knockback multiplier, splash radius and future status hooks. -Mutations stack with risk stances and change live combat/reward rules. +Current spell-flavored attacks include Ember Lance, Frost Shard, Void Orb, Sun Spear and Thorn Burst, alongside Staff Cast and Wand Flick. -## Validation status +## Monster depth -The repository uses a hardened Godot 4.7.2 CI because Godot can sometimes print script errors while returning exit code 0. +The normal monster catalog now targets six identities per current biome instead of a tiny shared roster. -A valid green pass requires: -- clean project parse; -- clean headless main-scene smoke; -- clean Web export; -- generated Web artifact; -- no hidden SCRIPT ERROR/autoload failure in captured logs. +Examples: +- Greenlands: Raider, Wolf, Slime, Tusk Boar, Bandit Archer, Meadow Shaman; +- Ancient Forest: Bramble Guard, Thorn Hound, Forest Wisp, Bark Guard, Vine Spitter, Lost Dryad; +- Iron Hills: Stone Golem, Cave Bat, Ironclad Deserter, Quarry Rat, Crystal Shardling, Miner Wraith; +- Mistfen: Mire Leech, Bog Slime, Fen Crawler, Mire Spitter, Swamp Witch, Lantern Wisp; +- Ash Wastes: Ember Imp, Cinder Hound, Ash Raider, Magma Beetle, Ember Cultist, Charred Golem; +- Frostwild: Frostling, Ice Wolf, Snow Wisp, Rime Archer, Crystal Yeti, Frost Seer. -The **Visual Atlas / Giant Local Map / Unit Quality pass passed all strict gates** after the first CI run correctly caught and forced fixes for one invalid Control preset and strict Variant inference issues in the new local-arena code. +Monsters can define behavior, attack, optional projectile, HP/speed/damage multipliers, sprite ID, biome membership and authored drop candidates. -## Architecture direction +Existing named regional boss patterns remain in place. -Current phase: -- Godot client; -- GitHub source + strict CI / Web export; -- local/browser persistence. +## Equipment / loot -Planned online phase: -- Supabase Auth/profile/Postgres/cloud state; -- authoritative server-side economy mutations; -- shared world/profile inspection; -- marketplace ledger/escrow; -- realtime authority only where genuinely needed. +Existing systems remain: +- procedural rarity / power; +- affixes; +- Forge upgrades; +- six regional equipment families; +- 54 regional set identities; +- 2pc / 4pc bonuses; +- boss provenance; +- equipment comparison. + +The new authored item catalog adds explicit weapon identities and gives new content drops a stable `content_id`. A generated weapon can carry its class, attack, projectile, attack speed, knockback and art links through the normal inventory/save flow. + +Inventory detail now exposes weapon behavior and its sprite links for debugging/content production. + +## Warband + +Founding families: +- Militia; +- Archer; +- War Wolf; +- Mage. -The 3-player-per-macro-territory target is a **local-map scale/architecture target**, not a claim that multiplayer networking is implemented today. +Rank-3 evolution branches: +- Vanguard / Shieldwall; +- Ranger / Longbow; +- Dire Wolf / Pack Alpha; +- Stormcaller / Lifebinder. -## Immediate priorities after this pass +Wild Bonds: +- Ridgeback; +- Thornkin; +- Stone Golem; +- Mire Leech; +- Ember Imp; +- Frost Wisp. -1. Interactive 1920×1080 and 1280×720 playtest. -2. Verify/correct atlas crop coordinates visually inside Godot. -3. Continue replacing programmer primitives with atlas/SVG assets. -4. Upload isolated Warden body + modular armor layers for the true paper doll. -5. Expand local construction beyond the first watchtower. -6. Add local-map chunk/content generation so the 192×192 territory becomes worth exploring, not merely large empty coordinates. -7. Public browser preview and Chrome persistence/performance testing. -8. Start authoritative online/account slice only after the revised local loop is fun. +Individual units can carry prefixes Swift / Ironhide / Blessed / Vicious / Ancient / Stormtouched plus an independent rare **Elite** classification. -## Documentation rule +## Visual direction + +Two visual scales intentionally coexist: + +1. **In-world player / army / enemies:** direction has shifted toward very compact, manually editable pixel sprites. Current production target under exploration is roughly a 24×32 logical character inside a 32×40 cell, with minimal side-view movement cycles and left/right mirroring where useful. +2. **Inventory / inspect / prestige surfaces:** the scene-driven paper-doll foundation remains available for richer equipment presentation. + +The provisional atlas and SVG placeholder kit remain implementation bridges, not a requirement that final world sprites share their detail level. + +## Scene-driven UI status -Every material gameplay/design/architecture change updates this ledger and, when relevant, `MILESTONES.md`, `CHANGELOG.md` or a focused implementation/audit note. +Converted to `.tscn` foundations: +- `scenes/ui/MainShell.tscn`; +- `scenes/ui/ExpeditionLayout.tscn`; +- `scenes/ui/ExpeditionResult.tscn`; +- `scenes/ui/PaperDoll.tscn`; +- `scenes/ui/ContentEditor.tscn`. + +Still significantly code-driven: +- outer Inventory layout; +- World Map shell; +- Dawnkeep custom canvas; +- Warband cards/layout; +- Contracts / Marketplace. + +Full visual audit: `docs/VISUAL_UI_AUDIT.md`. + +## Browser / validation + +Native design target: **1920×1080**. Minimum practical target: **1280×720**. + +CI is intentionally strict because Godot may report script errors while returning exit code 0. A green pass requires: +- project parse; +- headless Main smoke; +- Web export; +- Web artifact; +- no hidden script/autoload failure in captured logs. + +The first Depth Content CI correctly rejected loose Variant inference. The strongly typed rewrite then passed **parse + smoke + Web export**. + +## Important remaining near-term work + +- first hands-on balance/fun pass of the current large-map build; +- public browser preview URL / Chrome persistence and focus test; +- replace provisional in-world art with the newly chosen compact pixel-character language; +- expand in-expedition building beyond the first Watchtower; +- add more reusable spell/attack mechanics after the current primitives are proven fun; +- complete final inventory/item/world sprite asset mapping; +- sound/music/deeper feedback; +- Supabase/auth/cloud/authoritative economy phase; +- shared 2–3 player local-territory prototype later, after the solo loop is strong. diff --git a/docs/MILESTONES.md b/docs/MILESTONES.md index eec44b7..322c999 100644 --- a/docs/MILESTONES.md +++ b/docs/MILESTONES.md @@ -63,17 +63,19 @@ - [x] Expedition HUD moved into editable `.tscn` - [x] Expedition result moved into editable `.tscn` - [x] Paper-doll composition moved into editable `.tscn` foundation +- [x] Main global shell/navigation moved into editable `.tscn` +- [x] Content Lab moved into editable `.tscn` - [x] Visual/UI audit documented with scene-first rules -- [ ] Final approved isolated body-base RGBA asset in project -- [ ] Final isolated modular armor layers / Spine paper-doll rig -- [ ] First complete production modular armor set -- [ ] Weapon set +- [x] First real SVG placeholder pipeline proof imported and Web-exported in Godot +- [ ] Final compact pixel body/base sheet for in-world player and army +- [ ] Final side-view run cycle and left/right mirroring convention +- [ ] First production modular pixel armor/equipment overlay set +- [ ] Final weapon sprite set - [ ] Final resource icon set - [ ] Final settlement visual kit assets - [ ] Final World Map tile language - [ ] Final combat sprite language - [ ] Full Inventory shell converted to `.tscn` -- [ ] Main global shell converted to `.tscn` - [ ] Warband card components converted to reusable `.tscn` ## M1 — I Want One More Expedition @@ -123,6 +125,7 @@ - [x] Enemy spawning follows current local camera/player area instead of giant-map borders - [x] First actual in-expedition construction: Field Watchtower built from harvested Wood - [x] Field Watchtowers snap to local grid and automatically fight enemies +- [ ] Expand expedition building kit: walls / gates / traps / camp / logistics - [x] Macro territory data exposes future target capacity of 3 Wardens / players - [x] City placement/building scene instead of management cards only - [x] Buildings selectable/upgradable directly from settlement scene @@ -134,6 +137,32 @@ - [x] First March persists in the main save and disappears after completion - [x] First March final reward introduces regional set gear +### M1.4 Data-driven content depth +- [x] `ContentDB` autoload with shipped JSON catalogs and browser-safe local overrides +- [x] Separate catalogs for Items / Monsters / Attacks / Projectiles / Tiles +- [x] First reusable attack database with 20 definitions +- [x] First projectile database with 13 payloads +- [x] First authored content-item catalog with 26 weapons/armor definitions +- [x] First expanded normal-monster catalog with 36 identities across six biomes +- [x] First local-ground catalog with 24 biome variants +- [x] Weapon data separates inventory sprite / equipped sheet / attack sprite / projectile +- [x] Weapon attack speed, damage multiplier and knockback are data-driven +- [x] Sword/axe arc collision attacks +- [x] Spear/dagger thrust corridor attacks +- [x] Hammer radial slam attacks +- [x] Bow/crossbow/staff/wand ranged payload attacks +- [x] Different arrows/bolts/spells can be assigned independently from the ranged weapon +- [x] Data-driven monster biome membership, behavior, attack and drop candidates +- [x] New content drops preserve `content_id` through normal inventory/save flow +- [x] Content Lab browse/edit/reset/validate UI +- [x] Content Lab runtime overrides persist to `user://` +- [x] Inventory exposes weapon behavior and visual-asset links for content debugging +- [x] Deterministic fallback weapon-motion visuals while final pixel weapon art is absent +- [ ] Add reusable chain / orbit / beam / delayed-area spell primitives after first combat playtest +- [ ] Add status-effect runtime (slow / burn / poison / stun / armor break) rather than metadata only +- [ ] Add building definitions to Content Lab once multiple field structures exist +- [ ] Add visual sprite-picker UI rather than JSON-only asset IDs + ### Historical validation gates - [x] M1 Fun Pass 1 Godot 4.7.2 parse / smoke / Web export - [x] Gear Affix Pass Godot 4.7.2 parse / smoke / Web export @@ -146,39 +175,39 @@ - [x] First March Pass Godot 4.7.2 parse / smoke / Web export - [x] Frontier Mutations Pass strict Godot 4.7.2 parse / smoke / Web export - [x] Wild Bonds Pass strict Godot 4.7.2 parse / smoke / Web export -- [x] Visual Atlas / Giant Local Map / Unit Quality Pass strict parse validation -- [x] Visual Atlas / Giant Local Map / Unit Quality Pass strict headless smoke validation -- [x] Visual Atlas / Giant Local Map / Unit Quality Pass strict Web export validation +- [x] Visual Atlas / Giant Local Map / Unit Quality Pass strict parse / smoke / Web export +- [x] Real SVG Placeholder Proof strict parse / smoke / Web export +- [x] Data-driven Depth Content Pass strict parse / smoke / Web export ## M2 — Persistent Online Kingdom -- Supabase Auth -- profile -- cloud save -- authoritative settlement mutations -- world state stored online -- marketplace server transactions -- inspect another profile -- anti-dupe transaction ledger -- leaderboards -- basic telemetry/economy balancing +- [ ] Supabase Auth +- [ ] profile +- [ ] cloud save +- [ ] authoritative settlement mutations +- [ ] world state stored online +- [ ] marketplace server transactions +- [ ] inspect another profile +- [ ] anti-dupe transaction ledger +- [ ] leaderboards +- [ ] basic telemetry/economy balancing ## M3 — The World Has Other People -- map ownership / influence -- 2–3 player shared local-territory prototype -- local-territory authority / synchronization -- caravans and regional trade -- optional PvP frontier rules -- asynchronous conflict where appropriate -- social/guild foundations -- world events -- cooperative boss prototype +- [ ] map ownership / influence +- [ ] 2–3 player shared local-territory prototype +- [ ] local-territory authority / synchronization +- [ ] caravans and regional trade +- [ ] optional PvP frontier rules +- [ ] asynchronous conflict where appropriate +- [ ] social/guild foundations +- [ ] world events +- [ ] cooperative boss prototype ## M4 — Endless Frontier -- procedural frontier expansion -- seasons/prestige tuned -- specialization trees -- rare regions -- faction content -- rare/high-tier Wild Bonds and creature evolution paths -- world economy sinks -- long-term live content architecture +- [ ] procedural frontier expansion +- [ ] seasons/prestige tuned +- [ ] specialization trees +- [ ] rare regions +- [ ] faction content +- [ ] rare/high-tier Wild Bonds and creature evolution paths +- [ ] world economy sinks +- [ ] long-term live content architecture diff --git a/project.godot b/project.godot index 00473ec..68b3b8c 100644 --- a/project.godot +++ b/project.godot @@ -12,6 +12,7 @@ config/features=PackedStringArray("4.7", "GL Compatibility") GameState="*res://scripts/core/GameState.gd" SaveManager="*res://scripts/core/SaveManager.gd" ContractManager="*res://scripts/core/ContractManager.gd" +ContentDB="*res://scripts/core/ContentDB.gd" [display] window/size/viewport_width=1920 diff --git a/scenes/ui/ContentEditor.tscn b/scenes/ui/ContentEditor.tscn new file mode 100644 index 0000000..8736875 --- /dev/null +++ b/scenes/ui/ContentEditor.tscn @@ -0,0 +1,182 @@ +[gd_scene load_steps=4 format=3] + +[ext_resource type="Script" path="res://scripts/screens/ContentEditorScreen.gd" id="1"] + +[sub_resource type="StyleBoxFlat" id="Panel"] +bg_color = Color(0.055, 0.071, 0.11, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.18, 0.23, 0.34, 1) +corner_radius_top_left = 10 +corner_radius_top_right = 10 +corner_radius_bottom_right = 10 +corner_radius_bottom_left = 10 + +[sub_resource type="StyleBoxFlat" id="Editor"] +bg_color = Color(0.035, 0.045, 0.071, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.22, 0.29, 0.43, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[node name="ContentEditor" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") + +[node name="Margin" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 18.0 +offset_top = 14.0 +offset_right = -18.0 +offset_bottom = -14.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Root" type="VBoxContainer" parent="Margin"] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="Header" type="HBoxContainer" parent="Margin/Root"] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="Title" type="Label" parent="Margin/Root/Header"] +layout_mode = 2 +theme_override_colors/font_color = Color(0.95, 0.86, 0.68, 1) +theme_override_font_sizes/font_size = 24 +text = "CONTENT LAB" + +[node name="Subtitle" type="Label" parent="Margin/Root/Header"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(0.6, 0.67, 0.79, 1) +theme_override_font_sizes/font_size = 11 +text = "Items · monsters · attacks · projectiles · tiles. Runtime overrides save to user:// and do not mutate shipped content." +vertical_alignment = 1 + +[node name="Category" type="OptionButton" parent="Margin/Root/Header"] +custom_minimum_size = Vector2(180, 40) +layout_mode = 2 + +[node name="Body" type="HSplitContainer" parent="Margin/Root"] +layout_mode = 2 +size_flags_vertical = 3 +split_offset = 320 + +[node name="Left" type="PanelContainer" parent="Margin/Root/Body"] +custom_minimum_size = Vector2(300, 0) +layout_mode = 2 +theme_override_styles/panel = SubResource("Panel") + +[node name="List" type="ItemList" parent="Margin/Root/Body/Left"] +layout_mode = 2 +select_mode = 0 +allow_reselect = true + +[node name="Right" type="HSplitContainer" parent="Margin/Root/Body"] +layout_mode = 2 +size_flags_horizontal = 3 +split_offset = 620 + +[node name="EditorPanel" type="PanelContainer" parent="Margin/Root/Body/Right"] +layout_mode = 2 +theme_override_styles/panel = SubResource("Panel") + +[node name="EditorMargin" type="MarginContainer" parent="Margin/Root/Body/Right/EditorPanel"] +layout_mode = 2 +theme_override_constants/margin_left = 12 +theme_override_constants/margin_right = 12 +theme_override_constants/margin_top = 12 +theme_override_constants/margin_bottom = 12 + +[node name="Column" type="VBoxContainer" parent="Margin/Root/Body/Right/EditorPanel/EditorMargin"] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="EntryTitle" type="Label" parent="Margin/Root/Body/Right/EditorPanel/EditorMargin/Column"] +layout_mode = 2 +theme_override_colors/font_color = Color(0.9, 0.8, 0.6, 1) +theme_override_font_sizes/font_size = 18 +text = "Select an entry" + +[node name="Json" type="TextEdit" parent="Margin/Root/Body/Right/EditorPanel/EditorMargin/Column"] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_font_sizes/font_size = 12 +theme_override_styles/normal = SubResource("Editor") +wrap_mode = 1 + +[node name="Buttons" type="HBoxContainer" parent="Margin/Root/Body/Right/EditorPanel/EditorMargin/Column"] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="Save" type="Button" parent="Margin/Root/Body/Right/EditorPanel/EditorMargin/Column/Buttons"] +custom_minimum_size = Vector2(160, 40) +layout_mode = 2 +text = "SAVE OVERRIDE" + +[node name="DeleteOverride" type="Button" parent="Margin/Root/Body/Right/EditorPanel/EditorMargin/Column/Buttons"] +custom_minimum_size = Vector2(170, 40) +layout_mode = 2 +text = "RESET ENTRY" + +[node name="Validate" type="Button" parent="Margin/Root/Body/Right/EditorPanel/EditorMargin/Column/Buttons"] +custom_minimum_size = Vector2(150, 40) +layout_mode = 2 +text = "VALIDATE LINKS" + +[node name="InfoPanel" type="PanelContainer" parent="Margin/Root/Body/Right"] +custom_minimum_size = Vector2(310, 0) +layout_mode = 2 +theme_override_styles/panel = SubResource("Panel") + +[node name="InfoMargin" type="MarginContainer" parent="Margin/Root/Body/Right/InfoPanel"] +layout_mode = 2 +theme_override_constants/margin_left = 12 +theme_override_constants/margin_right = 12 +theme_override_constants/margin_top = 12 +theme_override_constants/margin_bottom = 12 + +[node name="Column" type="VBoxContainer" parent="Margin/Root/Body/Right/InfoPanel/InfoMargin"] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="PreviewTitle" type="Label" parent="Margin/Root/Body/Right/InfoPanel/InfoMargin/Column"] +layout_mode = 2 +theme_override_colors/font_color = Color(0.78, 0.84, 0.95, 1) +text = "VISUAL / RELATIONSHIPS" + +[node name="Preview" type="TextureRect" parent="Margin/Root/Body/Right/InfoPanel/InfoMargin/Column"] +custom_minimum_size = Vector2(220, 220) +layout_mode = 2 +expand_mode = 1 +stretch_mode = 5 + +[node name="Summary" type="Label" parent="Margin/Root/Body/Right/InfoPanel/InfoMargin/Column"] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_colors/font_color = Color(0.7, 0.76, 0.87, 1) +theme_override_font_sizes/font_size = 11 +text = "" +autowrap_mode = 2 + +[node name="Footer" type="Label" parent="Margin/Root"] +layout_mode = 2 +theme_override_colors/font_color = Color(0.63, 0.72, 0.65, 1) +theme_override_font_sizes/font_size = 11 +text = "Ready" diff --git a/scenes/ui/MainShell.tscn b/scenes/ui/MainShell.tscn index 9747027..a0acc51 100644 --- a/scenes/ui/MainShell.tscn +++ b/scenes/ui/MainShell.tscn @@ -167,47 +167,54 @@ alignment = 1 theme_override_constants/separation = 8 [node name="City" type="Button" parent="Root/NavPanel/Nav"] -custom_minimum_size = Vector2(140, 46) +custom_minimum_size = Vector2(132, 46) layout_mode = 2 theme_override_styles/normal = SubResource("NavButton") theme_override_styles/hover = SubResource("NavHover") text = "SETTLEMENT" [node name="World" type="Button" parent="Root/NavPanel/Nav"] -custom_minimum_size = Vector2(140, 46) +custom_minimum_size = Vector2(132, 46) layout_mode = 2 theme_override_styles/normal = SubResource("NavButton") theme_override_styles/hover = SubResource("NavHover") text = "WORLD MAP" [node name="Army" type="Button" parent="Root/NavPanel/Nav"] -custom_minimum_size = Vector2(140, 46) +custom_minimum_size = Vector2(132, 46) layout_mode = 2 theme_override_styles/normal = SubResource("NavButton") theme_override_styles/hover = SubResource("NavHover") text = "WAR-BAND" [node name="Inventory" type="Button" parent="Root/NavPanel/Nav"] -custom_minimum_size = Vector2(140, 46) +custom_minimum_size = Vector2(132, 46) layout_mode = 2 theme_override_styles/normal = SubResource("NavButton") theme_override_styles/hover = SubResource("NavHover") text = "INVENTORY" [node name="Contracts" type="Button" parent="Root/NavPanel/Nav"] -custom_minimum_size = Vector2(140, 46) +custom_minimum_size = Vector2(132, 46) layout_mode = 2 theme_override_styles/normal = SubResource("NavButton") theme_override_styles/hover = SubResource("NavHover") text = "CONTRACTS" [node name="Market" type="Button" parent="Root/NavPanel/Nav"] -custom_minimum_size = Vector2(140, 46) +custom_minimum_size = Vector2(132, 46) layout_mode = 2 theme_override_styles/normal = SubResource("NavButton") theme_override_styles/hover = SubResource("NavHover") text = "MARKETPLACE" +[node name="Content" type="Button" parent="Root/NavPanel/Nav"] +custom_minimum_size = Vector2(132, 46) +layout_mode = 2 +theme_override_styles/normal = SubResource("NavButton") +theme_override_styles/hover = SubResource("NavHover") +text = "CONTENT LAB" + [node name="Toast" type="Label" parent="."] layout_mode = 1 anchors_preset = 10 diff --git a/scripts/MainVisual.gd b/scripts/MainVisual.gd index c5ffc6f..2ebc31a 100644 --- a/scripts/MainVisual.gd +++ b/scripts/MainVisual.gd @@ -10,6 +10,7 @@ func _ready() -> void: screen_scripts["army"] = preload("res://scripts/screens/ArmyScreenVisual.gd") screen_scripts["inventory"] = preload("res://scripts/screens/InventoryScreenVisual.gd") screen_scripts["expedition"] = preload("res://scripts/screens/ExpeditionScreenVisual.gd") + screen_scripts["content"] = preload("res://scripts/screens/ContentLabScreen.gd") super._ready() func _build_shell() -> void: @@ -37,7 +38,8 @@ func _build_shell() -> void: "Army":"army", "Inventory":"inventory", "Contracts":"contracts", - "Market":"market" + "Market":"market", + "Content":"content" } for button_name in routes: var button:Button=nav.get_node(String(button_name)) diff --git a/scripts/combat/DepthCombatArena.gd b/scripts/combat/DepthCombatArena.gd new file mode 100644 index 0000000..36e17c9 --- /dev/null +++ b/scripts/combat/DepthCombatArena.gd @@ -0,0 +1,420 @@ +class_name DepthCombatArena +extends QualityCombatArena + +var weapon_profile:Dictionary = {} +var attack_profile:Dictionary = {} +var attack_visual_time:float = 0.0 +var attack_visual_duration:float = 0.0 +var attack_visual_dir:Vector2 = Vector2.RIGHT +var attack_visual_motion:String = "" +var drop_candidates:Array[String] = [] + +func begin(data:Dictionary) -> void: + drop_candidates.clear() + _resolve_equipped_weapon() + super.begin(data) + +func _process(delta:float) -> void: + attack_visual_time = maxf(0.0, attack_visual_time-delta) + super._process(delta) + +func _resolve_equipped_weapon() -> void: + var equipped_item:Dictionary = GameState.get_item(String(GameState.equipped.get("weapon",""))) + var content_id:String = String(equipped_item.get("content_id","")) + if content_id == "": + var lower:String = String(equipped_item.get("name","")).to_lower() + if "bow" in lower: + content_id = "hunter_bow" + elif "staff" in lower: + content_id = "apprentice_staff" + elif "spear" in lower or "pike" in lower: + content_id = "militia_spear" + elif "axe" in lower: + content_id = "woodsman_axe" + elif "dagger" in lower: + content_id = "mire_dagger" + elif "maul" in lower or "hammer" in lower: + content_id = "quarry_maul" + else: + content_id = "training_sword" + weapon_profile = ContentDB.weapon(content_id) + if weapon_profile.is_empty(): + weapon_profile = ContentDB.weapon("training_sword") + attack_profile = ContentDB.attack(String(weapon_profile.get("attack_id","slash_quick"))) + +func _biome_roster() -> Array: + var roster:Array[String] = ContentDB.monsters_for_biome(String(tile.get("biome","Greenlands"))) + if roster.is_empty(): + return super._biome_roster() + return roster + +func _enemy_behavior(type:String) -> String: + var definition:Dictionary = ContentDB.monster(type) + if definition.is_empty(): + return super._enemy_behavior(type) + return String(definition.get("behavior","melee")) + +func _spawn_enemy(boss:bool) -> void: + var before:int = enemies.size() + super._spawn_enemy(boss) + if boss or enemies.size() <= before: + return + var index:int = enemies.size()-1 + var enemy:Dictionary = enemies[index] + var definition:Dictionary = ContentDB.monster(String(enemy.get("type",""))) + if definition.is_empty(): + return + var new_hp:float = float(enemy.get("hp",25.0))*float(definition.get("hp_mult",1.0)) + enemy["hp"] = new_hp + enemy["max_hp"] = new_hp + enemy["speed"] = float(enemy.get("speed",60.0))*float(definition.get("speed_mult",1.0)) + enemy["damage_mult"] = float(enemy.get("damage_mult",1.0))*float(definition.get("damage_mult",1.0)) + enemy["behavior"] = String(definition.get("behavior",enemy.get("behavior","melee"))) + enemy["attack_id"] = String(definition.get("attack_id","enemy_claw")) + enemy["projectile_id"] = String(definition.get("projectile_id","")) + enemy["sprite_id"] = String(definition.get("sprite_id","")) + enemies[index] = enemy + +func _update_enemies(delta:float) -> void: + for i:int in range(enemies.size()-1,-1,-1): + var enemy:Dictionary = enemies[i] + enemy["flash"] = maxf(0.0,float(enemy.get("flash",0.0))-delta) + enemy["attack_cd"] = maxf(0.0,float(enemy.get("attack_cd",0.0))-delta) + enemy["shoot_cd"] = maxf(0.0,float(enemy.get("shoot_cd",0.0))-delta) + enemy["special_cd"] = maxf(0.0,float(enemy.get("special_cd",0.0))-delta) + enemy["charge_time"] = maxf(0.0,float(enemy.get("charge_time",0.0))-delta) + if float(enemy.get("hp",0.0)) <= 0.0: + _kill_enemy(enemy) + enemies.remove_at(i) + continue + + var enemy_pos:Vector2 = Vector2(enemy.get("pos",Vector2.ZERO)) + var distance:float = enemy_pos.distance_to(player_pos) + var direction:Vector2 = (player_pos-enemy_pos).normalized() + var behavior:String = String(enemy.get("behavior","melee")) + if behavior.begins_with("boss_"): + match behavior: + "boss_beast": + _update_beast_boss(enemy,direction,delta) + "boss_oracle": + _update_oracle_boss(enemy,direction,distance,delta) + "boss_colossus": + _update_colossus_boss(enemy,direction,delta) + _: + enemy["pos"] = enemy_pos + direction*float(enemy.get("speed",42.0))*delta + if float(enemy.get("shoot_cd",0.0)) <= 0.0: + _spawn_boss_volley(Vector2(enemy.get("pos",enemy_pos))) + enemy["shoot_cd"] = 2.8 + else: + match behavior: + "ranged": + if distance > 250.0: + enemy["pos"] = enemy_pos + direction*float(enemy.get("speed",60.0))*delta + elif distance < 145.0: + enemy["pos"] = enemy_pos - direction*float(enemy.get("speed",60.0))*0.65*delta + else: + enemy["pos"] = enemy_pos + direction.rotated(PI/2.0)*float(enemy.get("speed",60.0))*0.32*delta + if float(enemy.get("shoot_cd",0.0)) <= 0.0 and distance < 390.0: + _spawn_data_enemy_projectile(enemy,direction) + enemy["shoot_cd"] = _enemy_attack_cooldown(enemy) + "tank": + enemy["pos"] = enemy_pos + direction*float(enemy.get("speed",50.0))*delta + "rush": + enemy["pos"] = enemy_pos + direction*float(enemy.get("speed",70.0))*1.08*delta + _: + enemy["pos"] = enemy_pos + direction*float(enemy.get("speed",60.0))*delta + + if distance < float(enemy.get("radius",14.0))+17.0 and float(enemy.get("attack_cd",0.0)) <= 0.0: + var attack:Dictionary = ContentDB.attack(String(enemy.get("attack_id","enemy_claw"))) + var contact_damage:float = (7.0+float(int(tile.get("threat",1)))*1.7)*float(enemy.get("damage_mult",1.0))*float(attack.get("damage_mult",1.0)) + _damage_player(contact_damage) + enemy["attack_cd"] = maxf(0.36,float(attack.get("duration",0.75))) + enemies[i] = enemy + +func _enemy_attack_cooldown(enemy:Dictionary) -> float: + var attack:Dictionary = ContentDB.attack(String(enemy.get("attack_id","enemy_bolt"))) + return maxf(0.55,float(attack.get("duration",1.1))+0.45) + +func _spawn_data_enemy_projectile(enemy:Dictionary,direction:Vector2) -> void: + var projectile_id:String = String(enemy.get("projectile_id","")) + if projectile_id == "": + var attack:Dictionary = ContentDB.attack(String(enemy.get("attack_id","enemy_bolt"))) + projectile_id = String(attack.get("default_projectile","enemy_hex")) + var definition:Dictionary = ContentDB.projectile(projectile_id) + var projectile_color:Color = Color(String(definition.get("color","#e89975"))) + var shot_damage:float = (7.0+float(int(tile.get("threat",1)))*1.25)*float(enemy.get("damage_mult",1.0))*float(definition.get("damage_mult",1.0)) + _spawn_enemy_shot(Vector2(enemy.get("pos",Vector2.ZERO)),direction,shot_damage,bool(enemy.get("elite",false)),projectile_color,float(definition.get("speed",270.0))) + +func _auto_attack() -> void: + if attack_timer > 0.0 or enemies.is_empty() or projectiles.size() > 240: + return + if weapon_profile.is_empty() or attack_profile.is_empty(): + _resolve_equipped_weapon() + var target:Dictionary = nearest_enemy(player_pos) + if target.is_empty(): + return + var direction:Vector2 = (Vector2(target.get("pos",player_pos))-player_pos).normalized() + attack_visual_dir = direction + attack_visual_motion = String(attack_profile.get("motion","slash")) + attack_visual_duration = float(attack_profile.get("duration",0.28)) + attack_visual_time = attack_visual_duration + var speed_mult:float = maxf(0.15,float(weapon_profile.get("attack_speed",1.0))) + attack_timer = maxf(0.10,attack_interval/speed_mult) + var momentum:float = 1.0+float(mini(combo,30))*0.012 + var is_crit:bool = rng.randf() < crit_chance + var crit_factor:float = crit_mult if is_crit else 1.0 + var hit_damage:float = damage*momentum*float(weapon_profile.get("damage_mult",1.0))*float(attack_profile.get("damage_mult",1.0))*crit_factor + var knockback_amount:float = float(weapon_profile.get("knockback",0.6))*float(attack_profile.get("knockback_mult",1.0)) + match String(attack_profile.get("mode","melee_arc")): + "melee_arc": + _perform_arc_attack(direction,hit_damage,knockback_amount,is_crit) + "melee_thrust": + _perform_thrust_attack(direction,hit_damage,knockback_amount,is_crit) + "melee_slam": + _perform_slam_attack(direction,hit_damage,knockback_amount,is_crit) + _: + _perform_ranged_attack(direction,hit_damage,knockback_amount,is_crit) + +func _perform_arc_attack(direction:Vector2,hit_damage:float,knockback_amount:float,is_crit:bool) -> void: + var reach:float = float(attack_profile.get("reach",72.0)) + var half_arc:float = deg_to_rad(float(attack_profile.get("arc_degrees",100.0))*0.5) + for enemy:Dictionary in enemies: + var delta_pos:Vector2 = Vector2(enemy.get("pos",Vector2.ZERO))-player_pos + if delta_pos.length() > reach+float(enemy.get("radius",14.0)): + continue + if delta_pos.length_squared() > 0.1 and absf(direction.angle_to(delta_pos.normalized())) > half_arc: + continue + _hit_with_knockback(enemy,hit_damage,direction,knockback_amount,is_crit) + +func _perform_thrust_attack(direction:Vector2,hit_damage:float,knockback_amount:float,is_crit:bool) -> void: + var reach:float = float(attack_profile.get("reach",105.0)) + var width:float = float(attack_profile.get("width",20.0)) + for enemy:Dictionary in enemies: + var delta_pos:Vector2 = Vector2(enemy.get("pos",Vector2.ZERO))-player_pos + var forward:float = delta_pos.dot(direction) + if forward < 0.0 or forward > reach+float(enemy.get("radius",14.0)): + continue + var perpendicular:float = absf(delta_pos.cross(direction)) + if perpendicular > width+float(enemy.get("radius",14.0)): + continue + _hit_with_knockback(enemy,hit_damage,direction,knockback_amount,is_crit) + +func _perform_slam_attack(direction:Vector2,hit_damage:float,knockback_amount:float,is_crit:bool) -> void: + var center:Vector2 = player_pos+direction*float(attack_profile.get("reach",58.0))*0.55 + var radius:float = float(attack_profile.get("radius",52.0)) + _spawn_ring(center,Color("#e8c387")) + for enemy:Dictionary in enemies: + var enemy_pos:Vector2 = Vector2(enemy.get("pos",Vector2.ZERO)) + if enemy_pos.distance_to(center) <= radius+float(enemy.get("radius",14.0)): + var push_dir:Vector2 = (enemy_pos-center).normalized() + if push_dir.length_squared() < 0.1: + push_dir = direction + _hit_with_knockback(enemy,hit_damage,push_dir,knockback_amount,is_crit) + _shake(0.12,4.0) + +func _hit_with_knockback(enemy:Dictionary,hit_damage:float,direction:Vector2,knockback_amount:float,is_crit:bool) -> void: + _damage_enemy(enemy,hit_damage,is_crit) + _arc_from(enemy,hit_damage) + enemy["pos"] = Vector2(enemy.get("pos",Vector2.ZERO))+direction.normalized()*knockback_amount*18.0 + +func _perform_ranged_attack(direction:Vector2,hit_damage:float,knockback_amount:float,is_crit:bool) -> void: + var projectile_id:String = String(weapon_profile.get("projectile_id","")) + if projectile_id == "": + projectile_id = String(attack_profile.get("default_projectile","arcane_bolt")) + var definition:Dictionary = ContentDB.projectile(projectile_id) + for i:int in range(projectile_count): + var spread:float = (float(i)-float(projectile_count-1)/2.0)*0.13 + var shot_dir:Vector2 = direction.rotated(spread) + projectiles.append({ + "pos":player_pos+shot_dir*18.0, + "vel":shot_dir*float(definition.get("speed",430.0)), + "damage":hit_damage*float(definition.get("damage_mult",1.0)), + "life":float(definition.get("life",1.8)), + "crit":is_crit, + "radius":float(definition.get("radius",5.0)), + "pierce":int(definition.get("pierce",0)), + "splash_radius":float(definition.get("splash_radius",0.0)), + "slow":float(definition.get("slow",0.0)), + "knockback":knockback_amount*float(definition.get("knockback_mult",1.0)), + "visual_id":String(definition.get("visual_id","fx_arrow")), + "color":String(definition.get("color","#bfe4ff")) + }) + +func _update_projectiles(delta:float) -> void: + for i:int in range(projectiles.size()-1,-1,-1): + var projectile:Dictionary = projectiles[i] + projectile["pos"] = Vector2(projectile.get("pos",Vector2.ZERO))+Vector2(projectile.get("vel",Vector2.ZERO))*delta + projectile["life"] = float(projectile.get("life",0.0))-delta + var remove_projectile:bool = false + for enemy:Dictionary in enemies: + if Vector2(projectile.get("pos",Vector2.ZERO)).distance_to(Vector2(enemy.get("pos",Vector2.ZERO))) < float(enemy.get("radius",14.0))+float(projectile.get("radius",5.0)): + var direction:Vector2 = Vector2(projectile.get("vel",Vector2.RIGHT)).normalized() + _hit_with_knockback(enemy,float(projectile.get("damage",0.0)),direction,float(projectile.get("knockback",0.4)),bool(projectile.get("crit",false))) + var splash:float = float(projectile.get("splash_radius",0.0)) + if splash > 0.0: + for other:Dictionary in enemies: + if other == enemy: + continue + if Vector2(other.get("pos",Vector2.ZERO)).distance_to(Vector2(enemy.get("pos",Vector2.ZERO))) <= splash: + _damage_enemy(other,float(projectile.get("damage",0.0))*0.45,false) + var pierce:int = int(projectile.get("pierce",0)) + if pierce > 0: + projectile["pierce"] = pierce-1 + projectile["pos"] = Vector2(projectile.get("pos",Vector2.ZERO))+direction*12.0 + else: + remove_projectile = true + break + if remove_projectile or float(projectile.get("life",0.0)) <= 0.0 or not bounds.grow(30.0).has_point(Vector2(projectile.get("pos",Vector2.ZERO))): + projectiles.remove_at(i) + else: + projectiles[i] = projectile + +func _kill_enemy(enemy:Dictionary) -> void: + var enemy_type:String = String(enemy.get("type","")) + if not bool(enemy.get("boss",false)): + var drops:Array = ContentDB.drops_for_monster(enemy_type) + if not drops.is_empty() and rng.randf() < 0.13: + var drop_id:String = String(drops[rng.randi_range(0,drops.size()-1)]) + if drop_id not in drop_candidates: + drop_candidates.append(drop_id) + super._kill_enemy(enemy) + +func generate_loot_item(threat:int) -> Dictionary: + var candidates:Array[String] = drop_candidates.duplicate() + if candidates.is_empty(): + var biome:String = String(tile.get("biome","Greenlands")) + for id:String in ContentDB.ids("items"): + var definition:Dictionary = ContentDB.get_entry("items",id) + if biome in Array(definition.get("biomes",[])): + candidates.append(id) + if candidates.is_empty(): + return super.generate_loot_item(threat) + var content_id:String = candidates[rng.randi_range(0,candidates.size()-1)] + var definition:Dictionary = ContentDB.get_entry("items",content_id) + var slot:String = String(definition.get("slot","weapon")) + var tier:int = int(definition.get("tier",1)) + var rarity_names:Array[String] = ["common","uncommon","rare","epic","legendary"] + var rarity:String = rarity_names[clampi(tier-1,0,4)] + var power:int = int(definition.get("power",5))+threat*2 + var item:Dictionary = GameState.make_item(String(definition.get("name",content_id)),slot,rarity,power,{}) + item["content_id"] = content_id + for key:String in ["weapon_class","attack_id","projectile_id","attack_speed","damage_mult","knockback","inventory_sprite","equipped_sheet","attack_sprite"]: + if definition.has(key): + item[key] = definition[key] + var pool:Array = _affix_pool(slot).duplicate() + var affix_count:int = mini(_affix_count(rarity),pool.size()) + var bonuses:Dictionary = {} + var affixes:Array = [] + for _n:int in range(affix_count): + var pick:int = rng.randi_range(0,pool.size()-1) + var affix_key:String = String(pool[pick]) + pool.remove_at(pick) + var affix:Dictionary = _roll_affix(affix_key,threat,rarity) + affixes.append(affix) + bonuses[affix_key] = float(bonuses.get(affix_key,0.0))+float(affix.get("value",0.0)) + item["affixes"] = affixes + item["bonuses"] = bonuses + return item + +func _draw_ground() -> void: + super._draw_ground() + var biome:String = String(tile.get("biome","Greenlands")) + var variants:Array[Dictionary] = [] + for id:String in ContentDB.ids("tiles"): + var definition:Dictionary = ContentDB.tile_def(id) + if String(definition.get("biome","")) == biome: + variants.append(definition) + if variants.is_empty(): + return + var visible:Rect2 = _visible_world_rect(80.0) + var x0:int = int(floor(visible.position.x/float(LOCAL_TILE_PX)))*LOCAL_TILE_PX + var x1:int = int(ceil(visible.end.x/float(LOCAL_TILE_PX)))*LOCAL_TILE_PX + var y0:int = int(floor(visible.position.y/float(LOCAL_TILE_PX)))*LOCAL_TILE_PX + var y1:int = int(ceil(visible.end.y/float(LOCAL_TILE_PX)))*LOCAL_TILE_PX + for x:int in range(x0,x1+1,LOCAL_TILE_PX): + for y:int in range(y0,y1+1,LOCAL_TILE_PX): + var cell_x:int = x/LOCAL_TILE_PX + var cell_y:int = y/LOCAL_TILE_PX + var hashed:int = absi(hash("%s:%d:%d"%[String(tile.get("seed",1)),cell_x,cell_y])) + var definition:Dictionary = variants[hashed%variants.size()] + var accent:Color = Color(String(definition.get("accent","#ffffff"))) + if hashed%4 == 0: + var px:float = float(x+12+(hashed%37)) + var py:float = float(y+13+((hashed/7)%35)) + draw_circle(Vector2(px,py),2.5,Color(accent,0.34)) + +func _draw_enemy(enemy:Dictionary) -> void: + var sprite_id:String = String(enemy.get("sprite_id","")) + if sprite_id == "" or not VisualAtlas.has(sprite_id): + super._draw_enemy(enemy) + return + var size:Vector2 = Vector2(54,60) + if bool(enemy.get("elite",false)): + size *= 1.16 + _draw_atlas(sprite_id,Vector2(enemy.get("pos",Vector2.ZERO)),size,true) + if bool(enemy.get("elite",false)): + draw_arc(Vector2(enemy.get("pos",Vector2.ZERO)),float(enemy.get("radius",14.0))+12.0,0.0,TAU,24,Color("#d9a3ff"),3.0) + var hp_ratio:float = clampf(float(enemy.get("hp",1.0))/maxf(1.0,float(enemy.get("max_hp",1.0))),0.0,1.0) + if bool(enemy.get("elite",false)): + var pos:Vector2 = Vector2(enemy.get("pos",Vector2.ZERO)) + draw_rect(Rect2(pos+Vector2(-32,-48),Vector2(64,5)),Color("#241d28")) + draw_rect(Rect2(pos+Vector2(-32,-48),Vector2(64*hp_ratio,5)),Color("#d85c6c")) + +func _draw_player() -> void: + super._draw_player() + if attack_visual_time <= 0.0: + return + var progress:float = 1.0-attack_visual_time/maxf(0.01,attack_visual_duration) + var weapon_class:String = String(weapon_profile.get("weapon_class","sword")) + _draw_weapon_motion(weapon_class,progress) + +func _draw_weapon_motion(weapon_class:String,progress:float) -> void: + var base_angle:float = attack_visual_dir.angle() + var motion:String = attack_visual_motion + var angle:float = base_angle + var reach:float = 56.0 + if motion == "slash": + angle += lerpf(-1.05,1.05,progress) + elif motion == "slam": + angle += lerpf(-1.15,0.35,progress) + elif motion == "thrust": + reach = 45.0+sin(progress*PI)*36.0 + elif motion in ["draw","recoil","cast","flick"]: + reach = 44.0 + draw_set_transform(player_pos,angle,Vector2.ONE) + match weapon_class: + "spear": + draw_line(Vector2(14,0),Vector2(reach+40.0,0),Color("#9a7648"),5.0) + draw_colored_polygon(PackedVector2Array([Vector2(reach+40,-7),Vector2(reach+55,0),Vector2(reach+40,7)]),Color("#d8e0ea")) + "staff","wand": + draw_line(Vector2(10,0),Vector2(reach+38.0,0),Color("#725042"),6.0) + draw_circle(Vector2(reach+42.0,0),8.0,Color("#78c7ff")) + "bow": + draw_arc(Vector2(40,0),24.0,-1.25,1.25,16,Color("#9a744a"),5.0) + draw_line(Vector2(47,-23),Vector2(47,23),Color("#e6d8bd"),2.0) + "crossbow": + draw_line(Vector2(14,0),Vector2(68,0),Color("#8a6949"),6.0) + draw_arc(Vector2(50,0),20.0,-1.1,1.1,12,Color("#a07850"),4.0) + "hammer": + draw_line(Vector2(12,0),Vector2(reach+24,0),Color("#79533d"),7.0) + draw_rect(Rect2(Vector2(reach+18,-14),Vector2(28,28)),Color("#89919b")) + "axe": + draw_line(Vector2(12,0),Vector2(reach+22,0),Color("#79533d"),6.0) + draw_colored_polygon(PackedVector2Array([Vector2(reach+14,-20),Vector2(reach+42,-12),Vector2(reach+42,12),Vector2(reach+14,20)]),Color("#aeb8c4")) + "dagger": + draw_line(Vector2(12,0),Vector2(reach+12,0),Color("#dce3ed"),6.0) + _: + draw_line(Vector2(12,0),Vector2(reach+28,0),Color("#dce3ed"),7.0) + draw_line(Vector2(reach+8,-9),Vector2(reach+8,9),Color("#b08a55"),4.0) + draw_set_transform(Vector2.ZERO,0.0,Vector2.ONE) + +func _draw() -> void: + super._draw() + for projectile:Dictionary in projectiles: + var visual_id:String = String(projectile.get("visual_id","")) + if visual_id == "" or not VisualAtlas.has(visual_id): + continue + var angle:float = Vector2(projectile.get("vel",Vector2.RIGHT)).angle() + draw_set_transform(Vector2(projectile.get("pos",Vector2.ZERO)),angle,Vector2.ONE) + draw_texture_rect_region(VisualAtlas.ATLAS,Rect2(Vector2(-15,-7),Vector2(30,14)),VisualAtlas.region(visual_id)) + draw_set_transform(Vector2.ZERO,0.0,Vector2.ONE) diff --git a/scripts/core/ContentDB.gd b/scripts/core/ContentDB.gd new file mode 100644 index 0000000..47110e7 --- /dev/null +++ b/scripts/core/ContentDB.gd @@ -0,0 +1,140 @@ +extends Node + +signal content_changed(category:String,id:String) + +const FILES:Dictionary={ + "attacks":"res://data/content/attacks.json", + "projectiles":"res://data/content/projectiles.json", + "items":"res://data/content/items.json", + "monsters":"res://data/content/monsters.json", + "tiles":"res://data/content/tiles.json" +} +const OVERRIDE_PATH:="user://marchbound_content_overrides.json" + +var catalogs:Dictionary={} +var overrides:Dictionary={} + +func _ready()->void: + reload_all() + +func reload_all()->void: + catalogs.clear() + for category in FILES: + catalogs[category]=_load_json(String(FILES[category])) + overrides=_load_json(OVERRIDE_PATH) if FileAccess.file_exists(OVERRIDE_PATH) else {} + _apply_overrides() + +func _load_json(path:String)->Dictionary: + if not FileAccess.file_exists(path): + push_warning("ContentDB missing %s"%path) + return {} + var file:=FileAccess.open(path,FileAccess.READ) + if file==null: + return {} + var parsed:Variant=JSON.parse_string(file.get_as_text()) + if parsed is Dictionary: + return Dictionary(parsed) + push_error("ContentDB invalid JSON: %s"%path) + return {} + +func _apply_overrides()->void: + for category in overrides: + if not catalogs.has(category) or not overrides[category] is Dictionary: + continue + for id in overrides[category]: + catalogs[category][id]=Dictionary(overrides[category][id]).duplicate(true) + +func get_entry(category:String,id:String)->Dictionary: + if not catalogs.has(category): + return {} + var raw:Variant=Dictionary(catalogs[category]).get(id,{}) + return Dictionary(raw).duplicate(true) if raw is Dictionary else {} + +func all(category:String)->Dictionary: + var raw:Variant=catalogs.get(category,{}) + return Dictionary(raw).duplicate(true) if raw is Dictionary else {} + +func ids(category:String)->Array[String]: + var result:Array[String]=[] + if not catalogs.has(category): + return result + for id in Dictionary(catalogs[category]).keys(): + result.append(String(id)) + result.sort() + return result + +func update_entry(category:String,id:String,data:Dictionary,persist:=true)->void: + if not catalogs.has(category): + catalogs[category]={} + catalogs[category][id]=data.duplicate(true) + if persist: + if not overrides.has(category): + overrides[category]={} + overrides[category][id]=data.duplicate(true) + _save_overrides() + content_changed.emit(category,id) + +func delete_override(category:String,id:String)->void: + if overrides.has(category) and overrides[category] is Dictionary: + overrides[category].erase(id) + _save_overrides() + reload_all() + content_changed.emit(category,id) + +func reset_all_overrides()->void: + overrides.clear() + if FileAccess.file_exists(OVERRIDE_PATH): + DirAccess.remove_absolute(ProjectSettings.globalize_path(OVERRIDE_PATH)) + reload_all() + content_changed.emit("*","*") + +func _save_overrides()->void: + var file:=FileAccess.open(OVERRIDE_PATH,FileAccess.WRITE) + if file: + file.store_string(JSON.stringify(overrides," ")) + +func weapon(id:String)->Dictionary: + var d:=get_entry("items",id) + return d if String(d.get("kind",""))=="weapon" else {} + +func attack(id:String)->Dictionary: + return get_entry("attacks",id) + +func projectile(id:String)->Dictionary: + return get_entry("projectiles",id) + +func monster(id:String)->Dictionary: + return get_entry("monsters",id) + +func tile_def(id:String)->Dictionary: + return get_entry("tiles",id) + +func monsters_for_biome(biome:String)->Array[String]: + var result:Array[String]=[] + for id in ids("monsters"): + var d:=monster(id) + if biome in Array(d.get("biomes",[])): + result.append(id) + return result + +func drops_for_monster(id:String)->Array: + return Array(monster(id).get("drops",[])).duplicate(true) + +func validate_references()->Array[String]: + var errors:Array[String]=[] + for id in ids("items"): + var item:=get_entry("items",id) + if String(item.get("kind",""))!="weapon": + continue + var attack_id:=String(item.get("attack_id","")) + if attack_id!="" and attack(attack_id).is_empty(): + errors.append("item %s -> missing attack %s"%[id,attack_id]) + var projectile_id:=String(item.get("projectile_id","")) + if projectile_id!="" and projectile(projectile_id).is_empty(): + errors.append("item %s -> missing projectile %s"%[id,projectile_id]) + for id in ids("monsters"): + var monster_data:=monster(id) + var attack_id:=String(monster_data.get("attack_id","")) + if attack_id!="" and attack(attack_id).is_empty(): + errors.append("monster %s -> missing attack %s"%[id,attack_id]) + return errors diff --git a/scripts/screens/ContentEditorScreen.gd b/scripts/screens/ContentEditorScreen.gd new file mode 100644 index 0000000..c6f9e2d --- /dev/null +++ b/scripts/screens/ContentEditorScreen.gd @@ -0,0 +1,139 @@ +extends Control + +const CATEGORIES:Array[String] = ["items","monsters","attacks","projectiles","tiles"] + +@onready var category:OptionButton = $Margin/Root/Header/Category +@onready var list:ItemList = $Margin/Root/Body/Left/List +@onready var entry_title:Label = $Margin/Root/Body/Right/EditorPanel/EditorMargin/Column/EntryTitle +@onready var json_editor:TextEdit = $Margin/Root/Body/Right/EditorPanel/EditorMargin/Column/Json +@onready var save_button:Button = $Margin/Root/Body/Right/EditorPanel/EditorMargin/Column/Buttons/Save +@onready var reset_button:Button = $Margin/Root/Body/Right/EditorPanel/EditorMargin/Column/Buttons/DeleteOverride +@onready var validate_button:Button = $Margin/Root/Body/Right/EditorPanel/EditorMargin/Column/Buttons/Validate +@onready var preview:TextureRect = $Margin/Root/Body/Right/InfoPanel/InfoMargin/Column/Preview +@onready var summary:Label = $Margin/Root/Body/Right/InfoPanel/InfoMargin/Column/Summary +@onready var footer:Label = $Margin/Root/Footer + +var current_category:String = "items" +var current_id:String = "" + +func _ready() -> void: + for category_name:String in CATEGORIES: + category.add_item(category_name.capitalize()) + category.item_selected.connect(_category_changed) + list.item_selected.connect(_entry_selected) + save_button.pressed.connect(_save) + reset_button.pressed.connect(_reset_entry) + validate_button.pressed.connect(_validate) + ContentDB.content_changed.connect(_content_changed) + _refresh_list() + +func _category_changed(index:int) -> void: + current_category = CATEGORIES[index] + current_id = "" + _refresh_list() + +func _refresh_list() -> void: + list.clear() + for id:String in ContentDB.ids(current_category): + var definition:Dictionary = ContentDB.get_entry(current_category,id) + list.add_item("%s\n%s"%[String(definition.get("name",id)),id]) + list.set_item_metadata(list.item_count-1,id) + entry_title.text = current_category.to_upper() + json_editor.text = "" + preview.texture = null + summary.text = "%d shipped/runtime entries"%list.item_count + +func _entry_selected(index:int) -> void: + current_id = String(list.get_item_metadata(index)) + _show_current() + +func _show_current() -> void: + if current_id == "": + return + var definition:Dictionary = ContentDB.get_entry(current_category,current_id) + entry_title.text = "%s · %s"%[String(definition.get("name",current_id)),current_id] + json_editor.text = JSON.stringify(definition," ") + _update_preview(definition) + _update_summary(definition) + +func _update_preview(definition:Dictionary) -> void: + preview.texture = null + var candidates:Array[String] = [] + for key:String in ["sprite_id","visual_id","inventory_sprite","attack_sprite"]: + var value:String = String(definition.get(key,"")) + if value != "": + candidates.append(value) + for id:String in candidates: + if VisualAtlas.has(id): + preview.texture = VisualAtlas.texture(id) + return + +func _update_summary(definition:Dictionary) -> void: + var lines:Array[String] = [] + match current_category: + "items": + lines.append("Kind: %s · Slot: %s · Tier %d"%[String(definition.get("kind","?")),String(definition.get("slot","?")),int(definition.get("tier",1))]) + if String(definition.get("kind","")) == "weapon": + lines.append("Class: %s"%String(definition.get("weapon_class","?"))) + lines.append("Attack: %s"%String(definition.get("attack_id","?"))) + lines.append("Projectile: %s"%String(definition.get("projectile_id","none"))) + lines.append("Speed %.2f · Damage ×%.2f · Knockback %.2f"%[float(definition.get("attack_speed",1.0)),float(definition.get("damage_mult",1.0)),float(definition.get("knockback",0.0))]) + lines.append("Biomes: "+_join_values(Array(definition.get("biomes",[])))) + "monsters": + lines.append("Behavior: %s"%String(definition.get("behavior","?"))) + lines.append("Attack: %s"%String(definition.get("attack_id","?"))) + lines.append("Projectile: %s"%String(definition.get("projectile_id","none"))) + lines.append("Biomes: "+_join_values(Array(definition.get("biomes",[])))) + lines.append("Drops: "+_join_values(Array(definition.get("drops",[])))) + "attacks": + lines.append("Mode: %s · Motion: %s"%[String(definition.get("mode","?")),String(definition.get("motion","?"))]) + lines.append("Duration %.2f · Damage ×%.2f · Knockback ×%.2f"%[float(definition.get("duration",0.0)),float(definition.get("damage_mult",1.0)),float(definition.get("knockback_mult",1.0))]) + "projectiles": + lines.append("Speed %d · Radius %.1f · Pierce %d"%[int(definition.get("speed",0)),float(definition.get("radius",0.0)),int(definition.get("pierce",0))]) + lines.append("Damage ×%.2f · Knockback ×%.2f"%[float(definition.get("damage_mult",1.0)),float(definition.get("knockback_mult",1.0))]) + "tiles": + lines.append("Biome: %s · Kind: %s"%[String(definition.get("biome","?")),String(definition.get("kind","?"))]) + lines.append("Tags: "+_join_values(Array(definition.get("tags",[])))) + summary.text = "\n".join(lines) + +func _join_values(values:Array) -> String: + var strings:Array[String] = [] + for value:Variant in values: + strings.append(String(value)) + return ", ".join(strings) + +func _save() -> void: + if current_id == "": + return + var parsed:Variant = JSON.parse_string(json_editor.text) + if not parsed is Dictionary: + footer.text = "Invalid JSON · nothing saved" + footer.modulate = Color("#e39191") + return + ContentDB.update_entry(current_category,current_id,Dictionary(parsed),true) + footer.text = "Saved runtime override to user:// for %s/%s"%[current_category,current_id] + footer.modulate = Color("#8dd5a6") + _show_current() + +func _reset_entry() -> void: + if current_id == "": + return + ContentDB.delete_override(current_category,current_id) + footer.text = "Entry restored to shipped definition" + footer.modulate = Color("#d5c28d") + _show_current() + +func _validate() -> void: + var errors:Array[String] = ContentDB.validate_references() + if errors.is_empty(): + footer.text = "All item/monster attack and projectile links are valid" + footer.modulate = Color("#8dd5a6") + else: + footer.text = "Reference errors: "+" | ".join(errors) + footer.modulate = Color("#e39191") + +func _content_changed(category_name:String,id:String) -> void: + if category_name == "*" or category_name == current_category: + _refresh_list() + if id == current_id: + _show_current() diff --git a/scripts/screens/ContentLabScreen.gd b/scripts/screens/ContentLabScreen.gd new file mode 100644 index 0000000..1dd9b8e --- /dev/null +++ b/scripts/screens/ContentLabScreen.gd @@ -0,0 +1,8 @@ +extends Control + +const CONTENT_EDITOR_SCENE:=preload("res://scenes/ui/ContentEditor.tscn") + +func _ready()->void: + var editor:=CONTENT_EDITOR_SCENE.instantiate() + editor.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + add_child(editor) diff --git a/scripts/screens/ExpeditionScreenVisual.gd b/scripts/screens/ExpeditionScreenVisual.gd index cb3dde5..526e490 100644 --- a/scripts/screens/ExpeditionScreenVisual.gd +++ b/scripts/screens/ExpeditionScreenVisual.gd @@ -10,7 +10,7 @@ func _build() -> void: add_child(layout_scene) var holder:Control = layout_scene.get_node("Margin/MainRow/ArenaPanel/ArenaHolder") - arena = QualityCombatArena.new() + arena = DepthCombatArena.new() holder.add_child(arena) arena.set_view_size(Vector2(1400,840)) arena.hud_changed.connect(_hud) @@ -121,6 +121,8 @@ func _finished(result:Dictionary) -> void: var item:Dictionary=result.get("item",{}) if not item.is_empty(): loot_lines.append("\n[color=#9fc6f1][b]GEAR DROP · %s · %s[/b][/color]" % [String(item.get("name","Unknown")),String(item.get("rarity","common")).to_upper()]) + if item.has("weapon_class"): + loot_lines.append("[color=#9ab2d4]%s · %s · Knockback %.1f[/color]"%[String(item.get("weapon_class","weapon")).capitalize(),String(item.get("attack_id","attack")).replace("_"," ").capitalize(),float(item.get("knockback",0.0))]) for affix in item.get("affixes",[]): loot_lines.append("[color=#b9c9ed]◆ %s[/color]" % String(affix.get("text",""))) loot.text="\n".join(loot_lines) diff --git a/scripts/screens/InventoryScreenVisual.gd b/scripts/screens/InventoryScreenVisual.gd index 4c5b07f..31d6ee1 100644 --- a/scripts/screens/InventoryScreenVisual.gd +++ b/scripts/screens/InventoryScreenVisual.gd @@ -29,7 +29,7 @@ func _build()->void: var left_v=VBoxContainer.new() left_margin.add_child(left_v) left_v.add_child(UIFactory.title("Inventory",24)) - var intro=UIFactory.label("Regional equipment families make where an item came from matter. Mix affixes, sets and raw power instead of chasing one number.",12,Color("#92a0bf")) + var intro=UIFactory.label("Regional equipment families make where an item came from matter. Weapons now also define their attack motion, projectile and knockback.",12,Color("#92a0bf")) intro.autowrap_mode=TextServer.AUTOWRAP_WORD_SMART left_v.add_child(intro) left_v.add_child(UIFactory.hsep()) @@ -71,7 +71,7 @@ func _build()->void: doll.size_flags_horizontal=Control.SIZE_EXPAND_FILL doll.size_flags_vertical=Control.SIZE_EXPAND_FILL mid.add_child(doll) - var hint=UIFactory.label("Paper-doll composition lives in scenes/ui/PaperDoll.tscn. Body, slots and future armor layers can be repositioned visually in Godot.",10,Color("#7f8ca9")) + var hint=UIFactory.label("Paper-doll composition lives in scenes/ui/PaperDoll.tscn. World combat sprites remain deliberately simpler than this prestige/inspect surface.",10,Color("#7f8ca9")) hint.autowrap_mode=TextServer.AUTOWRAP_WORD_SMART hint.horizontal_alignment=HORIZONTAL_ALIGNMENT_CENTER mid.add_child(hint) @@ -99,6 +99,24 @@ func _build()->void: func _refresh_detail()->void: super._refresh_detail() + var item:=GameState.get_item(selected_uid) + if not item.is_empty() and String(item.get("slot",""))=="weapon": + detail.add_child(UIFactory.hsep()) + detail.add_child(UIFactory.label("WEAPON BEHAVIOR",15,Color("#f0dfae"))) + var content_id:=String(item.get("content_id","training_sword")) + var definition:=ContentDB.weapon(content_id) + if definition.is_empty(): definition=ContentDB.weapon("training_sword") + var attack:=ContentDB.attack(String(definition.get("attack_id","slash_quick"))) + var projectile_id:=String(definition.get("projectile_id",attack.get("default_projectile",""))) + var projectile:=ContentDB.projectile(projectile_id) if projectile_id!="" else {} + var behavior=UIFactory.label("%s · %s\nAttack speed ×%.2f · Damage ×%.2f · Knockback %.2f"%[String(definition.get("weapon_class","sword")).capitalize(),String(attack.get("name","Attack")),float(definition.get("attack_speed",1.0)),float(definition.get("damage_mult",1.0)),float(definition.get("knockback",0.0))],12,Color("#adc4e7")) + behavior.autowrap_mode=TextServer.AUTOWRAP_WORD_SMART + detail.add_child(behavior) + if not projectile.is_empty(): + detail.add_child(UIFactory.label("Projectile · %s · speed %d · pierce %d"%[String(projectile.get("name",projectile_id)),int(projectile.get("speed",0)),int(projectile.get("pierce",0))],11,Color("#9ccab8"))) + var links=UIFactory.label("Inventory sprite: %s\nEquipped sheet: %s\nAttack sprite: %s"%[String(definition.get("inventory_sprite","unset")),String(definition.get("equipped_sheet","unset")),String(definition.get("attack_sprite","unset"))],10,Color("#7f8ca9")) + links.autowrap_mode=TextServer.AUTOWRAP_WORD_SMART + detail.add_child(links) _make_column_wrap(detail) func _make_column_wrap(container:Node)->void: