From a989b8f19bf6563a82082e78c22785dbf61fde9d Mon Sep 17 00:00:00 2001 From: shenlei Date: Tue, 22 Sep 2026 17:51:52 +0900 Subject: [PATCH] chore: delete 106 unreachable *_system.gd and their parity tests These files were loaded only by their own test_*_parity.gd and never by the runtime (transitively checked, incl. client_phase_integration_system). Removes 76 manifest implementation/evidence references across 18 contracts; contract docs get a note that conclusions based on them are void. Live systems kept: chest, consumable, item_attr, metin_socket, whisper_chat. Full headless suite: no new failures (7 failing + 5 headless-hanging render tests are identical before the deletion). Co-Authored-By: Claude Opus 5 --- audit/contracts/audio.bgm-ambience-3d.md | 4 + audit/contracts/combat.affect-status.md | 4 + audit/contracts/combat.attack-combo-damage.md | 4 + .../combat.target-selection-rules.md | 4 + audit/contracts/function-inventory.md | 4 + audit/contracts/item.consumable-action.md | 4 + audit/contracts/item.refine-socket-costume.md | 4 + audit/contracts/movement.keyboard-motion.md | 4 + audit/contracts/npc.dialog-shop.md | 4 + .../contracts/quest.quest-event-navigation.md | 4 + audit/contracts/skill.player-ui-cooldown.md | 4 + audit/contracts/social.chat-text-tail.md | 4 + audit/contracts/social.exchange-safebox.md | 4 + .../contracts/social.party-guild-messenger.md | 4 + audit/contracts/world.dungeon-activity.md | 4 + audit/contracts/world.environment-weather.md | 4 + audit/contracts/world.fishing-mining.md | 4 + audit/contracts/world.mount-pet-polymorph.md | 4 + audit/contracts/world.npc-drop-actor-spawn.md | 4 + audit/history.jsonl | 1 + audit/manifest.json | 198 +++----- audit/remediation-roadmap.md | 2 +- audit/reports/coverage.md | 8 +- project/alignment_pk_system.gd | 187 -------- project/alignment_pk_system.gd.uid | 1 - project/alignment_system.gd | 128 ------ project/alignment_system.gd.uid | 1 - project/atlas_navigation_system.gd | 132 ------ project/atlas_navigation_system.gd.uid | 1 - project/attach_metin_dialog_system.gd | 144 ------ project/attach_metin_dialog_system.gd.uid | 1 - project/auto_combat_bot_system.gd | 175 ------- project/auto_combat_bot_system.gd.uid | 1 - project/battle_arena_duel_system.gd | 218 --------- project/battle_arena_duel_system.gd.uid | 1 - project/belt_system.gd | 161 ------- project/belt_system.gd.uid | 1 - project/bgm_jukebox_system.gd | 101 ---- project/bgm_jukebox_system.gd.uid | 1 - .../biologist_chaegirab_research_system.gd | 270 ----------- ...biologist_chaegirab_research_system.gd.uid | 1 - project/biologist_quest_system.gd | 263 ----------- project/biologist_quest_system.gd.uid | 1 - project/boss_summon_scroll_system.gd | 312 ------------- project/boss_summon_scroll_system.gd.uid | 1 - project/chat_hyperlink_dice_system.gd | 147 ------ project/chat_hyperlink_dice_system.gd.uid | 1 - project/class_specialization_system.gd | 233 ---------- project/class_specialization_system.gd.uid | 1 - project/classic_pet_system.gd | 203 -------- project/classic_pet_system.gd.uid | 1 - project/client_phase_integration_system.gd | 192 -------- .../client_phase_integration_system.gd.uid | 1 - project/combat_replay_snapshot_system.gd | 170 ------- project/combat_replay_snapshot_system.gd.uid | 1 - project/combat_status_effect_system.gd | 249 ---------- project/combat_status_effect_system.gd.uid | 1 - project/compass_treasure_hunt_system.gd | 192 -------- project/compass_treasure_hunt_system.gd.uid | 1 - project/costume_attr_transfer_system.gd | 244 ---------- project/costume_attr_transfer_system.gd.uid | 1 - project/costume_system.gd | 126 ----- project/costume_system.gd.uid | 1 - project/cube_crafting_system.gd | 293 ------------ project/cube_crafting_system.gd.uid | 1 - project/cursed_monolith_spire_system.gd | 196 -------- project/cursed_monolith_spire_system.gd.uid | 1 - project/demon_tower_system.gd | 225 --------- project/demon_tower_system.gd.uid | 1 - project/devils_catacomb_system.gd | 326 ------------- project/devils_catacomb_system.gd.uid | 1 - project/dragon_lair_system.gd | 257 ----------- project/dragon_lair_system.gd.uid | 1 - project/dragon_soul_system.gd | 215 --------- project/dragon_soul_system.gd.uid | 1 - project/dungeon_ranking_system.gd | 133 ------ project/dungeon_ranking_system.gd.uid | 1 - project/dynamic_bounty_event_system.gd | 163 ------- project/dynamic_bounty_event_system.gd.uid | 1 - project/emotion_action_system.gd | 99 ---- project/emotion_action_system.gd.uid | 1 - project/empire_kingdom_system.gd | 213 --------- project/empire_kingdom_system.gd.uid | 1 - project/endless_spire_dungeon_system.gd | 194 -------- project/endless_spire_dungeon_system.gd.uid | 1 - project/energy_system.gd | 237 ---------- project/energy_system.gd.uid | 1 - project/epic_refine_system.gd | 210 --------- project/epic_refine_system.gd.uid | 1 - project/equipment_inspect_system.gd | 134 ------ project/equipment_inspect_system.gd.uid | 1 - project/event_manager_system.gd | 171 ------- project/event_manager_system.gd.uid | 1 - project/fish_clam_roast_system.gd | 432 ------------------ project/fish_clam_roast_system.gd.uid | 1 - project/fishing_cooking_camp_system.gd | 214 --------- project/fishing_cooking_camp_system.gd.uid | 1 - project/fishing_mining_system.gd | 213 --------- project/fishing_mining_system.gd.uid | 1 - project/fishing_rod_reel_system.gd | 307 ------------- project/fishing_rod_reel_system.gd.uid | 1 - project/flame_dungeon_razador_system.gd | 196 -------- project/flame_dungeon_razador_system.gd.uid | 1 - project/game_button_money_dialog_system.gd | 127 ----- .../game_button_money_dialog_system.gd.uid | 1 - project/gm_toolkit_system.gd | 207 --------- project/gm_toolkit_system.gd.uid | 1 - project/guild_mark_uploader_system.gd | 156 ------- project/guild_mark_uploader_system.gd.uid | 1 - project/guild_skill_system.gd | 264 ----------- project/guild_skill_system.gd.uid | 1 - project/guild_war_observer_arena_system.gd | 188 -------- .../guild_war_observer_arena_system.gd.uid | 1 - project/guild_war_system.gd | 186 -------- project/guild_war_system.gd.uid | 1 - project/hair_dye_system.gd | 105 ----- project/hair_dye_system.gd.uid | 1 - project/herbal_dew_system.gd | 228 --------- project/herbal_dew_system.gd.uid | 1 - project/horse_combat_system.gd | 207 --------- project/horse_combat_system.gd.uid | 1 - project/horse_level_growth_system.gd | 283 ------------ project/horse_level_growth_system.gd.uid | 1 - project/horse_system.gd | 138 ------ project/horse_system.gd.uid | 1 - project/inventory_expansion_system.gd | 164 ------- project/inventory_expansion_system.gd.uid | 1 - project/inventory_sort_system.gd | 163 ------- project/inventory_sort_system.gd.uid | 1 - project/item_enchant_system.gd | 276 ----------- project/item_enchant_system.gd.uid | 1 - project/item_split_system.gd | 143 ------ project/item_split_system.gd.uid | 1 - project/jewelry_socket_system.gd | 175 ------- project/jewelry_socket_system.gd.uid | 1 - project/lucky_lottery_fortune_system.gd | 277 ----------- project/lucky_lottery_fortune_system.gd.uid | 1 - project/mail_system.gd | 213 --------- project/mail_system.gd.uid | 1 - project/mall_affect_system.gd | 194 -------- project/mall_affect_system.gd.uid | 1 - project/map_name_shower_system.gd | 168 ------- project/map_name_shower_system.gd.uid | 1 - project/marriage_system.gd | 111 ----- project/marriage_system.gd.uid | 1 - project/marriage_wedding_love_system.gd | 177 ------- project/marriage_wedding_love_system.gd.uid | 1 - project/mercenary_companion_system.gd | 279 ----------- project/mercenary_companion_system.gd.uid | 1 - project/messenger_system.gd | 169 ------- project/messenger_system.gd.uid | 1 - project/metin_spawn_event_system.gd | 174 ------- project/metin_spawn_event_system.gd.uid | 1 - project/mining_refinery_smelt_system.gd | 192 -------- project/mining_refinery_smelt_system.gd.uid | 1 - project/mob_combat_reaction_system.gd | 72 --- project/mob_combat_reaction_system.gd.uid | 1 - project/monarch_election_ui_system.gd | 230 ---------- project/monarch_election_ui_system.gd.uid | 1 - project/monarch_empire_treasury_system.gd | 294 ------------ project/monarch_empire_treasury_system.gd.uid | 1 - project/monster_card_system.gd | 167 ------- project/monster_card_system.gd.uid | 1 - project/mounted_combat_skill_system.gd | 224 --------- project/mounted_combat_skill_system.gd.uid | 1 - .../mysterious_merchant_blackmarket_system.gd | 259 ----------- ...terious_merchant_blackmarket_system.gd.uid | 1 - project/npc_shop_system.gd | 187 -------- project/npc_shop_system.gd.uid | 1 - project/offline_bazaar_market_system.gd | 311 ------------- project/offline_bazaar_market_system.gd.uid | 1 - project/party_management_system.gd | 216 --------- project/party_management_system.gd.uid | 1 - project/pet_companion_system.gd | 153 ------- project/pet_companion_system.gd.uid | 1 - project/pk_penalty_system.gd | 158 ------- project/pk_penalty_system.gd.uid | 1 - project/player_trade_exchange_system.gd | 343 -------------- project/player_trade_exchange_system.gd.uid | 1 - project/point_reset_system.gd | 130 ------ project/point_reset_system.gd.uid | 1 - project/polymorph_marble_system.gd | 200 -------- project/polymorph_marble_system.gd.uid | 1 - project/polymorph_system.gd | 149 ------ project/polymorph_system.gd.uid | 1 - project/portable_service_system.gd | 145 ------ project/portable_service_system.gd.uid | 1 - project/private_shop_system.gd | 88 ---- project/private_shop_system.gd.uid | 1 - project/privilege_empire_guild_system.gd | 179 -------- project/privilege_empire_guild_system.gd.uid | 1 - project/refine_effect_system.gd | 115 ----- project/refine_effect_system.gd.uid | 1 - project/safebox_storage_keeper_system.gd | 243 ---------- project/safebox_storage_keeper_system.gd.uid | 1 - project/save_snapshot_system.gd | 379 --------------- project/save_snapshot_system.gd.uid | 1 - project/security_lock_system.gd | 223 --------- project/security_lock_system.gd.uid | 1 - project/skill_book_reading_system.gd | 213 --------- project/skill_book_reading_system.gd.uid | 1 - project/skill_book_system.gd | 203 -------- project/skill_book_system.gd.uid | 1 - project/socket_glow_fx_system.gd | 123 ----- project/socket_glow_fx_system.gd.uid | 1 - project/solo_guild_domain_system.gd | 190 -------- project/solo_guild_domain_system.gd.uid | 1 - project/soul_stone_grandmaster_system.gd | 178 -------- project/soul_stone_grandmaster_system.gd.uid | 1 - project/soulbound_lock_security_system.gd | 169 ------- project/soulbound_lock_security_system.gd.uid | 1 - project/spider_dungeon_key_system.gd | 238 ---------- project/spider_dungeon_key_system.gd.uid | 1 - project/spider_dungeon_system.gd | 161 ------- project/spider_dungeon_system.gd.uid | 1 - project/status_skill_reset_system.gd | 183 -------- project/status_skill_reset_system.gd.uid | 1 - project/system_setting_system.gd | 147 ------ project/system_setting_system.gd.uid | 1 - project/test_alignment_parity.gd | 102 ----- project/test_alignment_parity.gd.uid | 1 - project/test_alignment_pk_parity.gd | 149 ------ project/test_alignment_pk_parity.gd.uid | 1 - project/test_atlas_navigation_parity.gd | 119 ----- project/test_atlas_navigation_parity.gd.uid | 1 - project/test_attach_metin_parity.gd | 92 ---- project/test_attach_metin_parity.gd.uid | 1 - project/test_auto_combat_bot_parity.gd | 168 ------- project/test_auto_combat_bot_parity.gd.uid | 1 - project/test_battle_arena_duel_parity.gd | 149 ------ project/test_battle_arena_duel_parity.gd.uid | 1 - project/test_belt_system_parity.gd | 113 ----- project/test_belt_system_parity.gd.uid | 1 - project/test_bgm_jukebox_parity.gd | 92 ---- project/test_bgm_jukebox_parity.gd.uid | 1 - ...est_biologist_chaegirab_research_parity.gd | 162 ------- ...biologist_chaegirab_research_parity.gd.uid | 1 - project/test_biologist_quest_parity.gd | 140 ------ project/test_biologist_quest_parity.gd.uid | 1 - project/test_boss_summon_scroll_parity.gd | 200 -------- project/test_boss_summon_scroll_parity.gd.uid | 1 - project/test_chat_hyperlink_dice_parity.gd | 103 ----- .../test_chat_hyperlink_dice_parity.gd.uid | 1 - project/test_class_specialization_parity.gd | 144 ------ .../test_class_specialization_parity.gd.uid | 1 - project/test_classic_pet_parity.gd | 146 ------ project/test_classic_pet_parity.gd.uid | 1 - .../test_client_phase_integration_parity.gd | 123 ----- ...est_client_phase_integration_parity.gd.uid | 1 - project/test_combat_replay_snapshot_parity.gd | 140 ------ .../test_combat_replay_snapshot_parity.gd.uid | 1 - project/test_combat_status_effect_parity.gd | 133 ------ .../test_combat_status_effect_parity.gd.uid | 1 - project/test_compass_treasure_hunt_parity.gd | 145 ------ .../test_compass_treasure_hunt_parity.gd.uid | 1 - project/test_costume_attr_transfer_parity.gd | 136 ------ .../test_costume_attr_transfer_parity.gd.uid | 1 - project/test_costume_system_parity.gd | 146 ------ project/test_costume_system_parity.gd.uid | 1 - project/test_cube_crafting_parity.gd | 158 ------- project/test_cube_crafting_parity.gd.uid | 1 - project/test_cursed_monolith_spire_parity.gd | 157 ------- .../test_cursed_monolith_spire_parity.gd.uid | 1 - project/test_demon_tower_parity.gd | 140 ------ project/test_demon_tower_parity.gd.uid | 1 - project/test_devils_catacomb_parity.gd | 223 --------- project/test_devils_catacomb_parity.gd.uid | 1 - project/test_dragon_lair_parity.gd | 185 -------- project/test_dragon_lair_parity.gd.uid | 1 - project/test_dragon_soul_parity.gd | 122 ----- project/test_dragon_soul_parity.gd.uid | 1 - project/test_dungeon_ranking_parity.gd | 108 ----- project/test_dungeon_ranking_parity.gd.uid | 1 - project/test_dynamic_bounty_event_parity.gd | 136 ------ .../test_dynamic_bounty_event_parity.gd.uid | 1 - project/test_emotion_action_parity.gd | 75 --- project/test_emotion_action_parity.gd.uid | 1 - project/test_empire_kingdom_parity.gd | 146 ------ project/test_empire_kingdom_parity.gd.uid | 1 - project/test_endless_spire_dungeon_parity.gd | 148 ------ .../test_endless_spire_dungeon_parity.gd.uid | 1 - project/test_energy_system_parity.gd | 167 ------- project/test_energy_system_parity.gd.uid | 1 - project/test_epic_refine_parity.gd | 183 -------- project/test_epic_refine_parity.gd.uid | 1 - project/test_equipment_inspect_parity.gd | 116 ----- project/test_equipment_inspect_parity.gd.uid | 1 - project/test_event_manager_parity.gd | 143 ------ project/test_event_manager_parity.gd.uid | 1 - project/test_fish_clam_roast_parity.gd | 207 --------- project/test_fish_clam_roast_parity.gd.uid | 1 - project/test_fishing_cooking_camp_parity.gd | 162 ------- .../test_fishing_cooking_camp_parity.gd.uid | 1 - project/test_fishing_mining_parity.gd | 100 ---- project/test_fishing_mining_parity.gd.uid | 1 - project/test_fishing_rod_reel_parity.gd | 169 ------- project/test_fishing_rod_reel_parity.gd.uid | 1 - project/test_flame_dungeon_razador_parity.gd | 169 ------- .../test_flame_dungeon_razador_parity.gd.uid | 1 - .../test_game_button_money_dialog_parity.gd | 88 ---- ...est_game_button_money_dialog_parity.gd.uid | 1 - project/test_gm_toolkit_parity.gd | 143 ------ project/test_gm_toolkit_parity.gd.uid | 1 - project/test_guild_mark_uploader_parity.gd | 84 ---- .../test_guild_mark_uploader_parity.gd.uid | 1 - project/test_guild_skill_parity.gd | 125 ----- project/test_guild_skill_parity.gd.uid | 1 - .../test_guild_war_observer_arena_parity.gd | 112 ----- ...est_guild_war_observer_arena_parity.gd.uid | 1 - project/test_guild_war_parity.gd | 137 ------ project/test_guild_war_parity.gd.uid | 1 - project/test_hair_dye_parity.gd | 140 ------ project/test_hair_dye_parity.gd.uid | 1 - project/test_herbal_dew_parity.gd | 113 ----- project/test_herbal_dew_parity.gd.uid | 1 - project/test_horse_combat_parity.gd | 160 ------- project/test_horse_combat_parity.gd.uid | 1 - project/test_horse_level_growth_parity.gd | 179 -------- project/test_horse_level_growth_parity.gd.uid | 1 - project/test_inventory_expansion_parity.gd | 147 ------ .../test_inventory_expansion_parity.gd.uid | 1 - project/test_inventory_sort_parity.gd | 119 ----- project/test_inventory_sort_parity.gd.uid | 1 - project/test_item_enchant_manager_parity.gd | 173 ------- .../test_item_enchant_manager_parity.gd.uid | 1 - project/test_item_split_parity.gd | 150 ------ project/test_item_split_parity.gd.uid | 1 - project/test_jewelry_socket_parity.gd | 130 ------ project/test_jewelry_socket_parity.gd.uid | 1 - project/test_lucky_lottery_fortune_parity.gd | 142 ------ .../test_lucky_lottery_fortune_parity.gd.uid | 1 - project/test_mail_system_parity.gd | 179 -------- project/test_mail_system_parity.gd.uid | 1 - project/test_mall_affect_parity.gd | 118 ----- project/test_mall_affect_parity.gd.uid | 1 - project/test_map_name_shower_parity.gd | 121 ----- project/test_map_name_shower_parity.gd.uid | 1 - project/test_marriage_parity.gd | 137 ------ project/test_marriage_parity.gd.uid | 1 - project/test_marriage_wedding_love_parity.gd | 124 ----- .../test_marriage_wedding_love_parity.gd.uid | 1 - project/test_mercenary_companion_parity.gd | 156 ------- .../test_mercenary_companion_parity.gd.uid | 1 - project/test_messenger_parity.gd | 134 ------ project/test_messenger_parity.gd.uid | 1 - project/test_metin_spawn_event_parity.gd | 168 ------- project/test_metin_spawn_event_parity.gd.uid | 1 - project/test_mining_refinery_smelt_parity.gd | 144 ------ .../test_mining_refinery_smelt_parity.gd.uid | 1 - project/test_mob_combat_reaction_parity.gd | 97 ---- .../test_mob_combat_reaction_parity.gd.uid | 1 - project/test_monarch_election_ui_parity.gd | 126 ----- .../test_monarch_election_ui_parity.gd.uid | 1 - .../test_monarch_empire_treasury_parity.gd | 151 ------ ...test_monarch_empire_treasury_parity.gd.uid | 1 - project/test_monster_card_parity.gd | 110 ----- project/test_monster_card_parity.gd.uid | 1 - project/test_mount_horse_parity.gd | 123 ----- project/test_mount_horse_parity.gd.uid | 1 - project/test_mounted_combat_skill_parity.gd | 140 ------ .../test_mounted_combat_skill_parity.gd.uid | 1 - ..._mysterious_merchant_blackmarket_parity.gd | 214 --------- ...terious_merchant_blackmarket_parity.gd.uid | 1 - project/test_npc_shop_parity.gd | 127 ----- project/test_npc_shop_parity.gd.uid | 1 - project/test_offline_bazaar_market_parity.gd | 172 ------- .../test_offline_bazaar_market_parity.gd.uid | 1 - project/test_party_management_parity.gd | 124 ----- project/test_party_management_parity.gd.uid | 1 - project/test_pet_companion_parity.gd | 160 ------- project/test_pet_companion_parity.gd.uid | 1 - project/test_pk_penalty_parity.gd | 122 ----- project/test_pk_penalty_parity.gd.uid | 1 - project/test_player_trade_exchange_parity.gd | 223 --------- .../test_player_trade_exchange_parity.gd.uid | 1 - project/test_point_reset_parity.gd | 99 ---- project/test_point_reset_parity.gd.uid | 1 - project/test_polymorph_marble_parity.gd | 132 ------ project/test_polymorph_marble_parity.gd.uid | 1 - project/test_polymorph_parity.gd | 116 ----- project/test_polymorph_parity.gd.uid | 1 - project/test_portable_service_parity.gd | 92 ---- project/test_portable_service_parity.gd.uid | 1 - project/test_private_shop_bundle_parity.gd | 99 ---- .../test_private_shop_bundle_parity.gd.uid | 1 - project/test_privilege_empire_guild_parity.gd | 120 ----- .../test_privilege_empire_guild_parity.gd.uid | 1 - project/test_refine_effect_parity.gd | 87 ---- project/test_refine_effect_parity.gd.uid | 1 - project/test_safebox_storage_keeper_parity.gd | 165 ------- .../test_safebox_storage_keeper_parity.gd.uid | 1 - project/test_save_snapshot_parity.gd | 248 ---------- project/test_save_snapshot_parity.gd.uid | 1 - project/test_security_lock_parity.gd | 219 --------- project/test_security_lock_parity.gd.uid | 1 - project/test_skill_book_parity.gd | 117 ----- project/test_skill_book_parity.gd.uid | 1 - project/test_skill_book_reading_parity.gd | 166 ------- project/test_skill_book_reading_parity.gd.uid | 1 - project/test_socket_glow_fx_parity.gd | 132 ------ project/test_socket_glow_fx_parity.gd.uid | 1 - project/test_solo_guild_domain_parity.gd | 155 ------- project/test_solo_guild_domain_parity.gd.uid | 1 - project/test_soul_stone_grandmaster_parity.gd | 123 ----- .../test_soul_stone_grandmaster_parity.gd.uid | 1 - .../test_soulbound_lock_security_parity.gd | 138 ------ ...test_soulbound_lock_security_parity.gd.uid | 1 - project/test_spider_dungeon_key_parity.gd | 189 -------- project/test_spider_dungeon_key_parity.gd.uid | 1 - project/test_spider_dungeon_parity.gd | 134 ------ project/test_spider_dungeon_parity.gd.uid | 1 - project/test_status_skill_reset_parity.gd | 167 ------- project/test_status_skill_reset_parity.gd.uid | 1 - project/test_system_setting_parity.gd | 116 ----- project/test_system_setting_parity.gd.uid | 1 - project/test_title_achievement_parity.gd | 121 ----- project/test_title_achievement_parity.gd.uid | 1 - project/test_title_trophy_buff_parity.gd | 140 ------ project/test_title_trophy_buff_parity.gd.uid | 1 - project/test_treasure_box_key_parity.gd | 166 ------- project/test_treasure_box_key_parity.gd.uid | 1 - project/test_wardrobe_transmog_parity.gd | 133 ------ project/test_wardrobe_transmog_parity.gd.uid | 1 - project/test_weather_daynight_parity.gd | 126 ----- project/test_weather_daynight_parity.gd.uid | 1 - project/test_wedding_marriage_parity.gd | 163 ------- project/test_wedding_marriage_parity.gd.uid | 1 - project/test_world_boss_ai_behavior_parity.gd | 177 ------- .../test_world_boss_ai_behavior_parity.gd.uid | 1 - project/test_world_boss_parity.gd | 204 --------- project/test_world_boss_parity.gd.uid | 1 - project/title_achievement_system.gd | 130 ------ project/title_achievement_system.gd.uid | 1 - project/title_trophy_buff_system.gd | 173 ------- project/title_trophy_buff_system.gd.uid | 1 - project/treasure_box_key_system.gd | 248 ---------- project/treasure_box_key_system.gd.uid | 1 - project/wardrobe_transmog_system.gd | 146 ------ project/wardrobe_transmog_system.gd.uid | 1 - project/weather_daynight_system.gd | 168 ------- project/weather_daynight_system.gd.uid | 1 - project/wedding_marriage_system.gd | 168 ------- project/wedding_marriage_system.gd.uid | 1 - project/world_boss_ai_system.gd | 298 ------------ project/world_boss_ai_system.gd.uid | 1 - project/world_boss_system.gd | 309 ------------- project/world_boss_system.gd.uid | 1 - 447 files changed, 151 insertions(+), 36567 deletions(-) delete mode 100644 project/alignment_pk_system.gd delete mode 100644 project/alignment_pk_system.gd.uid delete mode 100644 project/alignment_system.gd delete mode 100644 project/alignment_system.gd.uid delete mode 100644 project/atlas_navigation_system.gd delete mode 100644 project/atlas_navigation_system.gd.uid delete mode 100644 project/attach_metin_dialog_system.gd delete mode 100644 project/attach_metin_dialog_system.gd.uid delete mode 100644 project/auto_combat_bot_system.gd delete mode 100644 project/auto_combat_bot_system.gd.uid delete mode 100644 project/battle_arena_duel_system.gd delete mode 100644 project/battle_arena_duel_system.gd.uid delete mode 100644 project/belt_system.gd delete mode 100644 project/belt_system.gd.uid delete mode 100644 project/bgm_jukebox_system.gd delete mode 100644 project/bgm_jukebox_system.gd.uid delete mode 100644 project/biologist_chaegirab_research_system.gd delete mode 100644 project/biologist_chaegirab_research_system.gd.uid delete mode 100644 project/biologist_quest_system.gd delete mode 100644 project/biologist_quest_system.gd.uid delete mode 100644 project/boss_summon_scroll_system.gd delete mode 100644 project/boss_summon_scroll_system.gd.uid delete mode 100644 project/chat_hyperlink_dice_system.gd delete mode 100644 project/chat_hyperlink_dice_system.gd.uid delete mode 100644 project/class_specialization_system.gd delete mode 100644 project/class_specialization_system.gd.uid delete mode 100644 project/classic_pet_system.gd delete mode 100644 project/classic_pet_system.gd.uid delete mode 100644 project/client_phase_integration_system.gd delete mode 100644 project/client_phase_integration_system.gd.uid delete mode 100644 project/combat_replay_snapshot_system.gd delete mode 100644 project/combat_replay_snapshot_system.gd.uid delete mode 100644 project/combat_status_effect_system.gd delete mode 100644 project/combat_status_effect_system.gd.uid delete mode 100644 project/compass_treasure_hunt_system.gd delete mode 100644 project/compass_treasure_hunt_system.gd.uid delete mode 100644 project/costume_attr_transfer_system.gd delete mode 100644 project/costume_attr_transfer_system.gd.uid delete mode 100644 project/costume_system.gd delete mode 100644 project/costume_system.gd.uid delete mode 100644 project/cube_crafting_system.gd delete mode 100644 project/cube_crafting_system.gd.uid delete mode 100644 project/cursed_monolith_spire_system.gd delete mode 100644 project/cursed_monolith_spire_system.gd.uid delete mode 100644 project/demon_tower_system.gd delete mode 100644 project/demon_tower_system.gd.uid delete mode 100644 project/devils_catacomb_system.gd delete mode 100644 project/devils_catacomb_system.gd.uid delete mode 100644 project/dragon_lair_system.gd delete mode 100644 project/dragon_lair_system.gd.uid delete mode 100644 project/dragon_soul_system.gd delete mode 100644 project/dragon_soul_system.gd.uid delete mode 100644 project/dungeon_ranking_system.gd delete mode 100644 project/dungeon_ranking_system.gd.uid delete mode 100644 project/dynamic_bounty_event_system.gd delete mode 100644 project/dynamic_bounty_event_system.gd.uid delete mode 100644 project/emotion_action_system.gd delete mode 100644 project/emotion_action_system.gd.uid delete mode 100644 project/empire_kingdom_system.gd delete mode 100644 project/empire_kingdom_system.gd.uid delete mode 100644 project/endless_spire_dungeon_system.gd delete mode 100644 project/endless_spire_dungeon_system.gd.uid delete mode 100644 project/energy_system.gd delete mode 100644 project/energy_system.gd.uid delete mode 100644 project/epic_refine_system.gd delete mode 100644 project/epic_refine_system.gd.uid delete mode 100644 project/equipment_inspect_system.gd delete mode 100644 project/equipment_inspect_system.gd.uid delete mode 100644 project/event_manager_system.gd delete mode 100644 project/event_manager_system.gd.uid delete mode 100644 project/fish_clam_roast_system.gd delete mode 100644 project/fish_clam_roast_system.gd.uid delete mode 100644 project/fishing_cooking_camp_system.gd delete mode 100644 project/fishing_cooking_camp_system.gd.uid delete mode 100644 project/fishing_mining_system.gd delete mode 100644 project/fishing_mining_system.gd.uid delete mode 100644 project/fishing_rod_reel_system.gd delete mode 100644 project/fishing_rod_reel_system.gd.uid delete mode 100644 project/flame_dungeon_razador_system.gd delete mode 100644 project/flame_dungeon_razador_system.gd.uid delete mode 100644 project/game_button_money_dialog_system.gd delete mode 100644 project/game_button_money_dialog_system.gd.uid delete mode 100644 project/gm_toolkit_system.gd delete mode 100644 project/gm_toolkit_system.gd.uid delete mode 100644 project/guild_mark_uploader_system.gd delete mode 100644 project/guild_mark_uploader_system.gd.uid delete mode 100644 project/guild_skill_system.gd delete mode 100644 project/guild_skill_system.gd.uid delete mode 100644 project/guild_war_observer_arena_system.gd delete mode 100644 project/guild_war_observer_arena_system.gd.uid delete mode 100644 project/guild_war_system.gd delete mode 100644 project/guild_war_system.gd.uid delete mode 100644 project/hair_dye_system.gd delete mode 100644 project/hair_dye_system.gd.uid delete mode 100644 project/herbal_dew_system.gd delete mode 100644 project/herbal_dew_system.gd.uid delete mode 100644 project/horse_combat_system.gd delete mode 100644 project/horse_combat_system.gd.uid delete mode 100644 project/horse_level_growth_system.gd delete mode 100644 project/horse_level_growth_system.gd.uid delete mode 100644 project/horse_system.gd delete mode 100644 project/horse_system.gd.uid delete mode 100644 project/inventory_expansion_system.gd delete mode 100644 project/inventory_expansion_system.gd.uid delete mode 100644 project/inventory_sort_system.gd delete mode 100644 project/inventory_sort_system.gd.uid delete mode 100644 project/item_enchant_system.gd delete mode 100644 project/item_enchant_system.gd.uid delete mode 100644 project/item_split_system.gd delete mode 100644 project/item_split_system.gd.uid delete mode 100644 project/jewelry_socket_system.gd delete mode 100644 project/jewelry_socket_system.gd.uid delete mode 100644 project/lucky_lottery_fortune_system.gd delete mode 100644 project/lucky_lottery_fortune_system.gd.uid delete mode 100644 project/mail_system.gd delete mode 100644 project/mail_system.gd.uid delete mode 100644 project/mall_affect_system.gd delete mode 100644 project/mall_affect_system.gd.uid delete mode 100644 project/map_name_shower_system.gd delete mode 100644 project/map_name_shower_system.gd.uid delete mode 100644 project/marriage_system.gd delete mode 100644 project/marriage_system.gd.uid delete mode 100644 project/marriage_wedding_love_system.gd delete mode 100644 project/marriage_wedding_love_system.gd.uid delete mode 100644 project/mercenary_companion_system.gd delete mode 100644 project/mercenary_companion_system.gd.uid delete mode 100644 project/messenger_system.gd delete mode 100644 project/messenger_system.gd.uid delete mode 100644 project/metin_spawn_event_system.gd delete mode 100644 project/metin_spawn_event_system.gd.uid delete mode 100644 project/mining_refinery_smelt_system.gd delete mode 100644 project/mining_refinery_smelt_system.gd.uid delete mode 100644 project/mob_combat_reaction_system.gd delete mode 100644 project/mob_combat_reaction_system.gd.uid delete mode 100644 project/monarch_election_ui_system.gd delete mode 100644 project/monarch_election_ui_system.gd.uid delete mode 100644 project/monarch_empire_treasury_system.gd delete mode 100644 project/monarch_empire_treasury_system.gd.uid delete mode 100644 project/monster_card_system.gd delete mode 100644 project/monster_card_system.gd.uid delete mode 100644 project/mounted_combat_skill_system.gd delete mode 100644 project/mounted_combat_skill_system.gd.uid delete mode 100644 project/mysterious_merchant_blackmarket_system.gd delete mode 100644 project/mysterious_merchant_blackmarket_system.gd.uid delete mode 100644 project/npc_shop_system.gd delete mode 100644 project/npc_shop_system.gd.uid delete mode 100644 project/offline_bazaar_market_system.gd delete mode 100644 project/offline_bazaar_market_system.gd.uid delete mode 100644 project/party_management_system.gd delete mode 100644 project/party_management_system.gd.uid delete mode 100644 project/pet_companion_system.gd delete mode 100644 project/pet_companion_system.gd.uid delete mode 100644 project/pk_penalty_system.gd delete mode 100644 project/pk_penalty_system.gd.uid delete mode 100644 project/player_trade_exchange_system.gd delete mode 100644 project/player_trade_exchange_system.gd.uid delete mode 100644 project/point_reset_system.gd delete mode 100644 project/point_reset_system.gd.uid delete mode 100644 project/polymorph_marble_system.gd delete mode 100644 project/polymorph_marble_system.gd.uid delete mode 100644 project/polymorph_system.gd delete mode 100644 project/polymorph_system.gd.uid delete mode 100644 project/portable_service_system.gd delete mode 100644 project/portable_service_system.gd.uid delete mode 100644 project/private_shop_system.gd delete mode 100644 project/private_shop_system.gd.uid delete mode 100644 project/privilege_empire_guild_system.gd delete mode 100644 project/privilege_empire_guild_system.gd.uid delete mode 100644 project/refine_effect_system.gd delete mode 100644 project/refine_effect_system.gd.uid delete mode 100644 project/safebox_storage_keeper_system.gd delete mode 100644 project/safebox_storage_keeper_system.gd.uid delete mode 100644 project/save_snapshot_system.gd delete mode 100644 project/save_snapshot_system.gd.uid delete mode 100644 project/security_lock_system.gd delete mode 100644 project/security_lock_system.gd.uid delete mode 100644 project/skill_book_reading_system.gd delete mode 100644 project/skill_book_reading_system.gd.uid delete mode 100644 project/skill_book_system.gd delete mode 100644 project/skill_book_system.gd.uid delete mode 100644 project/socket_glow_fx_system.gd delete mode 100644 project/socket_glow_fx_system.gd.uid delete mode 100644 project/solo_guild_domain_system.gd delete mode 100644 project/solo_guild_domain_system.gd.uid delete mode 100644 project/soul_stone_grandmaster_system.gd delete mode 100644 project/soul_stone_grandmaster_system.gd.uid delete mode 100644 project/soulbound_lock_security_system.gd delete mode 100644 project/soulbound_lock_security_system.gd.uid delete mode 100644 project/spider_dungeon_key_system.gd delete mode 100644 project/spider_dungeon_key_system.gd.uid delete mode 100644 project/spider_dungeon_system.gd delete mode 100644 project/spider_dungeon_system.gd.uid delete mode 100644 project/status_skill_reset_system.gd delete mode 100644 project/status_skill_reset_system.gd.uid delete mode 100644 project/system_setting_system.gd delete mode 100644 project/system_setting_system.gd.uid delete mode 100644 project/test_alignment_parity.gd delete mode 100644 project/test_alignment_parity.gd.uid delete mode 100644 project/test_alignment_pk_parity.gd delete mode 100644 project/test_alignment_pk_parity.gd.uid delete mode 100644 project/test_atlas_navigation_parity.gd delete mode 100644 project/test_atlas_navigation_parity.gd.uid delete mode 100644 project/test_attach_metin_parity.gd delete mode 100644 project/test_attach_metin_parity.gd.uid delete mode 100644 project/test_auto_combat_bot_parity.gd delete mode 100644 project/test_auto_combat_bot_parity.gd.uid delete mode 100644 project/test_battle_arena_duel_parity.gd delete mode 100644 project/test_battle_arena_duel_parity.gd.uid delete mode 100644 project/test_belt_system_parity.gd delete mode 100644 project/test_belt_system_parity.gd.uid delete mode 100644 project/test_bgm_jukebox_parity.gd delete mode 100644 project/test_bgm_jukebox_parity.gd.uid delete mode 100644 project/test_biologist_chaegirab_research_parity.gd delete mode 100644 project/test_biologist_chaegirab_research_parity.gd.uid delete mode 100644 project/test_biologist_quest_parity.gd delete mode 100644 project/test_biologist_quest_parity.gd.uid delete mode 100644 project/test_boss_summon_scroll_parity.gd delete mode 100644 project/test_boss_summon_scroll_parity.gd.uid delete mode 100644 project/test_chat_hyperlink_dice_parity.gd delete mode 100644 project/test_chat_hyperlink_dice_parity.gd.uid delete mode 100644 project/test_class_specialization_parity.gd delete mode 100644 project/test_class_specialization_parity.gd.uid delete mode 100644 project/test_classic_pet_parity.gd delete mode 100644 project/test_classic_pet_parity.gd.uid delete mode 100644 project/test_client_phase_integration_parity.gd delete mode 100644 project/test_client_phase_integration_parity.gd.uid delete mode 100644 project/test_combat_replay_snapshot_parity.gd delete mode 100644 project/test_combat_replay_snapshot_parity.gd.uid delete mode 100644 project/test_combat_status_effect_parity.gd delete mode 100644 project/test_combat_status_effect_parity.gd.uid delete mode 100644 project/test_compass_treasure_hunt_parity.gd delete mode 100644 project/test_compass_treasure_hunt_parity.gd.uid delete mode 100644 project/test_costume_attr_transfer_parity.gd delete mode 100644 project/test_costume_attr_transfer_parity.gd.uid delete mode 100644 project/test_costume_system_parity.gd delete mode 100644 project/test_costume_system_parity.gd.uid delete mode 100644 project/test_cube_crafting_parity.gd delete mode 100644 project/test_cube_crafting_parity.gd.uid delete mode 100644 project/test_cursed_monolith_spire_parity.gd delete mode 100644 project/test_cursed_monolith_spire_parity.gd.uid delete mode 100644 project/test_demon_tower_parity.gd delete mode 100644 project/test_demon_tower_parity.gd.uid delete mode 100644 project/test_devils_catacomb_parity.gd delete mode 100644 project/test_devils_catacomb_parity.gd.uid delete mode 100644 project/test_dragon_lair_parity.gd delete mode 100644 project/test_dragon_lair_parity.gd.uid delete mode 100644 project/test_dragon_soul_parity.gd delete mode 100644 project/test_dragon_soul_parity.gd.uid delete mode 100644 project/test_dungeon_ranking_parity.gd delete mode 100644 project/test_dungeon_ranking_parity.gd.uid delete mode 100644 project/test_dynamic_bounty_event_parity.gd delete mode 100644 project/test_dynamic_bounty_event_parity.gd.uid delete mode 100644 project/test_emotion_action_parity.gd delete mode 100644 project/test_emotion_action_parity.gd.uid delete mode 100644 project/test_empire_kingdom_parity.gd delete mode 100644 project/test_empire_kingdom_parity.gd.uid delete mode 100644 project/test_endless_spire_dungeon_parity.gd delete mode 100644 project/test_endless_spire_dungeon_parity.gd.uid delete mode 100644 project/test_energy_system_parity.gd delete mode 100644 project/test_energy_system_parity.gd.uid delete mode 100644 project/test_epic_refine_parity.gd delete mode 100644 project/test_epic_refine_parity.gd.uid delete mode 100644 project/test_equipment_inspect_parity.gd delete mode 100644 project/test_equipment_inspect_parity.gd.uid delete mode 100644 project/test_event_manager_parity.gd delete mode 100644 project/test_event_manager_parity.gd.uid delete mode 100644 project/test_fish_clam_roast_parity.gd delete mode 100644 project/test_fish_clam_roast_parity.gd.uid delete mode 100644 project/test_fishing_cooking_camp_parity.gd delete mode 100644 project/test_fishing_cooking_camp_parity.gd.uid delete mode 100644 project/test_fishing_mining_parity.gd delete mode 100644 project/test_fishing_mining_parity.gd.uid delete mode 100644 project/test_fishing_rod_reel_parity.gd delete mode 100644 project/test_fishing_rod_reel_parity.gd.uid delete mode 100644 project/test_flame_dungeon_razador_parity.gd delete mode 100644 project/test_flame_dungeon_razador_parity.gd.uid delete mode 100644 project/test_game_button_money_dialog_parity.gd delete mode 100644 project/test_game_button_money_dialog_parity.gd.uid delete mode 100644 project/test_gm_toolkit_parity.gd delete mode 100644 project/test_gm_toolkit_parity.gd.uid delete mode 100644 project/test_guild_mark_uploader_parity.gd delete mode 100644 project/test_guild_mark_uploader_parity.gd.uid delete mode 100644 project/test_guild_skill_parity.gd delete mode 100644 project/test_guild_skill_parity.gd.uid delete mode 100644 project/test_guild_war_observer_arena_parity.gd delete mode 100644 project/test_guild_war_observer_arena_parity.gd.uid delete mode 100644 project/test_guild_war_parity.gd delete mode 100644 project/test_guild_war_parity.gd.uid delete mode 100644 project/test_hair_dye_parity.gd delete mode 100644 project/test_hair_dye_parity.gd.uid delete mode 100644 project/test_herbal_dew_parity.gd delete mode 100644 project/test_herbal_dew_parity.gd.uid delete mode 100644 project/test_horse_combat_parity.gd delete mode 100644 project/test_horse_combat_parity.gd.uid delete mode 100644 project/test_horse_level_growth_parity.gd delete mode 100644 project/test_horse_level_growth_parity.gd.uid delete mode 100644 project/test_inventory_expansion_parity.gd delete mode 100644 project/test_inventory_expansion_parity.gd.uid delete mode 100644 project/test_inventory_sort_parity.gd delete mode 100644 project/test_inventory_sort_parity.gd.uid delete mode 100644 project/test_item_enchant_manager_parity.gd delete mode 100644 project/test_item_enchant_manager_parity.gd.uid delete mode 100644 project/test_item_split_parity.gd delete mode 100644 project/test_item_split_parity.gd.uid delete mode 100644 project/test_jewelry_socket_parity.gd delete mode 100644 project/test_jewelry_socket_parity.gd.uid delete mode 100644 project/test_lucky_lottery_fortune_parity.gd delete mode 100644 project/test_lucky_lottery_fortune_parity.gd.uid delete mode 100644 project/test_mail_system_parity.gd delete mode 100644 project/test_mail_system_parity.gd.uid delete mode 100644 project/test_mall_affect_parity.gd delete mode 100644 project/test_mall_affect_parity.gd.uid delete mode 100644 project/test_map_name_shower_parity.gd delete mode 100644 project/test_map_name_shower_parity.gd.uid delete mode 100644 project/test_marriage_parity.gd delete mode 100644 project/test_marriage_parity.gd.uid delete mode 100644 project/test_marriage_wedding_love_parity.gd delete mode 100644 project/test_marriage_wedding_love_parity.gd.uid delete mode 100644 project/test_mercenary_companion_parity.gd delete mode 100644 project/test_mercenary_companion_parity.gd.uid delete mode 100644 project/test_messenger_parity.gd delete mode 100644 project/test_messenger_parity.gd.uid delete mode 100644 project/test_metin_spawn_event_parity.gd delete mode 100644 project/test_metin_spawn_event_parity.gd.uid delete mode 100644 project/test_mining_refinery_smelt_parity.gd delete mode 100644 project/test_mining_refinery_smelt_parity.gd.uid delete mode 100644 project/test_mob_combat_reaction_parity.gd delete mode 100644 project/test_mob_combat_reaction_parity.gd.uid delete mode 100644 project/test_monarch_election_ui_parity.gd delete mode 100644 project/test_monarch_election_ui_parity.gd.uid delete mode 100644 project/test_monarch_empire_treasury_parity.gd delete mode 100644 project/test_monarch_empire_treasury_parity.gd.uid delete mode 100644 project/test_monster_card_parity.gd delete mode 100644 project/test_monster_card_parity.gd.uid delete mode 100644 project/test_mount_horse_parity.gd delete mode 100644 project/test_mount_horse_parity.gd.uid delete mode 100644 project/test_mounted_combat_skill_parity.gd delete mode 100644 project/test_mounted_combat_skill_parity.gd.uid delete mode 100644 project/test_mysterious_merchant_blackmarket_parity.gd delete mode 100644 project/test_mysterious_merchant_blackmarket_parity.gd.uid delete mode 100644 project/test_npc_shop_parity.gd delete mode 100644 project/test_npc_shop_parity.gd.uid delete mode 100644 project/test_offline_bazaar_market_parity.gd delete mode 100644 project/test_offline_bazaar_market_parity.gd.uid delete mode 100644 project/test_party_management_parity.gd delete mode 100644 project/test_party_management_parity.gd.uid delete mode 100644 project/test_pet_companion_parity.gd delete mode 100644 project/test_pet_companion_parity.gd.uid delete mode 100644 project/test_pk_penalty_parity.gd delete mode 100644 project/test_pk_penalty_parity.gd.uid delete mode 100644 project/test_player_trade_exchange_parity.gd delete mode 100644 project/test_player_trade_exchange_parity.gd.uid delete mode 100644 project/test_point_reset_parity.gd delete mode 100644 project/test_point_reset_parity.gd.uid delete mode 100644 project/test_polymorph_marble_parity.gd delete mode 100644 project/test_polymorph_marble_parity.gd.uid delete mode 100644 project/test_polymorph_parity.gd delete mode 100644 project/test_polymorph_parity.gd.uid delete mode 100644 project/test_portable_service_parity.gd delete mode 100644 project/test_portable_service_parity.gd.uid delete mode 100644 project/test_private_shop_bundle_parity.gd delete mode 100644 project/test_private_shop_bundle_parity.gd.uid delete mode 100644 project/test_privilege_empire_guild_parity.gd delete mode 100644 project/test_privilege_empire_guild_parity.gd.uid delete mode 100644 project/test_refine_effect_parity.gd delete mode 100644 project/test_refine_effect_parity.gd.uid delete mode 100644 project/test_safebox_storage_keeper_parity.gd delete mode 100644 project/test_safebox_storage_keeper_parity.gd.uid delete mode 100644 project/test_save_snapshot_parity.gd delete mode 100644 project/test_save_snapshot_parity.gd.uid delete mode 100644 project/test_security_lock_parity.gd delete mode 100644 project/test_security_lock_parity.gd.uid delete mode 100644 project/test_skill_book_parity.gd delete mode 100644 project/test_skill_book_parity.gd.uid delete mode 100644 project/test_skill_book_reading_parity.gd delete mode 100644 project/test_skill_book_reading_parity.gd.uid delete mode 100644 project/test_socket_glow_fx_parity.gd delete mode 100644 project/test_socket_glow_fx_parity.gd.uid delete mode 100644 project/test_solo_guild_domain_parity.gd delete mode 100644 project/test_solo_guild_domain_parity.gd.uid delete mode 100644 project/test_soul_stone_grandmaster_parity.gd delete mode 100644 project/test_soul_stone_grandmaster_parity.gd.uid delete mode 100644 project/test_soulbound_lock_security_parity.gd delete mode 100644 project/test_soulbound_lock_security_parity.gd.uid delete mode 100644 project/test_spider_dungeon_key_parity.gd delete mode 100644 project/test_spider_dungeon_key_parity.gd.uid delete mode 100644 project/test_spider_dungeon_parity.gd delete mode 100644 project/test_spider_dungeon_parity.gd.uid delete mode 100644 project/test_status_skill_reset_parity.gd delete mode 100644 project/test_status_skill_reset_parity.gd.uid delete mode 100644 project/test_system_setting_parity.gd delete mode 100644 project/test_system_setting_parity.gd.uid delete mode 100644 project/test_title_achievement_parity.gd delete mode 100644 project/test_title_achievement_parity.gd.uid delete mode 100644 project/test_title_trophy_buff_parity.gd delete mode 100644 project/test_title_trophy_buff_parity.gd.uid delete mode 100644 project/test_treasure_box_key_parity.gd delete mode 100644 project/test_treasure_box_key_parity.gd.uid delete mode 100644 project/test_wardrobe_transmog_parity.gd delete mode 100644 project/test_wardrobe_transmog_parity.gd.uid delete mode 100644 project/test_weather_daynight_parity.gd delete mode 100644 project/test_weather_daynight_parity.gd.uid delete mode 100644 project/test_wedding_marriage_parity.gd delete mode 100644 project/test_wedding_marriage_parity.gd.uid delete mode 100644 project/test_world_boss_ai_behavior_parity.gd delete mode 100644 project/test_world_boss_ai_behavior_parity.gd.uid delete mode 100644 project/test_world_boss_parity.gd delete mode 100644 project/test_world_boss_parity.gd.uid delete mode 100644 project/title_achievement_system.gd delete mode 100644 project/title_achievement_system.gd.uid delete mode 100644 project/title_trophy_buff_system.gd delete mode 100644 project/title_trophy_buff_system.gd.uid delete mode 100644 project/treasure_box_key_system.gd delete mode 100644 project/treasure_box_key_system.gd.uid delete mode 100644 project/wardrobe_transmog_system.gd delete mode 100644 project/wardrobe_transmog_system.gd.uid delete mode 100644 project/weather_daynight_system.gd delete mode 100644 project/weather_daynight_system.gd.uid delete mode 100644 project/wedding_marriage_system.gd delete mode 100644 project/wedding_marriage_system.gd.uid delete mode 100644 project/world_boss_ai_system.gd delete mode 100644 project/world_boss_ai_system.gd.uid delete mode 100644 project/world_boss_system.gd delete mode 100644 project/world_boss_system.gd.uid diff --git a/audit/contracts/audio.bgm-ambience-3d.md b/audit/contracts/audio.bgm-ambience-3d.md index 5c3f37e9..5d2e4a5c 100644 --- a/audit/contracts/audio.bgm-ambience-3d.md +++ b/audit/contracts/audio.bgm-ambience-3d.md @@ -102,3 +102,7 @@ - 先审计 `lifecycle.platform_release`,核对 Windows 窗口、导出、自包含资源、崩溃处理和发布门禁。 - 回到实现阶段时,优先建立 BGM 多实例状态机和真实 3D 播放器清理测试,再处理固定实例池/Provider 的适配边界。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`test_bgm_jukebox_parity.gd`。 diff --git a/audit/contracts/combat.affect-status.md b/audit/contracts/combat.affect-status.md index ae245f34..acd7ce83 100644 --- a/audit/contracts/combat.affect-status.md +++ b/audit/contracts/combat.affect-status.md @@ -93,3 +93,7 @@ - 为每个 affect bit 建立 40250 资源、附着骨骼、开关、清理和石头烟雾矩阵。 - 增加 `GC_AFFECT_ADD → duration → GC_AFFECT_REMOVE`、重复/乱序/重连/换图的 native→HUD 端到端测试。 - 增加远端 Actor 的 stunned/knockdown/skill-cancel 状态机测试,并修复 `hit_view_test.gd` 的前向 damage 资源选择后再提升合同状态。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`combat_status_effect_system.gd`, `test_combat_status_effect_parity.gd`, `test_mob_combat_reaction_parity.gd`。 diff --git a/audit/contracts/combat.attack-combo-damage.md b/audit/contracts/combat.attack-combo-damage.md index b9d833f1..5638b6a4 100644 --- a/audit/contracts/combat.attack-combo-damage.md +++ b/audit/contracts/combat.attack-combo-damage.md @@ -237,3 +237,7 @@ native `CActorInstance` 的同一骨骼 composite matrix、旋转和绑定坐标 进程退出码为 0;此前记录的 race 0 / 03150 左手贴合断言失败不再是当前待办项,已从 manifest 的 `remaining` 移除。该校正只更新证据状态,不改变 WeaponTrace native 骨骼 composite matrix、远端/怪物覆盖和服务端攻击闭环仍未证明的结论。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`socket_glow_fx_system.gd`, `test_combat_status_effect_parity.gd`。 diff --git a/audit/contracts/combat.target-selection-rules.md b/audit/contracts/combat.target-selection-rules.md index 20516933..adb0e63b 100644 --- a/audit/contracts/combat.target-selection-rules.md +++ b/audit/contracts/combat.target-selection-rules.md @@ -119,3 +119,7 @@ - 增加完整 `.msa` 事件源的 `CanChangeTarget`/动作锁/重复点击时间窗测试,并对照 `_change_target_to_picked_instance` 的实际调用链。 - 增加地图属性 `ATTRIBUTE_BANPK` 与 `in_safe` 的移动、换图、目标切换矩阵。 - 只有以上差异修复并通过 native/classic 与场景回归后,才能将本合同从 `PARTIAL` 提升。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`alignment_pk_system.gd`, `test_alignment_pk_parity.gd`, `test_auto_combat_bot_parity.gd`。 diff --git a/audit/contracts/function-inventory.md b/audit/contracts/function-inventory.md index ad62d750..0bcd829f 100644 --- a/audit/contracts/function-inventory.md +++ b/audit/contracts/function-inventory.md @@ -84,3 +84,7 @@ - 本轮扫描到 524 个活跃源码/头文件:18 个 StdAfx 构建支持,142 个已在合同 manifest 中逐文件列出,364 个进入按行为合同归属的静态复核队列;当前未归属行为文件为 0。 - `EffectLib`、`EterGrnLib`、`MilesLib` 已完成本轮活跃符号和生命周期链静态复核,但仍按实现差异保持 `PARTIAL`;其余 inferred queue 不得因为自动归属而视为已审计。 - 下次审计必须先运行源码覆盖扫描,再从 `inferred_review_queue` 取下一组,补充对应合同的参考文件、符号、分支矩阵和测试证据;只有完成语义核对后才能转为 `reviewed_static_group`。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`combat_status_effect_system.gd`, `costume_system.gd`, `dungeon_ranking_system.gd`, `fishing_cooking_camp_system.gd`, `fishing_mining_system.gd`, `guild_mark_uploader_system.gd`, `guild_war_system.gd`, `horse_combat_system.gd`, `horse_system.gd`, `item_enchant_system.gd`, `item_split_system.gd`, `jewelry_socket_system.gd`, `messenger_system.gd`, `mining_refinery_smelt_system.gd`, `mounted_combat_skill_system.gd`, `npc_shop_system.gd`, `party_management_system.gd`, `pet_companion_system.gd`, `player_trade_exchange_system.gd`, `polymorph_system.gd`, `private_shop_system.gd`, `safebox_storage_keeper_system.gd`, `wardrobe_transmog_system.gd`, `weather_daynight_system.gd`, `world_boss_system.gd`。 diff --git a/audit/contracts/item.consumable-action.md b/audit/contracts/item.consumable-action.md index 600189bd..49b81494 100644 --- a/audit/contracts/item.consumable-action.md +++ b/audit/contracts/item.consumable-action.md @@ -151,3 +151,7 @@ `PARTIAL`。快捷栏增删/交换现在遵循请求→`GC_QUICKSLOT_*`→重建显示的权威时序,分页也已按 4 页取模; 但快捷栏使用的统一入口、消耗品反馈闭环、真实回包 fixture、音效/sequence 和跨生命周期清理仍未闭合, 因此不能把整个合同标记为 40250 等价。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`item_split_system.gd`, `test_item_split_parity.gd`。 diff --git a/audit/contracts/item.refine-socket-costume.md b/audit/contracts/item.refine-socket-costume.md index f60fdb9e..392e418c 100644 --- a/audit/contracts/item.refine-socket-costume.md +++ b/audit/contracts/item.refine-socket-costume.md @@ -120,3 +120,7 @@ - `EntityStore` 的正 HP 点数更新会清除 `dead`,`M2Client` 会重置 `dead_seen`,保证复活后下一次死亡可以再次发出 `entity_dead`。 本合同继续为 `PARTIAL`:本轮只修复了可直接验证的协议边界和死亡状态边沿,没有把本地属性/镶嵌/时装旁路误标为 40250 等价实现。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`costume_attr_transfer_system.gd`, `costume_system.gd`, `item_enchant_system.gd`, `jewelry_socket_system.gd`, `refine_effect_system.gd`, `test_costume_attr_transfer_parity.gd`, `test_costume_system_parity.gd`, `test_item_enchant_manager_parity.gd`, `test_jewelry_socket_parity.gd`, `test_refine_effect_parity.gd`, `test_wardrobe_transmog_parity.gd`, `wardrobe_transmog_system.gd`。 diff --git a/audit/contracts/movement.keyboard-motion.md b/audit/contracts/movement.keyboard-motion.md index 82df4c75..d7e35245 100644 --- a/audit/contracts/movement.keyboard-motion.md +++ b/audit/contracts/movement.keyboard-motion.md @@ -338,3 +338,7 @@ server_speed_scale = 0.0 if moving_speed > 1100 else float(moving_speed) / 100.0 - `OnWaiting`:参考只在 `CInstanceBase::Transform()` 的 `IsWalking() || IsUsingMovingSkill()` 分支内、本帧位移 ≤1.0 时调用(`InstanceBase.cpp:1922-1930`),100ms 节流,位移基准为 `m_kPPosPrevWaiting`(由 `OnSyncing`/`OnWaiting` 更新)。当前端原有的 `_tick_on_waiting()` 在站立期间周期补发 `FUNC_WAIT`,参考没有该机制,已删除(含 `SEND_WAITING_*` 与 `_last_waiting_sent_t`)。走/跑中本帧无位移的 OnWaiting 路径尚未移植(DIVERGENT)。 - `CInstanceBase::SHORSE::SetMoveSpeed` 只同步坐骑模型动画速率,不是另一套移动速度公式,归 `world.mount_pet_polymorph`。 - 测试:`netplay_test.gd` 新增“站立被推开不发包”断言;netplay/keyboard_motion_timeline/movement_parity/player_move/test_wasd_steering_parity/test_no_auto_move_regression/motion_event_warp/playable_combat 均 PASS。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`test_alignment_parity.gd`。 diff --git a/audit/contracts/npc.dialog-shop.md b/audit/contracts/npc.dialog-shop.md index b5f438e4..a7955111 100644 --- a/audit/contracts/npc.dialog-shop.md +++ b/audit/contracts/npc.dialog-shop.md @@ -101,3 +101,7 @@ ## Status `PARTIAL`。NPC 点击、基础对话承接、普通/扩展商店协议骨架、购买出售 UI 和个人商店字段已经存在,但当前端仍有明确的多 tab 更新索引错误、购买模式错误放行出售、个人商店招牌不可点击打开、自己商店状态不分支、native 动作门控缺失以及本地硬编码库存/金币旁路。已完成审计并记录,未修改实现代码。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`npc_shop_system.gd`, `private_shop_system.gd`, `test_npc_shop_parity.gd`, `test_private_shop_bundle_parity.gd`。 diff --git a/audit/contracts/quest.quest-event-navigation.md b/audit/contracts/quest.quest-event-navigation.md index 604c48b8..0e3f1837 100644 --- a/audit/contracts/quest.quest-event-navigation.md +++ b/audit/contracts/quest.quest-event-navigation.md @@ -223,3 +223,7 @@ Atlas overlay 使用与小地图相同的 server-centimetre→world→map 坐标 `test_quest_dialog_parity.gd` 新增缺失资源回归,当前 49 项全部通过;真实 `.msc` 文件的 完整事件序列、过场播放器消费者和中断清理仍保持 `PARTIAL`。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`atlas_navigation_system.gd`, `biologist_quest_system.gd`, `dynamic_bounty_event_system.gd`, `event_manager_system.gd`, `metin_spawn_event_system.gd`, `test_atlas_navigation_parity.gd`, `test_biologist_quest_parity.gd`, `test_event_manager_parity.gd`, `test_map_name_shower_parity.gd`。 diff --git a/audit/contracts/skill.player-ui-cooldown.md b/audit/contracts/skill.player-ui-cooldown.md index e064b7ce..65a1fca0 100644 --- a/audit/contracts/skill.player-ui-cooldown.md +++ b/audit/contracts/skill.player-ui-cooldown.md @@ -148,3 +148,7 @@ - `player_skill_test`、`skill_test`、`guild_war_skill_test` 均通过,覆盖非公会战拒绝、公会战独立包和 UI 公会技能页。 仍保持 `PARTIAL`:真实 `GUILD_WAR_ON_WAR` 回包、服务端拒绝/冷却回滚、跨生命周期乱序和完整动作/damage 证据尚未闭合。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`test_status_skill_reset_parity.gd`。 diff --git a/audit/contracts/social.chat-text-tail.md b/audit/contracts/social.chat-text-tail.md index 45e0dc90..42f13fba 100644 --- a/audit/contracts/social.chat-text-tail.md +++ b/audit/contracts/social.chat-text-tail.md @@ -127,3 +127,7 @@ ## Status `PARTIAL`。当前端已有聊天协议骨架、公共输入、私聊基础发送、聊天气泡和名字尾标的可测试镜像,但并非与 40250 统一实现:live 私聊已接入独立会话,但窗口历史/任务栏通知/服务端 ignore 命令仍未闭环;公共颜色/notice 分支不一致、入站表情/过滤/跨帝国文本缺失,且尾标渲染仍使用 Godot billboard 与宽度估算。本轮已修正 whisper mode 映射、空错误文案、截断 whisper 拒绝逻辑并接通 live conversation/ignore 判断,其他差异继续保持 PARTIAL。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`test_chat_hyperlink_dice_parity.gd`。 diff --git a/audit/contracts/social.exchange-safebox.md b/audit/contracts/social.exchange-safebox.md index 3a881db8..4286f0dc 100644 --- a/audit/contracts/social.exchange-safebox.md +++ b/audit/contracts/social.exchange-safebox.md @@ -97,3 +97,7 @@ ## Status `PARTIAL`。当前端已经具备 exchange/safebox/mall 的 native 协议骨架、回包存储和主要 UI,交易物品属性与仓库商城的基本网络状态能够展示;但 native exchange 请求缺 40250 动作门控,交易/仓库 UI 存在 optimistic 状态和提前关闭,本地 helper 直接承担交易结算、密码、库存和金条 authority,且真实错误、乱序、断线和回包回滚证据不足。本轮完成审计并记录,未修改实现。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`player_trade_exchange_system.gd`, `safebox_storage_keeper_system.gd`, `test_mall_affect_parity.gd`, `test_player_trade_exchange_parity.gd`, `test_safebox_storage_keeper_parity.gd`, `test_security_lock_parity.gd`。 diff --git a/audit/contracts/social.party-guild-messenger.md b/audit/contracts/social.party-guild-messenger.md index a610ac77..cc06f3ef 100644 --- a/audit/contracts/social.party-guild-messenger.md +++ b/audit/contracts/social.party-guild-messenger.md @@ -106,3 +106,7 @@ ## Status `PARTIAL`。当前端的 native 社交协议和主要 UI 已有较完整骨架,组队、公会、好友和会徽 live 主路径大体接上 40250;但本地 party/guild skill/war/messenger/mark helper 仍把服务端权威逻辑复制成直接状态机,且真实权限拒绝、回包顺序、重连清理和独立会徽连接缺端到端证据。因此不能把现有 parity 测试通过等同于实现统一。本轮完成审计并记录,未修改实现代码。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`guild_mark_uploader_system.gd`, `guild_skill_system.gd`, `guild_war_system.gd`, `messenger_system.gd`, `party_management_system.gd`, `test_guild_mark_uploader_parity.gd`, `test_guild_skill_parity.gd`, `test_guild_war_observer_arena_parity.gd`, `test_guild_war_parity.gd`, `test_messenger_parity.gd`, `test_party_management_parity.gd`, `test_privilege_empire_guild_parity.gd`。 diff --git a/audit/contracts/world.dungeon-activity.md b/audit/contracts/world.dungeon-activity.md index 1a569dc1..f725fe04 100644 --- a/audit/contracts/world.dungeon-activity.md +++ b/audit/contracts/world.dungeon-activity.md @@ -86,3 +86,7 @@ 本轮仍为 `PARTIAL`;完成三个地下城/活动方向的合同审计并登记,未修改实现。 `PARTIAL`。当前端已经具备 `GC_DUNGEON` 目标点的主要 parser、坐标状态和与 40250 一致的 Manhattan/20 秒算法,也有结果窗口骨架;但 40250 的 compass `.mse` alarm 没有实际消费者,结果链缺少 live 回包时序证据,且 Boss、排行和具体副本 helper 是当前端本地 authority,不是 40250 Windows 客户端实现。本轮完成审计并记录,未修改实现。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`dungeon_ranking_system.gd`, `endless_spire_dungeon_system.gd`, `flame_dungeon_razador_system.gd`, `spider_dungeon_system.gd`, `test_dungeon_ranking_parity.gd`, `test_endless_spire_dungeon_parity.gd`, `test_flame_dungeon_razador_parity.gd`, `test_spider_dungeon_key_parity.gd`, `test_spider_dungeon_parity.gd`, `world_boss_system.gd`。 diff --git a/audit/contracts/world.environment-weather.md b/audit/contracts/world.environment-weather.md index 9203ffb8..c4cbe358 100644 --- a/audit/contracts/world.environment-weather.md +++ b/audit/contracts/world.environment-weather.md @@ -102,3 +102,7 @@ ## 下一轮验证入口 - 进入实现阶段后,优先修复正式 GameScene 的天气/地图环境接入和 fog/filter/lens flare/wind 中的已确认 GAP;修复后重新运行本合同四组测试并增加真实渲染证据。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`test_weather_daynight_parity.gd`, `weather_daynight_system.gd`。 diff --git a/audit/contracts/world.fishing-mining.md b/audit/contracts/world.fishing-mining.md index 127001c1..41499f9c 100644 --- a/audit/contracts/world.fishing-mining.md +++ b/audit/contracts/world.fishing-mining.md @@ -86,3 +86,7 @@ 本轮仍为 `PARTIAL`;完成钓鱼/采矿方向的合同审计并登记,未修改实现。 `PARTIAL`。当前端已经具备 40250 钓鱼/采矿协议骨架、角色动作入口、水属性扫描和水面构造;但持竿/持镐 live 门控、角色状态条件、采矿 count 动作队列和真实资源副作用证据不足,且钓鱼收益、烹饪、营火、熔炼、宝石与库存脚本承担了 40250 客户端没有承担的本地 authority。本轮完成审计并记录,未修改实现。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`fishing_cooking_camp_system.gd`, `fishing_mining_system.gd`, `fishing_rod_reel_system.gd`, `mining_refinery_smelt_system.gd`, `test_fishing_cooking_camp_parity.gd`, `test_fishing_mining_parity.gd`, `test_fishing_rod_reel_parity.gd`, `test_mining_refinery_smelt_parity.gd`。 diff --git a/audit/contracts/world.mount-pet-polymorph.md b/audit/contracts/world.mount-pet-polymorph.md index ab1eb163..2d8aaad7 100644 --- a/audit/contracts/world.mount-pet-polymorph.md +++ b/audit/contracts/world.mount-pet-polymorph.md @@ -81,3 +81,7 @@ 本轮仍为 `PARTIAL`;完成坐骑、宠物和变身方向的合同审计并登记,未修改实现。 `PARTIAL`。当前端已具备 `mount_vnum` 的网络存储、mount_changed 信号、部分动作模式/旋转速度/攻击门控,并有本地马匹、宠物和变身 helper;但没有发现实际坐骑模型挂接链,变身状态尚未进入 native entity,宠物与马战/成长/耐力/伤害脚本承担了 40250 客户端没有承担的本地 authority。基础测试全部通过仍不足以证明与 40250 Windows 客户端实现统一。本轮完成审计并记录,未修改实现。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`classic_pet_system.gd`, `horse_combat_system.gd`, `horse_level_growth_system.gd`, `horse_system.gd`, `mounted_combat_skill_system.gd`, `pet_companion_system.gd`, `polymorph_marble_system.gd`, `polymorph_system.gd`, `test_battle_arena_duel_parity.gd`, `test_classic_pet_parity.gd`, `test_horse_combat_parity.gd`, `test_horse_level_growth_parity.gd`, `test_mount_horse_parity.gd`, `test_mounted_combat_skill_parity.gd`, `test_pet_companion_parity.gd`, `test_polymorph_marble_parity.gd`, `test_polymorph_parity.gd`。 diff --git a/audit/contracts/world.npc-drop-actor-spawn.md b/audit/contracts/world.npc-drop-actor-spawn.md index ff35e19c..4dbfc559 100644 --- a/audit/contracts/world.npc-drop-actor-spawn.md +++ b/audit/contracts/world.npc-drop-actor-spawn.md @@ -85,3 +85,7 @@ ## Status `PARTIAL`。当前端已经具备 40250 实体/掉落协议的主要 native parser、VID 数据层、NPC/怪物场景层、地面物体和 shop sign 状态入口;可见性基础回归和每帧更新边界已修复,但掉落模型/特效/音效、建筑碰撞、完整 actor/ground 生命周期和 live 拾取/商店回包证据仍不足。 + +### 2026-09-22 死代码删除 + +以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`metin_spawn_event_system.gd`, `test_metin_spawn_event_parity.gd`。 diff --git a/audit/history.jsonl b/audit/history.jsonl index 08010956..094eb107 100644 --- a/audit/history.jsonl +++ b/audit/history.jsonl @@ -484,3 +484,4 @@ {"time": "2026-09-22T01:30:00Z", "event": "audit_round", "result": "PARTIAL", "ids": ["movement.keyboard.motion/canact-paralysis-faint-sleep-gate"], "files": ["audit/contracts/movement.keyboard-motion.md", "audit/manifest.json", "audit/history.jsonl"], "tests": ["40250 reference source reachability audit (exhaustive grep -arn across UserInterface/ + GameLib/ for SetParalysis/Paralysis, AFFECT_SLEEP/SetSleep, SetFaint/FaintTest, Stun/RecvStunPacket)"], "reason": "Investigated the movement.keyboard-motion.md Remaining item \"CActorInstance::CanAct() Paralysis/Faint/Sleep movement gates not wired into net_play.gd\" (flagged unfixed by the prior round). Findings: (1) IsParalysis()/m_isParalysis has exactly one setter in the whole reachable 40250 source, CInstanceBase::__Shaman_SetParalysis (InstanceBaseEffect.cpp:821-823), which itself has zero callers anywhere in the tree -- Paralysis is dead code, never true in live gameplay, so not wiring it is correct parity, not a gap. (2) IsSleep()/m_isSleep: the AFFECT_SLEEP case in SetAffect()s switch is commented out (dead); the only live setter is the AFFECT_STUN case (m_GraphicThingInstance.SetSleep(isVisible), ~InstanceBaseEffect.cpp:932-933) -- IsSleep() is functionally just an alias for AFFECT_STUN in practice, which the current clients Entity.stunned field already represents (entity_store.h:119) and net_play.gd::_can_process_network_state() already gates movement on (dead/stunned/knock_down) -- already covered, not a gap. (3) IsFaint()/m_isFaint: only setter is chrFaintTest() (PythonCharacterModule.cpp:1087-1105), a Python-bound debug helper (\"FaintTest\", registered alongside chrtestRestoreRenderMode) operating on GetSelectedInstancePtr(); the available 40250 checkout has no root/ Python UI/quest scripts to prove or disprove live-gameplay reachability -- evidence-blocked, left unresolved per SKILL.md do-not-audit-unreachable-code-as-product-behavior rule, no fix attempted. Byproduct finding while tracing IsStun(): CActorInstance::Stun() is only reachable via RecvStunPacket() (PythonNetworkStreamPhaseGame.cpp:1556-1578), which explicitly routes GC_STUN to Die() for the main character and to Stun() only for remote actors; verified this does NOT create a movement.keyboard.motion discrepancy because CanAct() blocks movement identically on IsDead() and IsStun(), and net_play.gd::_can_process_network_state() already blocks movement identically on dead and stunned -- the movement-blocking outcome is already correct parity regardless of which reference path fires. The Die()-vs-Stun() distinction only affects death-system semantics (m_isRealDead, EXP drop, corpse/respawn flow), which is combat.death.exp_drop/combat.affect_status territory and is already tracked there (combat.affect-status.md Remaining #2, equivalence rows 63/67: native GC_STUN only sets stunned=true with no dedicated clear path) -- not re-logged as a new finding to avoid duplicate tracking. No code changed this round; removed the resolved Remaining bullet from manifest.json and movement.keyboard-motion.md, replaced with investigation evidence and citations. Contract stays PARTIAL (root-motion/resource-event-source/sync-hit-death-gate-stacking/real-model-bone-flip items remain open)."} {"time": "2026-09-22T03:15:00Z", "event": "implementation_fix_round", "result": "PARTIAL", "ids": ["movement.keyboard.motion/server-speed-scale-clamp"], "files": ["project/player_controller.gd", "project/keyboard_motion_timeline_test.gd", "audit/contracts/movement.keyboard-motion.md", "audit/manifest.json", "audit/history.jsonl", "audit/remediation-roadmap.md", "audit/reports/coverage.md"], "tests": ["40250 reference source inspection (PASS: InstanceBaseMovement.cpp CInstanceBase::SetMoveSpeed(UINT uMovSpd) only guards uMovSpd>1100 -> 0; otherwise applies uMovSpd/100.0f unclamped)", "extension/src/net/entity_store.cpp EntityStore::motion_move_speed re-read (PASS: already-fixed remote-entity path mirrors the same >1100 -> 0 / mount_vnum!=0 -> 0 guards, no [0.25,3.0]-style band)", "pre-fix player_controller.gd::set_server_speed (FAIL by construction: clampf(moving_speed/100.0, 0.25, 3.0) silently floors/ceils haste or slow stacks outside the reference's 25..300 movSpd band)", "post-fix godot --headless --path project --script keyboard_motion_timeline_test.gd (PASS, incl. new set_server_speed(10/500/1200/100) assertions)", "godot --headless --path project --script test_wasd_steering_parity.gd (PASS)", "godot --headless --path project --script test_no_auto_move_regression.gd (PASS)", "godot --headless --path project --script game_camera_test.gd (PASS)", "godot --headless --path project --script movement_parity_test.gd (PASS)", "godot --headless --path project --script player_move_test.gd (PASS)", "godot --headless --path project --script mouse_controller_test.gd (PASS)", "godot --headless --path project --script test_alignment_parity.gd (PASS)", "godot --headless --path project --script netplay_test.gd (PASS; unrelated mock set_server_speed in netplay_test.gd has no clamp, unaffected)", "git diff --check (PASS)", "audit_ledger.py refresh --write/report --write/validate (PASS)"], "reason": "在同一合同下追查『根运动 vs 连续速度』假设时发现这是假线索——entity_store.cpp::advance_walk_by_motion 与本地 _process() 的位移积分在种类上同构(都是恒速直线插值);真正的差异是 set_server_speed() 把 moving_speed/100.0 塞进无参考依据的 clampf(0.25, 3.0),而参考实现 CInstanceBase::SetMoveSpeed 和本项目已修复的 EntityStore::motion_move_speed 都只有 moving_speed>1100 -> 0 这一个门。移除该私有夹钳,改为 0.0 if moving_speed > 1100 else moving_speed/100.0,两处调用方(net_world.gd、net_play.gd)均不额外夹钳,改动范围限于 player_controller.gd。新增回归断言覆盖旧边界内外和 1100 冻结门。副产品发现:CInstanceBase::SHORSE::SetMoveSpeed 是坐骑上玩家的独立速度设定,只在 IsMounting() 时生效,与远端 mount_vnum!=0 -> 0 的处理方式不同——本轮未下定论,留作独立候选。未修改根运动/AccumulationMovement 适配层、资源与网络事件完整顺序、同步/受击门叠加或真实模型测试,合同保持 PARTIAL。"} {"time": "2026-09-22T06:00:00Z", "event": "port_round", "unit": "UserInterface/PythonPlayerEventHandler.cpp", "ported": ["CPythonPlayerEventHandler::OnMove", "CPythonPlayerEventHandler::OnMoving", "CPythonPlayerEventHandler::OnStop"], "deleted": ["net_play.gd _tick_on_waiting idle FUNC_WAIT resend"], "divergent": ["CPythonPlayerEventHandler::OnWaiting (walking-but-stalled path not ported)"], "needs_live": [], "tests": ["project/netplay_test.gd PASS", "project/keyboard_motion_timeline_test.gd PASS", "project/movement_parity_test.gd PASS", "project/player_move_test.gd PASS"]} +{"time":"2026-09-22T12:00:00Z","event":"dead_code_removal","deleted_system_files":106,"deleted_tests":106,"lines_removed":36221,"manifest_refs_removed":76,"contracts_touched":18,"reason":"project/*_system.gd reachable only from their own test_*_parity.gd; not implementations of 40250 behavior (SKILL.md: unreachable code is not evidence)","tests":["full headless Godot suite: 192 PASS, 7 FAIL and 5 render tests hanging in headless, all identical on the pre-deletion tree"]} diff --git a/audit/manifest.json b/audit/manifest.json index 19b7e875..c714cc98 100644 --- a/audit/manifest.json +++ b/audit/manifest.json @@ -1414,7 +1414,6 @@ "project/ui/ground_items.gd", "project/entity_rules.gd", "project/game_scene.gd", - "project/metin_spawn_event_system.gd", "project/mob_drop_scatter.gd" ], "symbols": [ @@ -1466,7 +1465,6 @@ "project/mob_death_drop_test.gd", "project/test_mob_drop_scatter_parity.gd", "project/mob_winding_test.gd", - "project/test_metin_spawn_event_parity.gd", "build/extension/net_entity_test", "build/extension/net_classic_session_test", "build/extension/net_classic_wire_test" @@ -1478,13 +1476,14 @@ "补齐不可见 race、未知 race/vnum、模型工厂未就绪、真实 drop model/effect/sound 和建筑碰撞的失败矩阵", "验证 ground add/owner/del 与 CG_ITEM_PICKUP→GC_DEL/ITEM_GET/ITEM_UPDATE 的 live 包序、失败、乱序和重连清理", "验证 GC_SHOP_SIGN 空/非空、未知 VID、重复包、私人商店 UI 和换图/断线清理", - "把本地掉落散落、活动石头和 owner 保护与 40250 client presentation 分层,不把 custom authority 当 parity" + "把本地掉落散落、活动石头和 owner 保护与 40250 client presentation 分层,不把 custom authority 当 parity", + "2026-09-22 删除了只被自身测试引用的 *_system.gd 及其 test_*_parity.gd;原来引用它们的实现/证据不再成立,需按 40250 源码单元重新移植" ], "notes": "已完成 40250 actor/ground/shop-sign 调用链和当前端 parser/store/view 对照;合同保持 PARTIAL。net_world_vis_test 复现 4 项可见性生命周期失败,本轮仅登记审计结果,未修改实现。", "fingerprints": { "reference": "97dfe247272a6098ce623ceae986732205de3273cb1199377f969faeec8d0ba0", - "implementation": "3a7d87cad0ba405495ba51e13feab294af9ebddbde2fb18b5df9c606bbe3af08", - "tests": "0100c7c3894330afa222f7a7d6bc60ec38ca08fc981a6d30746deffa5e159298" + "implementation": "bdc38d8ef47e8a8f2676f6e9a080c7924a56b88ceb546affeb281dc57243a1d1", + "tests": "702ac283f682d937fa8a02c70ca929942b68cacffc7adc686a5779e45f10f178" } }, { @@ -1936,8 +1935,7 @@ "project/damage_effect.gd", "project/ui/player_view.gd", "project/ui/equip_model.gd", - "project/ui/weapon_trace.gd", - "project/socket_glow_fx_system.gd" + "project/ui/weapon_trace.gd" ], "symbols": [] }, @@ -1964,12 +1962,13 @@ "服务端伤害/死亡/重复命中与客户端命中窗的真实包级 fixture", "换图、断线、缺失动作资源和攻击队列清理测试", "WeaponTrace 已按 bound-box 最大长度、局部 Z 和 reach scale 接入 PlayerView;当前仍未证明 native bone matrix 的同一 composite/rotation 坐标语义、远端/怪物覆盖、真实刀光纹理视觉和服务端动作资源", - "socket_glow_fx_system.has_trail 目前只有配置字段,没有进入武器刀光渲染链" + "socket_glow_fx_system.has_trail 目前只有配置字段,没有进入武器刀光渲染链", + "2026-09-22 删除了只被自身测试引用的 *_system.gd 及其 test_*_parity.gd;原来引用它们的实现/证据不再成立,需按 40250 源码单元重新移植" ], "notes": "已按 40250 isValidAttacking/HitDataContainer 删除缺失动作攻击数据、空命中窗和缺少 .msm 防御球时的本地合成命中;状态包、命中窗、去重、受击、弓箭 FLY、本地主角伤害飘字和 WeaponTrace 多部件生命周期有回归证据。WeaponTrace 目前通过 weapon_trace.gd 适配层接入,包含武器类型过滤、每个 Weapon/Shield 部件独立 trace、双轨迹时间队列、cubic spline triangle strip、alpha/texture 材质、攻击动作开关、纹理控制入口、bound-box 最大长度/局部 Z/reach scale 和换装清理;AFFECT_GEOMGYEONG 的 1.5/1.0 reach scale 已同时接入刀光与普通攻击扫掠。native 骨骼矩阵坐标语义、远端/怪物覆盖、真实视觉资源及服务端伤害闭环仍不等价。强化配置中的 has_trail 仍是独立未消费字段。", "fingerprints": { "reference": "abf0577e3430979fbf4e1ea85fa637b2f59e798bf57e257060528940045eef43", - "implementation": "b835d862fb211a964fb36df0d435247ad120636a81491e9e108b2c545710bba9", + "implementation": "6f93c4bcb3aee4af48beee7cff2eb4c5923a3b0bb32eae0f2b132f2873524e8f", "tests": "228e1bb5ebb0ef52822956e3d33d9c51d55cc2090c323300b466f937249e5d3c" } }, @@ -2013,8 +2012,7 @@ "project/ui/mouse_controller.gd", "project/player_controller.gd", "project/net_play.gd", - "project/net_world.gd", - "project/alignment_pk_system.gd" + "project/net_world.gd" ], "symbols": [ "EntityRules.is_attackable_instance", @@ -2053,9 +2051,7 @@ "tests": [ "project/entity_rules_test.gd", "project/target_board_test.gd", - "project/test_alignment_pk_parity.gd", "project/test_click_target_effect_parity.gd", - "project/test_auto_combat_bot_parity.gd", "project/playable_combat_test.gd", "project/netplay_test.gd" ], @@ -2066,13 +2062,14 @@ "隐身、复活隐身、视锥、门死亡、裁剪/淡出和迟到实体更新的目标生命周期矩阵", "地图 ATTRIBUTE_BANPK 与实体 in_safe 的实时数据权威和移动/换图更新链", "SetTarget 切换时间窗、OnTargeted/OnUntargeted、飞行目标和 CG_TARGET 顺序", - "真实服务端目标包、死亡/删除/乱序/断线重连和目标框清理" + "真实服务端目标包、死亡/删除/乱序/断线重连和目标框清理", + "2026-09-22 删除了只被自身测试引用的 *_system.gd 及其 test_*_parity.gd;原来引用它们的实现/证据不再成立,需按 40250 源码单元重新移植" ], "notes": "已完成目标攻击资格、目标可选中、目标框和安全区的逐分支静态审计;目标入口已统一接入 CanChangeTarget/CanPickInstance、重复目标窗口和 CG_TARGET(0) 清理,合同保持 PARTIAL。", "fingerprints": { "reference": "d1c911b8bba1ccd48228a794568e3bc13ffa77ecd7d124d4e442ed9fecfdfb85", - "implementation": "9d68120ea53f8da8821803c91ea5065ce208cfd14e0d48b1e1239b39895f9e25", - "tests": "2cf38a10e34184c9477f0e7fc7316b6ba201c0adaa4db43a0ce8cb7d6936c1a4" + "implementation": "47ddc51bc30196640a8e27ac2286405bc6bd5fc2a25d80b636884382fc0d468a", + "tests": "3b20fa82a05fdf13597b631c6463f15d6a303dd7eddf43ed6f65d3e873bce756" } }, { @@ -2218,7 +2215,6 @@ }, "implementation": { "files": [ - "project/combat_status_effect_system.gd", "project/ui/hit_reaction.gd", "project/ui/player_view.gd", "project/ui/mob_view.gd", @@ -2262,10 +2258,8 @@ "evidence": { "contract": "audit/contracts/combat.affect-status.md", "tests": [ - "project/test_combat_status_effect_parity.gd", "project/damage_effect_test.gd", "project/combat_fx_test.gd", - "project/test_mob_combat_reaction_parity.gd", "project/test_combat_death_pickup_entry_parity.gd", "project/hit_view_test.gd", "build/extension/net_entity_test", @@ -2278,13 +2272,14 @@ "补齐 affect bit 到附着特效、图标、骨骼、stone smoke 和建筑 no-op 的逐位资源矩阵", "统一远端 stunned/knockdown/skill-cancel 的动作和网络状态门控", "修复 hit_view_test 前向 damage 资源/动作锁时序失败", - "补齐重复、乱序、duration、死亡、换图、重连和 VID 重用清理测试" + "补齐重复、乱序、duration、死亡、换图、重连和 VID 重用清理测试", + "2026-09-22 删除了只被自身测试引用的 *_system.gd 及其 test_*_parity.gd;原来引用它们的实现/证据不再成立,需按 40250 源码单元重新移植" ], "notes": "已完成 40250 AffectFlagContainer、Actor affect/受击动作、damage/affect 协议和当前 native/GDScript 链的逐分支审计;确认本地状态结算器的数据权威错误,且 hit_view_test 有 1 项失败。合同保持 PARTIAL,本轮仅登记审计结果,未修改实现。", "fingerprints": { "reference": "92ddbb559ef4966bcecc9358beb85f7fd8edec830f833cde7067f0c35eaa2aee", - "implementation": "c783877a1a45419d9c8fd143e7dc3ce5cd37838e6c044941d36260d586249386", - "tests": "f0e1f4279d7fed8a897eccd9d0978781e4f19c27765d4571138bd0c0f6faefce" + "implementation": "0012356c81b291e8bff2f96561efad74ac45ba624c91986ce53309b25abf76cc", + "tests": "1c7ab5e0776d4e144a37d2d2e659c02eb259aa0b5caec4111b50b673ef39b112" } }, { @@ -2375,7 +2370,6 @@ "project/netplay_test.gd", "project/test_skill_inventory_parity.gd", "project/test_taskbar_quickslot_parity.gd", - "project/test_status_skill_reset_parity.gd", "project/mobile_gesture_test.gd", "project/playable_combat_test.gd", "project/guild_war_skill_test.gd", @@ -2389,13 +2383,14 @@ "CanUseSkill 五类动作门已补齐;仍需把真实 shape/图形动作字段和拒绝副作用接入端到端回归", "POINT_CASTING_SPEED 已暴露并按 40250 __RunCoolTime 修正;仍需真实点变更/冷却回包时序 fixture", "补齐 skill/group/quickslot/cooldown-end 乱序、重复、断线、重连和换图测试", - "建立全部技能 ID/grade/.msk 公式/动作/目标追加和服务端 damage 顺序快照" + "建立全部技能 ID/grade/.msk 公式/动作/目标追加和服务端 damage 顺序快照", + "2026-09-22 删除了只被自身测试引用的 *_system.gd 及其 test_*_parity.gd;原来引用它们的实现/证据不再成立,需按 40250 源码单元重新移植" ], "notes": "已完成 40250 技能数据、三层施放校验、CanUseSkill、casting-speed 冷却公式、快捷栏、普通/公会技能发送入口、skill/group/cooldown 协议和 FUNC_SKILL→CG_USE_SKILL 动作/意图包序的逐分支审计;CanUseSkill 五类动作门、POINT_CASTING_SPEED 桥接和公会技独立路径已实现,但真实拒绝/乱序/跨生命周期 fixture、完整动作资源包序和服务端 damage 顺序仍使合同保持 PARTIAL。", "fingerprints": { "reference": "c38e930585edcd35a8e0911f10f3fb6694a39bc7735a3bac0b3d4eea157c9bcb", "implementation": "233c5cd7ee94ba2fa9ecb1a833a3ecad3e805ade4a87333243480199b18e7d4b", - "tests": "82695ce797fd9e07f6278d8beb3cab35441e8cd59be43e2be6920236d3736369" + "tests": "e3b00c87a54d1af4effeef310d90de4c72dacf7a7323bab005f6850ace556bb5" } }, { @@ -2567,7 +2562,6 @@ "implementation": { "files": [ "project/consumable_system.gd", - "project/item_split_system.gd", "project/ui/quickbar.gd", "project/ui/inventory_ui.gd", "project/ui/shop_ui.gd", @@ -2578,7 +2572,6 @@ "extension/src/net/entity_store.cpp", "extension/tests/net_classic_session_test.cpp", "project/test_consumables_parity.gd", - "project/test_item_split_parity.gd", "project/test_taskbar_quickslot_parity.gd", "project/test_skill_inventory_parity.gd", "project/skill_test.gd", @@ -2632,7 +2625,6 @@ "contract": "audit/contracts/item.consumable-action.md", "tests": [ "project/test_consumables_parity.gd", - "project/test_item_split_parity.gd", "project/test_taskbar_quickslot_parity.gd", "project/complex_item_drop_test.gd", "project/netplay_test.gd" @@ -2645,13 +2637,14 @@ "堆叠拆分已统一为 CG_ITEM_MOVE 请求并等待服务器确认;快捷出售已统一为 CG_SHOP SELL/SELL2,价格、金币和物品变化仍待真实回包 fixture", "消耗品 native action gate 与装备类型门已补齐;物品音效和 SendSequence 的资源/包序证据仍缺", "断线、换图、重复回包、请求失败和延迟期间的状态清理/回滚", - "快捷栏乐观更新和页面取模差异已修复;quickbar 资源泄漏、完整生命周期清理和真实服务器确认 fixture 仍待补齐;complex_item_drop_test 的购买模式卖出目标判定已修复" + "快捷栏乐观更新和页面取模差异已修复;quickbar 资源泄漏、完整生命周期清理和真实服务器确认 fixture 仍待补齐;complex_item_drop_test 的购买模式卖出目标判定已修复", + "2026-09-22 删除了只被自身测试引用的 *_system.gd 及其 test_*_parity.gd;原来引用它们的实现/证据不再成立,需按 40250 源码单元重新移植" ], "notes": "已将拆分、快捷出售和快捷栏增删交换统一到 request→server/GC 确认路径,并修复购买模式误发出售请求;快捷栏使用、消耗品真实反馈包序、音效/sequence 和断线清理仍使合同保持 PARTIAL。", "fingerprints": { "reference": "f364d4bbba79ef514f5f8aeccbf34a3d6adea8fcacf4cb82664476b6c29ebbfe", - "implementation": "468b4155a11204d546d49ca92afea109ced58669c2ac2286f92c742f538a49e7", - "tests": "5fe55c9e41140a0d3deb270df8cecb785d45d8eed3e6769d5a24b907d772e66c" + "implementation": "20983090cb18e010bcd3fd674166491d589f32335253d4b19241c556431cf3b2", + "tests": "360f28348ea118717464fd3ebe179791a95377b658f60364ff7286a041059d97" } }, { @@ -2815,13 +2808,7 @@ "implementation": { "files": [ "project/ui/refine_ui.gd", - "project/refine_effect_system.gd", "project/item_attr_system.gd", - "project/item_enchant_system.gd", - "project/jewelry_socket_system.gd", - "project/costume_system.gd", - "project/costume_attr_transfer_system.gd", - "project/wardrobe_transmog_system.gd", "project/ui/equip_model.gd", "extension/src/net/m2_client.cpp", "extension/src/net/m2_client.h", @@ -2867,13 +2854,7 @@ "contract": "audit/contracts/item.refine-socket-costume.md", "tests": [ "project/test_refine_parity.gd", - "project/test_refine_effect_parity.gd", "project/test_item_attr_parity.gd", - "project/test_item_enchant_manager_parity.gd", - "project/test_jewelry_socket_parity.gd", - "project/test_costume_system_parity.gd", - "project/test_costume_attr_transfer_parity.gd", - "project/test_wardrobe_transmog_parity.gd", "extension/tests/net_classic_session_test.cpp", "extension/tests/net_entity_test.cpp", "extension/tests/net_bounds_test.cpp" @@ -2887,13 +2868,14 @@ "把时装穿戴、属性、过期和 bonus 接入服务端 item/affect/point 与 actor armor/weapon/hair 更新链", "让强化等级/socket/type/subtype 驱动实际装备模型、强化特效 attach/clear,并覆盖资源缺失清理", "补充拒绝、断线、重复/乱序回包、换图、过期和服务端回滚测试", - "将 wardrobe/transmog 自定义扩展与 40250 parity 分开标注,除非补齐对应服务端协议和角色部件同步" + "将 wardrobe/transmog 自定义扩展与 40250 parity 分开标注,除非补齐对应服务端协议和角色部件同步", + "2026-09-22 删除了只被自身测试引用的 *_system.gd 及其 test_*_parity.gd;原来引用它们的实现/证据不再成立,需按 40250 源码单元重新移植" ], "notes": "本轮已修复 M2Client::refine 对 40250 取消 sentinel(255,255) 的错误拦截,并补充边界/EntityStore 回归;属性、socket、时装和幻化仍绕过服务器权威回包,合同保持 PARTIAL。", "fingerprints": { "reference": "6cc67309be64986008984215add763db78e2990b665274500cf4e76661b782e8", - "implementation": "176ad923087d84b8f11dee97d2bd198c495d91014d8b720d3bf58acd0bd26fb8", - "tests": "6bb13ce9e81271dfa30eaaab03c5cf5e801ef22965c32d4b4d38af95f6874bfb" + "implementation": "0dceaccac8bd37f946846dfd7bebf7cb38563896dc2d24ec7eed3560854e2b31", + "tests": "785b8529135d9169573531c379e0997d61a61acfffb40dbf1bc8f715ee3813de" } }, { @@ -2935,8 +2917,6 @@ }, "implementation": { "files": [ - "project/npc_shop_system.gd", - "project/private_shop_system.gd", "project/ui/shop_ui.gd", "project/ui/private_shop_ui.gd", "project/net_play.gd", @@ -2983,11 +2963,9 @@ "evidence": { "contract": "audit/contracts/npc.dialog-shop.md", "tests": [ - "project/test_npc_shop_parity.gd", "project/test_npc_shop_flow.gd", "project/test_npc_dialog_shop_ui_parity.gd", "project/private_shop_ui_test.gd", - "project/test_private_shop_bundle_parity.gd", "project/shop_rules_test.gd", "project/p8_test.gd", "project/complex_item_drop_test.gd", @@ -3004,13 +2982,14 @@ "为买卖请求补齐动作门控、实际库存/anti flag/商店状态校验和服务器结果回写", "移除 live 路径对硬编码 NPC 库存与本地金币/背包 helper 的权威依赖", "补充 START/START_EX/UPDATE/ERROR/END、拒绝、售罄、断线、重连和乱序包的端到端测试", - "补充个人商店死亡/骑乘/战斗/bundle/重复源格校验和服务端拒绝清理测试" + "补充个人商店死亡/骑乘/战斗/bundle/重复源格校验和服务端拒绝清理测试", + "2026-09-22 删除了只被自身测试引用的 *_system.gd 及其 test_*_parity.gd;原来引用它们的实现/证据不再成立,需按 40250 源码单元重新移植" ], "notes": "已完成 40250 与当前端的 NPC 点击、脚本对话、普通/多货架商店、买卖、个人商店招牌和摆摊提交逐分支/协议/状态审计。协议骨架和本地 UI 测试存在,但多货架更新、个人商店点击/状态、动作门控及本地直改旁路使合同保持 PARTIAL。", "fingerprints": { "reference": "2fa18ff8979d5c03e2daf44129702e3a1b26bb9e6f7b3b585f6fa55ac898e765", - "implementation": "5373e00e8785a5fa8a98f8c361fabc4b1cdc3625093811d9a74082957126db77", - "tests": "3829ff07408c019b3a4a65fa8d4f283fe723342bcc2d1ed51ec4993d7f9c1874" + "implementation": "4adc63160de3b1b21f51c11d6c512d7f550c976a50c49be46784c967eca574ca", + "tests": "c4a51e1a265294adaf10693271297d832ba2cc7ef6de6e6125bded961cea7594" } }, { @@ -3109,7 +3088,6 @@ "project/guild_mark_tail_test.gd", "project/test_whisper_chat_parity.gd", "project/chat_mobile_ui_test.gd", - "project/test_chat_hyperlink_dice_parity.gd", "build/extension/net_entity_test", "build/extension/net_classic_session_test" ], @@ -3122,13 +3100,14 @@ "复现入站表情解析、跨帝国文字转换、辱骂过滤和 40250 延迟聊天队列", "统一公共聊天颜色和文本前缀处理,发送失败不能生成成功回显", "让聊天/信息尾标使用同一权威实体生命周期,补齐真实字体度量、屏幕投影和死亡/换图清理", - "补充重复/乱序/断线/重连、未知类型、超长文本和 whisper 错误端到端测试" + "补充重复/乱序/断线/重连、未知类型、超长文本和 whisper 错误端到端测试", + "2026-09-22 删除了只被自身测试引用的 *_system.gd 及其 test_*_parity.gd;原来引用它们的实现/证据不再成立,需按 40250 源码单元重新移植" ], "notes": "已完成 40250 与当前端的公共聊天、私聊、忽略、聊天气泡、名字/称号/等级/公会名尾标及文本布局逐分支/协议/状态审计。已修正 whisper mode 映射、截断 GC_WHISPER 拒绝逻辑,并将 live ChatUI 的普通/GM whisper 接入独立会话和 ignore 判断;基础网络与静态尾标测试存在,但窗口通知、公共提示/过滤/表情/颜色和 Godot 字体布局差异使合同保持 PARTIAL。", "fingerprints": { "reference": "2b70c64e18344d4f465354239899289d5df2f7b9c38e165c66d553f790e2859e", "implementation": "cd02d44596b377534a4200e0ef7002b3c6de8e7c328fbdd96afc7e9ed8a6cb8f", - "tests": "47305f945c76eb3c208880fd4860775262489bc2dc5dd7d42eb3d136e28761ab" + "tests": "83f508464e76c7b6eee0796af45dedabeea3fbe2a2c235ad5efbad753c39842e" } }, { @@ -3175,12 +3154,6 @@ }, "implementation": { "files": [ - "project/party_management_system.gd", - "project/guild_skill_system.gd", - "project/guild_war_system.gd", - "project/guild_war_observer_arena_system.gd", - "project/messenger_system.gd", - "project/guild_mark_uploader_system.gd", "project/ui/party_ui.gd", "project/ui/guild_ui.gd", "project/ui/friend_ui.gd", @@ -3241,16 +3214,9 @@ "evidence": { "contract": "audit/contracts/social.party-guild-messenger.md", "tests": [ - "project/test_party_management_parity.gd", - "project/test_messenger_parity.gd", - "project/test_guild_mark_uploader_parity.gd", "project/guild_mark_test.gd", "project/guild_mark_tail_test.gd", - "project/test_guild_war_parity.gd", - "project/test_guild_war_observer_arena_parity.gd", - "project/test_guild_skill_parity.gd", "project/guild_creation_ui_test.gd", - "project/test_privilege_empire_guild_parity.gd", "project/p8_test.gd", "extension/tests/net_entity_test.cpp", "extension/tests/net_classic_session_test.cpp" @@ -3264,13 +3230,14 @@ "补充公会权限拒绝、离会/踢人、公告/技能/金钱回包和邀请清理", "补充公会战声明/开始/结束/list/point 的连续事件与服务端奖励验证", "补充独立 mark client 的握手、index、CRC、压缩 block、损坏、断线、重试和角色刷新测试", - "上传会徽/战旗必须验证真实用户资源、权限和服务端完成回包,避免默认占位图成为成功状态" + "上传会徽/战旗必须验证真实用户资源、权限和服务端完成回包,避免默认占位图成为成功状态", + "2026-09-22 删除了只被自身测试引用的 *_system.gd 及其 test_*_parity.gd;原来引用它们的实现/证据不再成立,需按 40250 源码单元重新移植" ], "notes": "已完成 40250 与当前端的组队、公会、好友/Messenger、会徽/战旗及相关 UI/native 回包链审计。live native 主路径较完整,但本地队伍/技能/公会战/Messenger/会徽 helper 仍存在直接状态旁路,真实权限拒绝、mark 独立连接和重连清理证据不足,合同保持 PARTIAL。", "fingerprints": { "reference": "c0ba3dce2da7c2bd6c1f934ebd66eedaf0d37bc0d3bf99b22e401b939a150bc4", - "implementation": "bf62ad6be63031d6630d7f4d1be497469f4a42ea8137be95b066864cd566f55c", - "tests": "89f4d155ee7d3b4f3457ef2114aed642ef6030a7a7c226d5731fcc20cd2ba2dd" + "implementation": "0be944c07b20317ca684d3219d86421e4e8a34e3610daf587ca0c1fb7671a660", + "tests": "0377c649a21cdd049d7a76e9b241ed11eba0e16ba057f7cec27f583702c2cf94" } }, { @@ -3311,8 +3278,6 @@ }, "implementation": { "files": [ - "project/player_trade_exchange_system.gd", - "project/safebox_storage_keeper_system.gd", "project/ui/exchange_ui.gd", "project/ui/safebox_ui.gd", "project/ui/mall_ui.gd", @@ -3356,18 +3321,19 @@ "evidence": { "contract": "audit/contracts/social.exchange-safebox.md", "tests": [], - "last_test_result": "PARTIAL" + "last_test_result": "NOT_RUN" }, "remaining": [ "补齐 GC_EXCHANGE/GC_SAFEBOX/GC_MALL 的拒绝、乱序、断线和回包回滚测试", "将交易和仓库 UI 的 optimistic 状态改为由 native 回包重建并验证源位置", "区分并隔离本地 helper,避免将交易结算、密码、库存和金条规则当作客户端 authority", - "补齐 exchange 动作门控、真实服务端物品/金币校验和商城 checkout 失败清理" + "补齐 exchange 动作门控、真实服务端物品/金币校验和商城 checkout 失败清理", + "2026-09-22 删除了只被自身测试引用的 *_system.gd 及其 test_*_parity.gd;原来引用它们的实现/证据不再成立,需按 40250 源码单元重新移植" ], "notes": "已完成交易、仓库、安全箱、商城和金币确认的逐分支/协议/状态审计;native 主链存在但与 40250 仍是 PARTIAL,本轮未修改实现。", "fingerprints": { "reference": "8497886941be7e41b6b1960b22fc8e07b1c0fe65c4a032b13d662191b06a0404", - "implementation": "d2cad2b8bdf7fc421478157ea0085bef07a4979d3d3799ff81823e1dd16216ab", + "implementation": "23a59fe47d21a602f5baff19828f9db8f4bd89495bcd1a14922a7476ee5906a2", "tests": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } }, @@ -3417,18 +3383,13 @@ "project/ui/char_status_ui.gd", "project/ui/atlas_ui.gd", "project/ui/minimap.gd", - "project/atlas_navigation_system.gd", "project/game_scene.gd", "extension/src/net/m2_client.cpp", "extension/src/net/m2_client.h", "extension/src/net/entity_store.cpp", "extension/src/net/entity_store.h", "extension/src/net/classic/classic_session.cpp", - "extension/src/net/classic/classic_session.h", - "project/biologist_quest_system.gd", - "project/dynamic_bounty_event_system.gd", - "project/event_manager_system.gd", - "project/metin_spawn_event_system.gd" + "extension/src/net/classic/classic_session.h" ], "symbols": [ "EntityStore::apply", @@ -3477,12 +3438,13 @@ "补齐真实 server 脚本序列与中断回归;script-button、DONE、ESC/CANCEL answer 语义已闭合", "验证 RUN_CINEMA 的有效 .msc 资源序列、过场消费者和中断清理;验证地图信号在 Minimap/Atlas 的切图与多点生命周期", "补齐对话确认超时、fade/camera/cinema 中断、断线和重连清理测试", - "隔离生物学家/悬赏/活动 helper,避免其本地规则冒充 40250 客户端实现" + "隔离生物学家/悬赏/活动 helper,避免其本地规则冒充 40250 客户端实现", + "2026-09-22 删除了只被自身测试引用的 *_system.gd 及其 test_*_parity.gd;原来引用它们的实现/证据不再成立,需按 40250 源码单元重新移植" ], "notes": "已完成任务状态、事件脚本、对话、HUD、地图导航和过场链的逐分支/协议/时序审计;GC_QUEST_INFO END、script-button 任务索引 sentinel、DONE close-only/DoneEvent、ESC/CANCEL answer 分支和 RUN_CINEMA 缺失资源分支已按 40250 修复,其他明确差异继续保持 PARTIAL。", "fingerprints": { "reference": "e05974ea414ca0d1bdcec3298119380ec35bfd20637a9c8e48588246347fc364", - "implementation": "5aa1707764f5a74db510210bfd8803a9e90e0a4d989202795fe506d1f29a8395", + "implementation": "71343d79bbcf40cf9437adac21bc5d6cef35b568e390445f5824ad9bba35e28f", "tests": "a65c31ca4ade9fe4359f1238ed7080a5a50c3a336b75a9d9ee2a1e2f7faeb02a" } }, @@ -3518,12 +3480,6 @@ "project/dungeon_state.gd", "project/ui/dungeon_result_ui.gd", "project/game_scene.gd", - "project/world_boss_system.gd", - "project/dungeon_ranking_system.gd", - "project/spider_dungeon_system.gd", - "project/spider_dungeon_key_system.gd", - "project/flame_dungeon_razador_system.gd", - "project/endless_spire_dungeon_system.gd", "extension/src/net/entity_store.cpp", "extension/src/net/entity_store.h", "extension/src/net/m2_client.cpp", @@ -3555,18 +3511,19 @@ "evidence": { "contract": "audit/contracts/world.dungeon-activity.md", "tests": [], - "last_test_result": "PARTIAL" + "last_test_result": "NOT_RUN" }, "remaining": [ "接通 alarm_requested 到等价的 compass/mse 资源效果,并验证方向、初次提示和 20 秒重复提示", "补齐 GC_DUNGEON/DUNGEON_RESULT 的 live 回包、重复/乱序、拒绝、断线、切图和重连测试", "确认 server cm 到 Godot 世界坐标的轴向、地图基准和到达阈值实机一致", - "把 Boss、排行和具体副本 helper 与 40250 client parity 分开,禁止本地 authority 冒充客户端等价实现" + "把 Boss、排行和具体副本 helper 与 40250 client parity 分开,禁止本地 authority 冒充客户端等价实现", + "2026-09-22 删除了只被自身测试引用的 *_system.gd 及其 test_*_parity.gd;原来引用它们的实现/证据不再成立,需按 40250 源码单元重新移植" ], "notes": "已完成 GC_DUNGEON 目标点、结果窗口、地图事件及本地 Boss/排行/副本 helper 的逐分支、协议、资源和时序审计;目标状态算法接近参考,但 compass 副作用未接通,本地 authority 不能视为 40250 客户端实现。", "fingerprints": { "reference": "66b5e63a273e45873bf4c4b90c8b658a9f073ba425de6f275d622b3c47fbac99", - "implementation": "c4885c44a33a107d3095a20b988f26a5a62d50814514e34ffb03c86bfa7a745d", + "implementation": "f962e7e5f470f447024073009b195aacf877cfbcbf624aeb88cf13f9e3646dc0", "tests": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } }, @@ -3605,10 +3562,6 @@ }, "implementation": { "files": [ - "project/fishing_mining_system.gd", - "project/fishing_rod_reel_system.gd", - "project/fishing_cooking_camp_system.gd", - "project/mining_refinery_smelt_system.gd", "project/net_play.gd", "project/net_world.gd", "extension/src/metin2_world.cpp", @@ -3649,18 +3602,19 @@ "evidence": { "contract": "audit/contracts/world.fishing-mining.md", "tests": [], - "last_test_result": "PARTIAL" + "last_test_result": "NOT_RUN" }, "remaining": [ "补齐 CanFishing/持竿/持镐动作门控、服务端拒绝和角色消失/重连测试", "验证 GetFishingRot 的轴向、MapCoord heading 和全地图 water attribute/水高单位", "让 GC_DIG_MOTION 按 count 复现多个 dig motion,并验证目标缺失、动作完成和中断清理", - "把钓鱼收益、烹饪、营火、采矿、熔炼和宝石脚本标记为 custom/server authority,禁止冒充 40250 client parity" + "把钓鱼收益、烹饪、营火、采矿、熔炼和宝石脚本标记为 custom/server authority,禁止冒充 40250 client parity", + "2026-09-22 删除了只被自身测试引用的 *_system.gd 及其 test_*_parity.gd;原来引用它们的实现/证据不再成立,需按 40250 源码单元重新移植" ], "notes": "已完成钓鱼输入/水面扫描、GC_FISHING、GC_DIG_MOTION、地图水面和当前端生活技能 helper 的逐分支、协议、状态与资源审计;协议骨架和部分算法接近参考,但 live 动作/门控/清理证据不足,本地收益脚本不是 40250 客户端 authority。", "fingerprints": { "reference": "770f14f2d2bfdf025b471dbc90280f891671ea898faef52d4112684934f5bdb7", - "implementation": "6be8a56d9630a7c8aa785ff2981dd46ed5a0b08b3fa097a20f55a1a4d31d768f", + "implementation": "e9c3c25e1e61db656451f5a091ecf1365f54f01ee7a94b8098f5e625d67dded8", "tests": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } }, @@ -3697,14 +3651,6 @@ }, "implementation": { "files": [ - "project/horse_system.gd", - "project/horse_combat_system.gd", - "project/horse_level_growth_system.gd", - "project/pet_companion_system.gd", - "project/classic_pet_system.gd", - "project/polymorph_system.gd", - "project/polymorph_marble_system.gd", - "project/mounted_combat_skill_system.gd", "project/net_play.gd", "project/net_world.gd", "project/ui/equip_model.gd", @@ -3740,18 +3686,19 @@ "evidence": { "contract": "audit/contracts/world.mount-pet-polymorph.md", "tests": [], - "last_test_result": "PARTIAL" + "last_test_result": "NOT_RUN" }, "remaining": [ "补齐 mount_vnum/GC_MOUNT 到实际 horse actor/model attach/detach 的 live 链和位置清理", "验证骑乘 motion root、300/1200 度旋转速度、马等级攻击门和 weapon mode 的 live 回包时序", "把变身 affect/shape/装备遮挡接入 native entity 或明确标为缺失,不用本地 helper 代替", - "将宠物、马匹成长/耐力/马上技能和伤害脚本与 40250 client parity 分离,禁止 custom authority 冒充等价实现" + "将宠物、马匹成长/耐力/马上技能和伤害脚本与 40250 client parity 分离,禁止 custom authority 冒充等价实现", + "2026-09-22 删除了只被自身测试引用的 *_system.gd 及其 test_*_parity.gd;原来引用它们的实现/证据不再成立,需按 40250 源码单元重新移植" ], "notes": "已完成 mount_vnum/GC_MOUNT、骑乘动作/攻击门、变身和当前端宠物/马匹 helper 的逐分支、协议、资源和时序审计;网络字段与部分门控存在,但实际坐骑模型挂接和变身 native 闭环缺失,本地宠物/成长/技能规则不是 40250 客户端 authority。", "fingerprints": { "reference": "5ef243a1ff134dfc8afff02136d104daf0a0ac12eca781a1c476b53f7da9ff19", - "implementation": "47780fc336fe531fea973aa9196d587b581aabb2a8dfe00bd10a6d275ce98b4a", + "implementation": "e69e964433af7177390e558ef32087425386bf6923a9408d457c48644c4fc6ad", "tests": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } }, @@ -4064,7 +4011,6 @@ "formats/environment.cpp", "formats/environment.h", "project/game_scene.gd", - "project/weather_daynight_system.gd", "project/fx/weather.gd" ], "symbols": [ @@ -4099,7 +4045,6 @@ "project/environment_test.gd", "project/skybox_test.gd", "project/p9_test.gd", - "project/test_weather_daynight_parity.gd", "extension/tests/net_entity_test.cpp", "project/gamescene_test.gd", "project/netbridge_test.gd" @@ -4111,13 +4056,14 @@ "修正/验证 40250 线性 fog near/far/color 与 HTP/STP/terrain 的参数和时序", "补齐 DayMode/PRESERVE_DayMode 正式环境切换;WeatherDayNightSystem 的自动地图预设继续隔离,不能替代服务器 xmas_snow 命令", "对天空 transition、云 plane、SnowEnvironment 池和真实渲染顺序增加参数/screenshot oracle", - "增加重复加载、快速切图、暂停恢复、资源缺失和环境/粒子清理测试" + "增加重复加载、快速切图、暂停恢复、资源缺失和环境/粒子清理测试", + "2026-09-22 删除了只被自身测试引用的 *_system.gd 及其 test_*_parity.gd;原来引用它们的实现/证据不再成立,需按 40250 源码单元重新移植" ], "notes": "已完成 .msenv、天空/云、双方向光、fog、天气粒子、服务器 xmas_snow 命令和正式场景过滤的逐分支审计;命令链与四张排除地图已有回归,但 SnowEnvironment 池、DayMode、filter/lens flare/wind、线性 fog 仍与 40250 不一致或缺失,故保持 PARTIAL。", "fingerprints": { "reference": "1cffc55eb356a522049e4f7bc36595ea968ad1e0ccd7780486064a3e250cc36b", - "implementation": "dfc0fe4b13645b06daeea26f517d813a8c8ebb4cfcb2db19ebe2ee7e8f573c7c", - "tests": "05fd8d3202d9774f7043311e8615e1210a09cc579f1346eb2fb9ebbfec339b5b" + "implementation": "81e6a81489baf9f90e830458212910db6feaa713508d173236d2d255446aae53", + "tests": "fa1ee00144332c33a93da2f94371a0b3602c46271356aa78b243d782aaa71c65" } }, { @@ -4502,7 +4448,6 @@ "project/audio_miles_test.gd", "project/ambience_test.gd", "project/bgm_test.gd", - "project/test_bgm_jukebox_parity.gd", "project/app_flow_lifecycle_test.gd", "project/system_option_ui_test.gd", "project/rendering_scenario_test.gd" @@ -4513,13 +4458,14 @@ "验证真实 3D 播放实例不会绕过 master SFX,并补设备/实例路径的后台静音回归", "验证 BGM 多实例淡入/限幅淡出/全量淡出/加载失败回滚状态机", "验证 3D attenuation、实例池耗尽、远端动作音效和完整销毁顺序", - "验证流式地图快速切换、环境声来源删除和退出时无悬挂播放器" + "验证流式地图快速切换、环境声来源删除和退出时无悬挂播放器", + "2026-09-22 删除了只被自身测试引用的 *_system.gd 及其 test_*_parity.gd;原来引用它们的实现/证据不再成立,需按 40250 源码单元重新移植" ], "notes": "已完成首轮逐分支审计;基础音量/监听器/环境声/动作音效路径存在,后台 BGM/SFX 同时静音恢复与退出 Audio 缓存清理已有回归,但 Miles 实例池和 BGM 逐帧状态机仍不等价。", "fingerprints": { "reference": "1a8a9232bcc63724814d15df99f211ddfe67a28fcb2052bb4c84ad79373bdfef", "implementation": "f4c3fed1f7cbf08c1032c536927a689a9a1b71b1c431de2afb3c0041ee577315", - "tests": "a7ca9e4555b71f92776b56669a0b74e443dc2aaab78a2f208c86f9fc4bc75bfc" + "tests": "96a0171f670842fc52b665ae9fcae3cb30ef0046ef147836ca7f586e21646aab" } }, { @@ -4618,13 +4564,7 @@ "symbols": [] }, "implementation": { - "files": [ - "project/offline_bazaar_market_system.gd", - "project/mail_system.gd", - "project/dynamic_bounty_event_system.gd", - "project/dragon_soul_system.gd", - "project/solo_guild_domain_system.gd" - ], + "files": [], "symbols": [] }, "equivalence": {}, @@ -4635,11 +4575,11 @@ "last_test_result": "NOT_RUN" }, "remaining": [], - "notes": "完整功能清单首批合同;尚未完成 40250 逐分支等价审计。", + "notes": "2026-09-22 所列当前端独有功能(*_system.gd,仅被自身测试引用)已全部删除;按 SKILL.md,没有 40250 对应的逻辑属于缺陷,不再在此登记新条目。", "exclusion_reason": "在指定 40250 ClientVS22 source 中没有可达对应行为;记录该当前端扩展以防止漏审,但不计入 40250 等价率。", "fingerprints": { "reference": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "implementation": "fdaf946a6bffa5e914dfe7f40c2bbd42dc00e64d43e60231e9290021f14cbba2", + "implementation": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tests": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } } diff --git a/audit/remediation-roadmap.md b/audit/remediation-roadmap.md index f8678164..de7b89b1 100644 --- a/audit/remediation-roadmap.md +++ b/audit/remediation-roadmap.md @@ -15,7 +15,7 @@ python3 .agents/skills/metin2-40250-parity-audit/scripts/port_map.py queue --lim | 批次 | 内容 | 状态 | | --- | --- | --- | | 0 | 工具:参考根解析、缺失文件报错、`port_map.py`、构建/测试入口 | 完成 2026-09-22 | -| 1 | 删除只被自身测试引用的 `project/*_system.gd` 与其 `test_*_parity.gd`,清除 manifest 中对应证据 | 待确认删除清单 | +| 1 | 删除只被自身测试引用的 `project/*_system.gd` 与其 `test_*_parity.gd`,清除 manifest 中对应证据 | 完成 2026-09-22(106+106 个文件) | | 2 | P0 角色/移动单元(并行 worktree) | 未开始 | | 3 | P1 战斗/技能 → P2 游戏阶段封包 → P3 物品与 Python UI | 未开始 | | 4 | `NEEDS_LIVE` 真服验证批 | 未开始 | diff --git a/audit/reports/coverage.md b/audit/reports/coverage.md index 077fa321..8b89b88e 100644 --- a/audit/reports/coverage.md +++ b/audit/reports/coverage.md @@ -24,10 +24,10 @@ Total contracts: 45 ## Pending branch items -- Total: 224 -- P0: 93 -- P1: 118 -- P2: 13 +- Total: 241 +- P0: 94 +- P1: 132 +- P2: 15 - P3: 0 - Routing: see `audit/remediation-roadmap.md` diff --git a/project/alignment_pk_system.gd b/project/alignment_pk_system.gd deleted file mode 100644 index 23a84779..00000000 --- a/project/alignment_pk_system.gd +++ /dev/null @@ -1,187 +0,0 @@ -# alignment_pk_system.gd —— Metin2 40250 善恶值与名望头衔奖惩系统 1:1 -# 对照 40250 服务端 char.cpp:1190-1205, char_battle.cpp:UpdateAlignment, root/localeinfo.py:GetAlignmentTitleName -class_name AlignmentPkSystem -extends RefCounted - -signal alignment_changed(old_val: int, new_val: int, title: String) -signal title_changed(old_title: String, new_title: String) -signal items_dropped_on_death(dropped_items: Array) -signal zen_bean_consumed(cleared_amount: int, current_alignment: int) - -# 40250 官方善恶值极值 (内部数值 -200000 ~ +200000, 显示数值 -20000 ~ +20000) -const MIN_REAL_ALIGNMENT := -200000 -const MAX_REAL_ALIGNMENT := 200000 - -const VNUM_ZEN_BEAN := 70102 # 禅豆 (消减罪孽值) -const ZEN_BEAN_CLEAR_POINTS := 500 # 每次消除 500 显示点数 (5000 内部点数) - -# 9 大官方经典头衔段位表 (40250 localeinfo.py / locale_game.txt) -const TITLES: Array = [ - {"threshold": 12000, "name": "崇高骑士", "en_name": "Chivalric", "is_evil": false}, - {"threshold": 8000, "name": "尊贵贵族", "en_name": "Noble", "is_evil": false}, - {"threshold": 4000, "name": "正义良民", "en_name": "Good", "is_evil": false}, - {"threshold": 1000, "name": "友好伙伴", "en_name": "Friendly", "is_evil": false}, - {"threshold": 0, "name": "中立行者", "en_name": "Neutral", "is_evil": false}, - {"threshold": -3999, "name": "好斗狂徒", "en_name": "Aggressive","is_evil": true}, - {"threshold": -7999, "name": "狡诈恶徒", "en_name": "Fraudulent","is_evil": true}, - {"threshold": -11999,"name": "险恶凶手", "en_name": "Malicious", "is_evil": true}, - {"threshold": -20000,"name": "残忍魔头", "en_name": "Cruel", "is_evil": true} -] - -var real_alignment: int = 0 # 内部实际值 (-200000 ~ +200000) - -# 获取玩家当前对外显示的善恶值 (40250: sAlignment = m_iAlignment / 10) -func get_alignment() -> int: - return int(real_alignment / 10) - -# 根据善恶值获取官方 1:1 头衔名称 (40250 localeinfo.py:GetAlignmentTitleName) -static func get_title_name(alignment: int) -> String: - if alignment >= 12000: - return "崇高骑士" - elif alignment >= 8000: - return "尊贵贵族" - elif alignment >= 4000: - return "正义良民" - elif alignment >= 1000: - return "友好伙伴" - elif alignment >= 0: - return "中立行者" - elif alignment > -4000: - return "好斗狂徒" - elif alignment > -8000: - return "狡诈恶徒" - elif alignment > -12000: - return "险恶凶手" - else: - return "残忍魔头" - -# 检查当前是否为红名/恶名 -func is_evil() -> bool: - return get_alignment() < 0 - -# 更新善恶值 (40250 char_battle.cpp:UpdateAlignment) -# amount 为内部值加减 (如传入 100 表示内部 +100) -func update_real_alignment(amount: int) -> void: - var old_display = get_alignment() - var old_title = get_title_name(old_display) - - real_alignment = clampi(real_alignment + amount, MIN_REAL_ALIGNMENT, MAX_REAL_ALIGNMENT) - - var new_display = get_alignment() - var new_title = get_title_name(new_display) - - if old_display != new_display: - alignment_changed.emit(old_display, new_display, new_title) - - if old_title != new_title: - title_changed.emit(old_title, new_title) - -# 按显示点数直接调整善恶值 (如传入 +500 相当于内部 +5000) -func update_alignment_points(points: int) -> void: - update_real_alignment(points * 10) - -# 在线时间自然恢复善恶值 (40250 char.cpp:1194-1203) -# minutes: 挂机分钟数 -func recover_alignment_by_time(minutes: int, has_fast_recovery_item: bool = false) -> void: - if minutes <= 0: - return - - if real_alignment < 0: - var rate_per_min = 120 if has_fast_recovery_item else 60 - update_alignment_points(rate_per_min * minutes) - else: - var rate_per_min = 5 - update_alignment_points(rate_per_min * minutes) - -# 使用禅豆消减罪孽 (40250 Zen Bean 70102) -func use_zen_bean(inventory: Array, slot_index: int) -> Dictionary: - if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null: - return {"ok": false, "reason": "INVALID_SLOT"} - - var item: Dictionary = inventory[slot_index] - if int(item.get("vnum", 0)) != VNUM_ZEN_BEAN: - return {"ok": false, "reason": "NOT_A_ZEN_BEAN", "msg": "此物品不是禅豆!"} - - var cur_align := get_alignment() - if cur_align >= 0: - return {"ok": false, "reason": "ALIGNMENT_ALREADY_GOOD", "msg": "当前心境平和,无需使用禅豆消业!"} - - # 消耗 1 颗禅豆 - var cnt := int(item.get("count", 1)) - if cnt > 1: - item["count"] = cnt - 1 - else: - inventory[slot_index] = null - - # 恢复 500 点善恶值 (若剩余负值不足 500 则刚好回满到 0) - var clear_amount: int = mini(ZEN_BEAN_CLEAR_POINTS, -cur_align) - update_alignment_points(clear_amount) - - zen_bean_consumed.emit(clear_amount, get_alignment()) - - return { - "ok": true, - "cleared": clear_amount, - "current_alignment": get_alignment(), - "title": get_title_name(get_alignment()), - "msg": "服用禅豆,消除 %d 点罪业!当前善恶值:%d (%s)" % [clear_amount, get_alignment(), get_title_name(get_alignment())] - } - -# 击杀和平角色 (恶名惩罚) -func record_pk_kill() -> void: - update_alignment_points(-2000) - -# 击破魔石或野外怪物 (正向善名积累) -func record_monster_kill(is_metin: bool = false) -> void: - var bonus = 50 if is_metin else 10 - update_alignment_points(bonus) - -# 死亡惩罚计算 (40250 红名野外死亡爆装机制) -func handle_death_drop_penalty(inventory: Array) -> Array: - var cur_align := get_alignment() - if cur_align >= 0: - return [] # 白名/良民免死掉落 - - var drop_chance := 0.10 - var max_drop_count := 1 - - if cur_align <= -12000: # 残忍魔头 - drop_chance = 0.80 - max_drop_count = 3 - elif cur_align <= -8000: # 险恶凶手 - drop_chance = 0.50 - max_drop_count = 2 - elif cur_align <= -4000: # 狡诈恶徒 - drop_chance = 0.25 - max_drop_count = 1 - - var dropped_items: Array = [] - for i in range(inventory.size()): - if inventory[i] == null: - continue - var item: Dictionary = inventory[i] - # 灵魂绑定锁定的装备免于死亡掉落 - if item.get("is_soulbound", false): - continue - - # 判定概率爆出 - if randf() <= drop_chance: - dropped_items.append(item.duplicate(true)) - inventory[i] = null - if dropped_items.size() >= max_drop_count: - break - - if not dropped_items.is_empty(): - items_dropped_on_death.emit(dropped_items) - - return dropped_items - -# 序列化 -func serialize() -> Dictionary: - return { - "real_alignment": real_alignment - } - -# 反序列化 -func deserialize(data: Dictionary) -> void: - real_alignment = int(data.get("real_alignment", 0)) diff --git a/project/alignment_pk_system.gd.uid b/project/alignment_pk_system.gd.uid deleted file mode 100644 index 6115c245..00000000 --- a/project/alignment_pk_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cvfimpfsp5t6b diff --git a/project/alignment_system.gd b/project/alignment_system.gd deleted file mode 100644 index 2c88780f..00000000 --- a/project/alignment_system.gd +++ /dev/null @@ -1,128 +0,0 @@ -# alignment_system.gd —— 人物善恶道德与红名系统(40250 官方 1:1 对齐) -# 对齐源码: -# - 40250 server/game/src/char.cpp:1190-1205 (UpdateAlignment, 自然恢复公式) -# - 40250 server/game/src/char.cpp:3300 (UpdateAlignment) -# - 40250 ClientVS22/source/UserInterface/InstanceBaseEffect.cpp:609 (GetTitleColor) -# - 40250 ClientVS22/source/UserInterface/InstanceBase.h:262 (TITLE_NUM=9, TITLE_NONE=4) -# - 40250 text_tail.gd (title_color, title_name_key) -class_name AlignmentSystem -extends RefCounted - -const MIN_ALIGNMENT := -20000 -const MAX_ALIGNMENT := 20000 - -# 9 档善恶称号常量定义 -const TITLE_HEROIC := 0 # 英雄 (Heroic, >= 12000) -const TITLE_NOBLE := 1 # 尊者 (Noble, 8000..11999) -const TITLE_GOOD := 2 # 善良 (Good, 4000..7999) -const TITLE_FRIENDLY := 3 # 友好 (Friendly, 1000..3999) -const TITLE_NEUTRAL := 4 # 普通 (Neutral, 0..999) -const TITLE_AGGRESSIVE := 5 # 无赖 (Aggressive, -1..-3999) -const TITLE_FRAUDULENT := 6 # 欺诈 (Fraudulent, -4000..-7999) -const TITLE_MALICIOUS := 7 # 邪恶 (Malicious, -8000..-11999) -const TITLE_CRUEL := 8 # 魔头 (Cruel, <= -12000) - -const VNUM_ZEN_BEAN := 70102 # 禅定豆 / 清恶豆 - -const GRADE_NAMES := { - TITLE_HEROIC: "英雄", - TITLE_NOBLE: "尊者", - TITLE_GOOD: "善良", - TITLE_FRIENDLY: "友好", - TITLE_NEUTRAL: "普通", - TITLE_AGGRESSIVE: "无赖", - TITLE_FRAUDULENT: "欺诈", - TITLE_MALICIOUS: "邪恶", - TITLE_CRUEL: "魔头", -} - -const GRADE_COLORS := { - TITLE_HEROIC: Color8(0, 204, 255), - TITLE_NOBLE: Color8(0, 144, 255), - TITLE_GOOD: Color8(92, 110, 255), - TITLE_FRIENDLY: Color8(155, 155, 255), - TITLE_NEUTRAL: Color8(255, 255, 255), - TITLE_AGGRESSIVE: Color8(207, 117, 0), - TITLE_FRAUDULENT: Color8(235, 83, 0), - TITLE_MALICIOUS: Color8(227, 0, 0), - TITLE_CRUEL: Color8(255, 0, 0), -} - -## 获取善恶值对应的 40250 称号等级 (0..8) -static func get_alignment_grade(alignment: int) -> int: - if alignment >= 12000: - return TITLE_HEROIC - elif alignment >= 8000: - return TITLE_NOBLE - elif alignment >= 4000: - return TITLE_GOOD - elif alignment >= 1000: - return TITLE_FRIENDLY - elif alignment >= 0: - return TITLE_NEUTRAL - elif alignment > -4000: - return TITLE_AGGRESSIVE - elif alignment > -8000: - return TITLE_FRAUDULENT - elif alignment > -12000: - return TITLE_MALICIOUS - else: - return TITLE_CRUEL - -## 获取善恶称号中文名 -static func get_title_name(grade: int) -> String: - return GRADE_NAMES.get(grade, "普通") - -## 获取善恶称号颜色 -static func get_title_color(grade: int) -> Color: - return GRADE_COLORS.get(grade, Color.WHITE) - -## 检查是否为红名(罪恶状态) -static func is_red_name(alignment: int) -> bool: - return alignment < 0 - -## 检查是否为蓝名(正义状态) -static func is_blue_name(alignment: int) -> bool: - return alignment >= 1000 - -## 40250 char.cpp:1197 在线自然恢复善恶计算 -## minutes: 在线分钟数;has_fast_item: 是否佩戴禅定香囊等加速道具 -static func calculate_time_recovery(cur_alignment: int, minutes: int, has_fast_item := false) -> int: - if minutes <= 0: - return cur_alignment - var updated := cur_alignment - if cur_alignment < 0: - # 恶名状态:基础每分钟 +60 点,带香囊每分钟 +120 点 - var rate: int = 120 if has_fast_item else 60 - updated = mini(0, cur_alignment + rate * minutes) - else: - # 善良状态:每分钟 +5 点 - updated = mini(MAX_ALIGNMENT, cur_alignment + 5 * minutes) - return updated - -## 击杀怪物增加善恶值 (40250 char_battle.cpp: UpdateAlignment) -## 击杀野外怪物根据怪物等级恢复善恶(红名时恢复极快) -static func on_kill_monster(cur_alignment: int, mob_level: int, player_level: int) -> int: - var delta := maxi(1, mob_level - player_level / 2) - if cur_alignment < 0: - delta *= 2 # 红名击杀怪物恢复加倍 - return mini(MAX_ALIGNMENT, cur_alignment + delta) - -## 使用禅定豆 (Zen Bean 70102) -## 40250 原生逻辑:仅在红名 (alignment < 0) 时可使用,一次清除 500 点罪恶 -static func use_zen_bean(cur_alignment: int) -> Dictionary: - if cur_alignment >= 0: - return { - "ok": false, - "code": "CANNOT_USE_NOT_EVIL", - "msg": "你未处于恶名状态,无需使用禅定豆。" - } - var new_align: int = mini(0, cur_alignment + 500) - return { - "ok": true, - "code": "SUCCESS", - "old_alignment": cur_alignment, - "new_alignment": new_align, - "consumed": true, - "msg": "使用了禅定豆,洗清了 500 点罪恶值!(当前善恶: %d)" % new_align - } diff --git a/project/alignment_system.gd.uid b/project/alignment_system.gd.uid deleted file mode 100644 index 69141968..00000000 --- a/project/alignment_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://vg3mkqiy31mq diff --git a/project/atlas_navigation_system.gd b/project/atlas_navigation_system.gd deleted file mode 100644 index 33fa7eef..00000000 --- a/project/atlas_navigation_system.gd +++ /dev/null @@ -1,132 +0,0 @@ -# atlas_navigation_system.gd —— 40250 Atlas M 键全景大地图与 NPC 寻路导航光标系统 1:1 -# 严格对照: -# Client/Eternexus/root/uiminimap.py (AtlasWindow, MiniMap) -# ClientVS22/source/UserInterface/PythonMiniMap.cpp, PythonMiniMap.h (AddSignalPoint, ClearAllSignalPoint) -# Client/Eternexus/root/atlasinfo.txt -extends RefCounted - -# 地图手绘全景 Atlas 纹理与尺寸映射 -const MAP_ATLAS_REGISTRY := { - "metin2_map_a1": { "name": "赤蛇国主城 (Yongan)", "size": Vector2(256, 256), "world_size": Vector2(4000, 4000) }, - "metin2_map_b1": { "name": "白虎国主城 (Joan)", "size": Vector2(256, 256), "world_size": Vector2(4000, 4000) }, - "metin2_map_c1": { "name": "苍龙国主城 (Pyungmoo)", "size": Vector2(256, 256), "world_size": Vector2(4000, 4000) }, - "metin2_map_milgyo": { "name": "幽冥神殿 (Dark Temple)", "size": Vector2(256, 256), "world_size": Vector2(2000, 2000) }, - "metin2_map_deviltower": { "name": "恶魔之塔 (Demon Tower)", "size": Vector2(256, 256), "world_size": Vector2(1000, 1000) }, -} - -# 官方预设标准城镇 NPC 索引目录 (NPC VID -> 数据) -const PRESET_TOWN_NPCS := [ - { "vid": 20016, "name": "铁匠 (Blacksmith)", "category": "Smith", "pos": Vector2(580, 620) }, - { "vid": 9001, "name": "武器商人 (Weapon Dealer)", "category": "Shop", "pos": Vector2(610, 630) }, - { "vid": 9002, "name": "防具商人 (Armor Dealer)", "category": "Shop", "pos": Vector2(590, 650) }, - { "vid": 9003, "name": "杂货商人 (General Store)", "category": "Shop", "pos": Vector2(630, 600) }, - { "vid": 9005, "name": "仓库保管员 (Storekeeper)", "category": "Safebox", "pos": Vector2(550, 580) }, - { "vid": 20084, "name": "生物学家采吉拉布 (Biologist)", "category": "Quest", "pos": Vector2(680, 520) }, - { "vid": 20092, "name": "老行商 (Old Wanderer)", "category": "Special", "pos": Vector2(450, 710) }, - { "vid": 20008, "name": "城门守卫 (City Guard)", "category": "Guard", "pos": Vector2(800, 800) }, - { "vid": 20001, "name": "药店掌柜 (Alchemist)", "category": "Craft", "pos": Vector2(520, 640) }, - { "vid": 20091, "name": "修仙道长 (Taoist)", "category": "Skill", "pos": Vector2(640, 750) }, -] - -# 状态变量 -var is_atlas_visible: bool = false -var current_map_id: String = "metin2_map_a1" -var zoom_level: float = 1.0 # 滚轮缩放倍率 (0.5x ~ 3.0x) -var pan_offset: Vector2 = Vector2.ZERO # 拖拽平移偏移 - -# 动态信标点列表 (1:1 CPythonMiniMap::m_SignalPointVector) -var signal_points: Array = [] - -func _init(map_id: String = "metin2_map_a1") -> void: - current_map_id = map_id - is_atlas_visible = false - zoom_level = 1.0 - pan_offset = Vector2.ZERO - signal_points.clear() - -# 1. M 键快捷切换全景大地图 (1:1 ToggleAtlasWindow) -func toggle_atlas() -> bool: - is_atlas_visible = !is_atlas_visible - return is_atlas_visible - -func show_atlas() -> void: - is_atlas_visible = true - -func hide_atlas() -> void: - is_atlas_visible = false - -# 2. 切换当前地图与底图载入 (SetMapName) -func set_map(map_id: String) -> bool: - if not MAP_ATLAS_REGISTRY.has(map_id): - return false - current_map_id = map_id - clear_all_signals() - pan_offset = Vector2.ZERO - zoom_level = 1.0 - return true - -func get_current_map_info() -> Dictionary: - return MAP_ATLAS_REGISTRY.get(current_map_id, {}) - -# 3. 缩放与平移控制 -func set_zoom(zoom: float) -> void: - zoom_level = clamp(zoom, 0.5, 3.0) - -func apply_pan(delta_offset: Vector2) -> void: - pan_offset += delta_offset - -# 4. NPC 目录检索与过滤 -func get_npcs(category_filter: String = "") -> Array: - if category_filter.is_empty(): - return PRESET_TOWN_NPCS - var filtered: Array = [] - for npc in PRESET_TOWN_NPCS: - if npc.get("category") == category_filter: - filtered.append(npc) - return filtered - -func find_npc_by_vid(vid: int) -> Dictionary: - for npc in PRESET_TOWN_NPCS: - if npc.get("vid") == vid: - return npc - return {} - -# 5. 信标寻路导航点机制 (1:1 CPythonMiniMap::AddSignalPoint / ClearAllSignalPoint) -func add_signal_point(target_x: float, target_y: float) -> Dictionary: - var pt := { - "x": target_x, - "y": target_y, - "active": true - } - signal_points.append(pt) - return pt - -func clear_all_signals() -> void: - signal_points.clear() - -# 6. 指南针方位角与距离计算 (角色与目标间的连线向量) -func calculate_navigation_vector(player_pos: Vector2, target_pos: Vector2) -> Dictionary: - var diff := target_pos - player_pos - var dist := diff.length() - var angle_rad := diff.angle() - var angle_deg := rad_to_deg(angle_rad) - - return { - "distance": dist, - "direction_vector": diff.normalized() if dist > 0.001 else Vector2.ZERO, - "angle_rad": angle_rad, - "angle_deg": angle_deg, - "arrived": (dist <= 30.0) # 30 码内视为到达目标 - } - -# 7. 世界坐标至大地图 UI 局部像素坐标投影转换 -func world_to_atlas_coord(world_pos: Vector2) -> Vector2: - var map_info := get_current_map_info() - var world_size: Vector2 = map_info.get("world_size", Vector2(4000, 4000)) - var atlas_size: Vector2 = map_info.get("size", Vector2(256, 256)) - - var ratio_x := atlas_size.x / world_size.x - var ratio_y := atlas_size.y / world_size.y - - var projected := Vector2(world_pos.x * ratio_x, world_pos.y * ratio_y) - return (projected * zoom_level) + pan_offset diff --git a/project/atlas_navigation_system.gd.uid b/project/atlas_navigation_system.gd.uid deleted file mode 100644 index a3dbad6a..00000000 --- a/project/atlas_navigation_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dras8n6cjudtn diff --git a/project/attach_metin_dialog_system.gd b/project/attach_metin_dialog_system.gd deleted file mode 100644 index 65c29c64..00000000 --- a/project/attach_metin_dialog_system.gd +++ /dev/null @@ -1,144 +0,0 @@ -# attach_metin_dialog_system.gd -# 40250 官方 1:1 装备幽魂石/灵魂石镶嵌与双重属性对比预览系统 -# 对照: uiattachmetin.py, attachstonedialog.py, char_item.cpp -class_name AttachMetinDialogSystem -extends RefCounted - -const METIN_SOCKET_MAX_NUM: int = 3 - -const METIN_NORMAL: int = 0 -const METIN_GOLD: int = 1 - -const METIN_SOCKET_TYPE_NONE: int = 0 -const METIN_SOCKET_TYPE_SILVER: int = 1 -const METIN_SOCKET_TYPE_GOLD: int = 2 - -const ITEM_BROKEN_METIN_VNUM: int = 28960 - -# 经典灵魂石属性对照表 (40250 canonical metin stones 28000~28443) -const METIN_STONES: Dictionary = { - 28030: {"name": "战神之石+0", "bonus_type": "ATT_BONUS_TO_WARRIOR", "bonus_value": 5}, - 28130: {"name": "战神之石+1", "bonus_type": "ATT_BONUS_TO_WARRIOR", "bonus_value": 8}, - 28230: {"name": "战神之石+2", "bonus_type": "ATT_BONUS_TO_WARRIOR", "bonus_value": 12}, - 28330: {"name": "战神之石+3", "bonus_type": "ATT_BONUS_TO_WARRIOR", "bonus_value": 17}, - 28430: {"name": "战神之石+4", "bonus_type": "ATT_BONUS_TO_WARRIOR", "bonus_value": 25}, - 28434: {"name": "穿透之石+4", "bonus_type": "POINT_PENETRATE_PCT", "bonus_value": 8}, - 28435: {"name": "致命之石+4", "bonus_type": "POINT_CRITICAL_PCT", "bonus_value": 8}, - 28438: {"name": "防御之石+4", "bonus_type": "POINT_DEF_GRADE_BONUS", "bonus_value": 15}, - 28442: {"name": "加速之石+4", "bonus_type": "POINT_MOV_SPEED", "bonus_value": 30}, - 28960: {"name": "破碎的石头碎块", "bonus_type": "NONE", "bonus_value": 0} -} - -var is_open: bool = false -var metin_pos: int = -1 -var target_pos: int = -1 - -var metin_vnum: int = 0 -var metin_subtype: int = METIN_NORMAL - -var target_item_vnum: int = 0 -var current_sockets: Array = [0, 0, 0] -var preview_sockets: Array = [0, 0, 0] -var target_socket_idx: int = -1 - -var last_packet_sent: Dictionary = {} -var last_sound_played: String = "" - -signal dialog_opened(metin_pos: int, target_pos: int, preview_sockets: Array) -signal dialog_closed() -signal metin_attached(metin_pos: int, target_pos: int, target_slot_idx: int) -signal attach_failed(reason: String) - -func can_attach_metin(slot_type: int, metin_type: int) -> bool: - if metin_type == METIN_NORMAL: - if slot_type == METIN_SOCKET_TYPE_SILVER or slot_type == METIN_SOCKET_TYPE_GOLD: - return true - elif metin_type == METIN_GOLD: - if slot_type == METIN_SOCKET_TYPE_GOLD: - return true - return false - -func open_dialog(p_metin_pos: int, p_target_pos: int, p_metin_vnum: int, p_metin_subtype: int, p_target_vnum: int, p_sockets: Array) -> bool: - metin_pos = p_metin_pos - target_pos = p_target_pos - metin_vnum = p_metin_vnum - metin_subtype = p_metin_subtype - target_item_vnum = p_target_vnum - - current_sockets = [0, 0, 0] - preview_sockets = [0, 0, 0] - target_socket_idx = -1 - - for i in range(mini(p_sockets.size(), METIN_SOCKET_MAX_NUM)): - current_sockets[i] = p_sockets[i] - preview_sockets[i] = p_sockets[i] - - # 寻找首个符合条件的开放槽位 - var found_slot: bool = false - for i in range(METIN_SOCKET_MAX_NUM): - var slot_state: int = current_sockets[i] - if can_attach_metin(slot_state, metin_subtype): - preview_sockets[i] = metin_vnum - target_socket_idx = i - found_slot = true - break - - if not found_slot: - attach_failed.emit("NO_AVAILABLE_SOCKET") - return false - - is_open = true - dialog_opened.emit(metin_pos, target_pos, preview_sockets) - return true - -func accept() -> bool: - if not is_open: - return false - if target_socket_idx < 0: - attach_failed.emit("INVALID_SOCKET") - return false - - # 40250 发送 net.SendItemUseToItemPacket(self.metinItemPos, self.targetItemPos) - last_packet_sent = { - "header": "HEADER_CG_ITEM_USE_TO_ITEM", - "metin_pos": metin_pos, - "target_pos": target_pos, - "target_socket_idx": target_socket_idx, - "metin_vnum": metin_vnum - } - - # 官方原版音效 - last_sound_played = "sound/ui/metinstone_insert.wav" - - metin_attached.emit(metin_pos, target_pos, target_socket_idx) - close_dialog() - return true - -func close_dialog() -> void: - is_open = false - metin_pos = -1 - target_pos = -1 - dialog_closed.emit() - -# 模拟服务端结算镶嵌 (支持成功率判定: 成功直接嵌入,失败产生破碎石块 28960) -func simulate_server_process(target_sockets: Array, socket_idx: int, attached_metin_vnum: int, success: bool) -> Array: - var result_sockets: Array = target_sockets.duplicate() - if socket_idx >= 0 and socket_idx < result_sockets.size(): - if success: - result_sockets[socket_idx] = attached_metin_vnum - else: - result_sockets[socket_idx] = ITEM_BROKEN_METIN_VNUM - return result_sockets - -# 模拟卷轴清理破碎石头 (vnum 71083 / Scroll of Cleansing) -func clean_broken_socket(target_sockets: Array, socket_idx: int) -> Array: - var result_sockets: Array = target_sockets.duplicate() - if socket_idx >= 0 and socket_idx < result_sockets.size(): - if result_sockets[socket_idx] == ITEM_BROKEN_METIN_VNUM: - result_sockets[socket_idx] = METIN_SOCKET_TYPE_SILVER - return result_sockets - -func get_metin_info(vnum: int) -> Dictionary: - if METIN_STONES.has(vnum): - return METIN_STONES[vnum] - return {"name": "未知幽魂石", "bonus_type": "UNKNOWN", "bonus_value": 0} diff --git a/project/attach_metin_dialog_system.gd.uid b/project/attach_metin_dialog_system.gd.uid deleted file mode 100644 index da55bb78..00000000 --- a/project/attach_metin_dialog_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bkw5vjkd4yqgq diff --git a/project/auto_combat_bot_system.gd b/project/auto_combat_bot_system.gd deleted file mode 100644 index 5d1e865f..00000000 --- a/project/auto_combat_bot_system.gd +++ /dev/null @@ -1,175 +0,0 @@ -# auto_combat_bot_system.gd —— Metin2 40250 自动战斗猎杀挂机助手 1:1 -# 对照 40250 服务端 char_battle.cpp, bot.cpp, item_manager.cpp -class_name AutoCombatBotSystem -extends RefCounted - -signal bot_toggled(enabled: bool) -signal potion_used(potion_vnum: int, ptype: String) -signal target_locked(target_id: String) -signal auto_attack_fired(target_id: String, damage: int) -signal auto_skill_fired(skill_vnum: int, target_id: String) -signal auto_loot_collected(item_name: String, slot: int) - -# 红药与蓝药 VNUM -const HP_POTION_VNUMS: Array = [27001, 27002, 27003] # 小/中/大红 -const SP_POTION_VNUMS: Array = [27004, 27005, 27006] # 小/中/大蓝 - -var is_enabled: bool = false -var auto_potion_enabled: bool = true -var hp_threshold: float = 0.50 # 50% HP 触发喝红 -var sp_threshold: float = 0.40 # 40% SP 触发喝蓝 - -var auto_attack_enabled: bool = true -var scan_radius: float = 15.0 -var attack_range: float = 3.0 - -var auto_skill_enabled: bool = true -var assigned_skills: Array = [] -var skill_cooldowns: Dictionary = {} - -var auto_loot_enabled: bool = true -var loot_radius: float = 8.0 - -var current_target_id: String = "" -var potion_cooldown: float = 0.0 - -# 开启/关闭挂机内挂 -func toggle_bot(enable: bool) -> Dictionary: - is_enabled = enable - if not is_enabled: - current_target_id = "" - bot_toggled.emit(is_enabled) - return {"ok": true, "enabled": is_enabled, "msg": "自动战斗猎杀助手已%s!" % ("开启" if is_enabled else "关闭")} - -# 帧心跳驱动挂机逻辑 -func update_bot(delta: float, player_data: Dictionary, inventory: Array, nearby_mobs: Array, nearby_drops: Array) -> Dictionary: - if not is_enabled: - return {"status": "disabled"} - - if potion_cooldown > 0.0: - potion_cooldown = max(0.0, potion_cooldown - delta) - - for sk in skill_cooldowns.keys(): - if skill_cooldowns[sk] > 0.0: - skill_cooldowns[sk] = max(0.0, skill_cooldowns[sk] - delta) - - var p_pos: Vector3 = player_data.get("pos", Vector3.ZERO) - var p_hp: int = int(player_data.get("hp", 100)) - var p_max_hp: int = max(1, int(player_data.get("max_hp", 100))) - var p_sp: int = int(player_data.get("sp", 100)) - var p_max_sp: int = max(1, int(player_data.get("max_sp", 100))) - - # 1. 自动智能喝药判定 - if auto_potion_enabled and potion_cooldown <= 0.0: - if float(p_hp) / float(p_max_hp) <= hp_threshold: - if _use_potion(HP_POTION_VNUMS, inventory, player_data, "hp"): - potion_cooldown = 1.0 # 喝药公共冷却 1 秒 - - if float(p_sp) / float(p_max_sp) <= sp_threshold: - if _use_potion(SP_POTION_VNUMS, inventory, player_data, "sp"): - potion_cooldown = 1.0 - - # 2. 自动拾取掉落物 - if auto_loot_enabled: - _process_auto_loot(p_pos, nearby_drops, inventory) - - # 3. 自动搜敌与战斗打击 - if auto_attack_enabled: - var act = _process_auto_combat(delta, p_pos, player_data, nearby_mobs) - return act - - return {"status": "idle"} - -# 自动喝药 -func _use_potion(potion_vnums: Array, inventory: Array, player_data: Dictionary, ptype: String) -> bool: - for i in range(inventory.size()): - if inventory[i] != null and int(inventory[i].get("vnum", 0)) in potion_vnums: - var pot = inventory[i] - var pvnum = int(pot["vnum"]) - var cnt = int(pot.get("count", 1)) - if cnt > 1: - pot["count"] = cnt - 1 - else: - inventory[i] = null - - var rec = 1500 if pvnum in [27003, 27006] else 800 - if ptype == "hp": - player_data["hp"] = min(int(player_data["max_hp"]), int(player_data["hp"]) + rec) - else: - player_data["sp"] = min(int(player_data["max_sp"]), int(player_data["sp"]) + rec) - - potion_used.emit(pvnum, ptype) - return true - return false - -# 自动拾取 -func _process_auto_loot(player_pos: Vector3, drops: Array, inventory: Array) -> void: - for d in drops: - if d.get("looted", false): - continue - var d_pos: Vector3 = d.get("pos", Vector3.ZERO) - if player_pos.distance_to(d_pos) <= loot_radius: - # 放入背包 - for i in range(inventory.size()): - if inventory[i] == null: - inventory[i] = { - "vnum": d.get("vnum", 1), - "name": d.get("name", "战利品"), - "count": int(d.get("count", 1)) - } - d["looted"] = true - auto_loot_collected.emit(d["name"], i) - break - -# 自动锁敌与攻击 -func _process_auto_combat(delta: float, player_pos: Vector3, player_data: Dictionary, mobs: Array) -> Dictionary: - var closest_mob = null - var min_dist = scan_radius + 1.0 - - for m in mobs: - if not m.get("alive", true): - continue - var m_pos: Vector3 = m.get("pos", Vector3.ZERO) - var dist = player_pos.distance_to(m_pos) - if dist <= scan_radius and dist < min_dist: - min_dist = dist - closest_mob = m - - if closest_mob == null: - current_target_id = "" - return {"status": "no_targets"} - - var target_id = str(closest_mob.get("id", "mob")) - if current_target_id != target_id: - current_target_id = target_id - target_locked.emit(target_id) - - var target_pos: Vector3 = closest_mob.get("pos", Vector3.ZERO) - var dist_to_target = player_pos.distance_to(target_pos) - - # 走位靠拢 - if dist_to_target > attack_range: - var new_pos = player_pos.move_toward(target_pos, 5.0 * delta) - player_data["pos"] = new_pos - return {"status": "moving_to_target", "target": target_id, "dist": dist_to_target} - - # 尝试释放技能 - if auto_skill_enabled: - for sk in assigned_skills: - if skill_cooldowns.get(sk, 0.0) <= 0.0: - skill_cooldowns[sk] = 8.0 # 默认技能 8 秒冷却 - auto_skill_fired.emit(sk, target_id) - var sk_dmg = int(float(player_data.get("atk", 500)) * 2.0) - closest_mob["hp"] = max(0, int(closest_mob.get("hp", 1000)) - sk_dmg) - if closest_mob["hp"] <= 0: - closest_mob["alive"] = false - return {"status": "skill_attack", "skill": sk, "damage": sk_dmg} - - # 普通挥砍攻击 - var atk_dmg = int(player_data.get("atk", 500)) - closest_mob["hp"] = max(0, int(closest_mob.get("hp", 1000)) - atk_dmg) - if closest_mob["hp"] <= 0: - closest_mob["alive"] = false - - auto_attack_fired.emit(target_id, atk_dmg) - return {"status": "basic_attack", "target": target_id, "damage": atk_dmg} diff --git a/project/auto_combat_bot_system.gd.uid b/project/auto_combat_bot_system.gd.uid deleted file mode 100644 index 24cccc81..00000000 --- a/project/auto_combat_bot_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d2i1ud7k3o0l5 diff --git a/project/battle_arena_duel_system.gd b/project/battle_arena_duel_system.gd deleted file mode 100644 index 852b5c71..00000000 --- a/project/battle_arena_duel_system.gd +++ /dev/null @@ -1,218 +0,0 @@ -# battle_arena_duel_system.gd —— Metin2 40250 官方武斗竞技场切磋与裁判规则 1:1 -# 对照 40250 服务端 arena.cpp, arena.h, BattleArena.cpp, questlua_arena.cpp -class_name BattleArenaDuelSystem -extends RefCounted - -signal duel_started(opponent_name: String, set_count: int, time_limit: float) -signal round_started(current_round: int) -signal round_ended(winner_side: String, score_a: int, score_b: int) -signal duel_finished(winner_side: String, final_score_a: int, final_score_b: int, reward: Dictionary) -signal rule_violation_intercepted(reason: String) - -const NPC_REFEREE := 20017 # 比武场裁判 (Arena Referee) -const ARENA_MAP_INDEX := 112 - -# 擂台对立出生点 (Point A 玩家, Point B 对手) -const START_POINT_A := Vector2(100.0, 100.0) -const START_POINT_B := Vector2(200.0, 200.0) -const ARENA_BOUNDS := Rect2(80.0, 80.0, 140.0, 140.0) # 擂台活动合法矩形区域 - -# 决斗状态枚举 -const STATE_IDLE := "IDLE" -const STATE_COUNTDOWN := "COUNTDOWN" -const STATE_FIGHTING := "FIGHTING" -const STATE_ROUND_OVER:= "ROUND_OVER" -const STATE_FINISHED := "FINISHED" - -# 单机官方 4 大门派守擂试炼 NPC 导师 -const OPPONENT_PRESETS: Dictionary = { - "warrior": { - "id": "warrior", - "name": "战神武者教头", - "job": "warrior", - "max_hp": 12000, - "attack_power": 450, - "defense": 200, - "skills": ["三连斩", "剑气破空"] - }, - "assassin": { - "id": "assassin", - "name": "暗夜游侠神箭手", - "job": "assassin", - "max_hp": 9000, - "attack_power": 520, - "defense": 150, - "skills": ["连环毒矢", "疾影步"] - }, - "sura": { - "id": "sura", - "name": "冥界暗影修罗", - "job": "sura", - "max_hp": 11000, - "attack_power": 480, - "defense": 220, - "skills": ["幽冥炎刃", "魔灵护体"] - }, - "shaman": { - "id": "shaman", - "name": "雷鸣灵术萨满", - "job": "shaman", - "max_hp": 10000, - "attack_power": 400, - "defense": 180, - "skills": ["天顶奔雷", "金钟守护"] - } -} - -# 比赛运行时状态 -var match_state: String = STATE_IDLE -var current_round: int = 1 -var max_sets: int = 3 # 3 局 2 胜 -var sets_to_win: int = 2 -var time_limit_per_round: float = 300.0 # 5 分钟 -var round_timer: float = 300.0 -var countdown_timer: float = 3.0 - -var score_a: int = 0 # 玩家得分 -var score_b: int = 0 # 对手得分 - -var player_pos: Vector2 = START_POINT_A -var opponent_pos: Vector2 = START_POINT_B - -var active_opponent: Dictionary = {} -var opponent_current_hp: int = 0 - -# 启动武斗切磋 -func start_duel(opponent_id: String, sets: int = 3, time_limit: float = 300.0) -> Dictionary: - if not OPPONENT_PRESETS.has(opponent_id): - return {"ok": false, "reason": "INVALID_OPPONENT", "msg": "未知的守擂武者!"} - - active_opponent = OPPONENT_PRESETS[opponent_id].duplicate(true) - max_sets = sets - sets_to_win = int(ceil(float(max_sets) / 2.0)) - time_limit_per_round = time_limit - - score_a = 0 - score_b = 0 - current_round = 1 - - _prepare_round() - - duel_started.emit(active_opponent["name"], max_sets, time_limit_per_round) - return { - "ok": true, - "opponent": active_opponent["name"], - "max_sets": max_sets, - "sets_to_win": sets_to_win, - "pos_a": player_pos, - "pos_b": opponent_pos - } - -# 准备新回合 -func _prepare_round() -> void: - match_state = STATE_COUNTDOWN - countdown_timer = 3.0 - round_timer = time_limit_per_round - - player_pos = START_POINT_A - opponent_pos = START_POINT_B - opponent_current_hp = int(active_opponent.get("max_hp", 10000)) - -# 心跳逻辑推进 -func update(delta: float) -> void: - if match_state == STATE_COUNTDOWN: - countdown_timer -= delta - if countdown_timer <= 0.0: - match_state = STATE_FIGHTING - round_started.emit(current_round) - elif match_state == STATE_FIGHTING: - round_timer -= delta - if round_timer <= 0.0: - # 回合超时:双方均未击倒,判定为平局或根据剩余生命决出 - _handle_round_over("timeout") - -# 40250 禁药规则拦截 -func use_potion_check(item_vnum: int) -> bool: - if match_state == STATE_FIGHTING or match_state == STATE_COUNTDOWN: - # 27001~27006 为红蓝药水 - if item_vnum >= 27001 and item_vnum <= 27006: - rule_violation_intercepted.emit("POTION_FORBIDDEN_IN_ARENA") - return false - return true - -# 40250 禁马规则拦截 -func mount_check() -> bool: - if match_state == STATE_FIGHTING or match_state == STATE_COUNTDOWN: - rule_violation_intercepted.emit("MOUNT_FORBIDDEN_IN_ARENA") - return false - return true - -# 40250 擂台边界巡检 -func check_boundary(pos: Vector2) -> bool: - if not ARENA_BOUNDS.has_point(pos): - rule_violation_intercepted.emit("OUT_OF_ARENA_BOUNDS") - return false - return true - -# 击倒判定处理 -func record_defeat(defeated_side: String) -> Dictionary: - if match_state != STATE_FIGHTING: - return {"ok": false, "reason": "NOT_IN_FIGHTING_STATE"} - - var round_winner := "" - if defeated_side == "B": # 对手倒下,玩家得分 - score_a += 1 - round_winner = "A" - elif defeated_side == "A": # 玩家倒下,对手得分 - score_b += 1 - round_winner = "B" - - return _handle_round_over(round_winner) - -# 回合结束与大局胜负判定 -func _handle_round_over(winner_side: String) -> Dictionary: - match_state = STATE_ROUND_OVER - round_ended.emit(winner_side, score_a, score_b) - - # 检查大局是否已有胜者 - if score_a >= sets_to_win or score_b >= sets_to_win or current_round >= max_sets: - match_state = STATE_FINISHED - var grand_winner := "A" if score_a > score_b else ("B" if score_b > score_a else "DRAW") - - var reward: Dictionary = {} - if grand_winner == "A": - reward = { - "arena_points": 100 * sets_to_win, - "gold": 500000, - "title": "【竞技场常胜军】" - } - - duel_finished.emit(grand_winner, score_a, score_b, reward) - return { - "ok": true, - "match_finished": true, - "winner": grand_winner, - "score_a": score_a, - "score_b": score_b, - "reward": reward - } - - # 进入下一回合 - current_round += 1 - _prepare_round() - - return { - "ok": true, - "match_finished": false, - "current_round": current_round, - "score_a": score_a, - "score_b": score_b - } - -# 重置武斗场状态 -func reset_arena() -> void: - match_state = STATE_IDLE - current_round = 1 - score_a = 0 - score_b = 0 - active_opponent.clear() diff --git a/project/battle_arena_duel_system.gd.uid b/project/battle_arena_duel_system.gd.uid deleted file mode 100644 index 458928c3..00000000 --- a/project/battle_arena_duel_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bsbmx3e0l0jgs diff --git a/project/belt_system.gd b/project/belt_system.gd deleted file mode 100644 index a9157f27..00000000 --- a/project/belt_system.gd +++ /dev/null @@ -1,161 +0,0 @@ -# belt_system.gd —— Metin2 40250 腰带系统与专属药水背包 1:1 -# 对照 40250 服务端 belt.cpp, char_item.cpp, uiinventory.py -class_name BeltSystem -extends RefCounted - -signal belt_equipped(belt_name: String, slots_unlocked: int) -signal belt_unequipped() -signal belt_item_stored(slot: int, item_name: String) -signal belt_item_removed(slot: int) - -const MAX_BELT_SLOTS := 16 # 最大 4x4 空间 - -# 允许存入腰带的消耗品/药水类型 -const ALLOWED_CONSUMABLE_TYPES: Array = ["potion", "consumable", "dew", "elixir"] - -# 已装备的腰带 -var equipped_belt: Dictionary = {} -var belt_inventory: Array = [] # 16 个格子 - -func _init() -> void: - belt_inventory.resize(MAX_BELT_SLOTS) - for i in range(MAX_BELT_SLOTS): - belt_inventory[i] = null - -# 根据腰带强化等级计算开放的药水槽位数量 -static func calculate_unlocked_slots(refine_level: int) -> int: - if refine_level >= 9: - return 16 # 4x4 - elif refine_level >= 7: - return 12 # 3x4 - elif refine_level >= 5: - return 10 - elif refine_level >= 3: - return 8 # 2x4 - elif refine_level >= 1: - return 6 - else: - return 4 # 1x4 - -# 获取当前开放的药水格子数 -func get_unlocked_slot_count() -> int: - if equipped_belt.is_empty(): - return 0 - var refine: int = int(equipped_belt.get("refine_level", 0)) - return calculate_unlocked_slots(refine) - -# 佩戴腰带 -func equip_belt(belt_item: Dictionary) -> Dictionary: - if belt_item.is_empty(): - return {"ok": false, "reason": "EMPTY_BELT", "msg": "腰带数据为空!"} - - equipped_belt = belt_item.duplicate(true) - var unlocked := get_unlocked_slot_count() - var bname: String = equipped_belt.get("name", "腰带") - - belt_equipped.emit(bname, unlocked) - - return { - "ok": true, - "belt": equipped_belt, - "unlocked_slots": unlocked, - "msg": "已佩戴【%s】,解锁了 %d 格专属药水空间!" % [bname, unlocked] - } - -# 卸下腰带 (需确保腰带内物品能够全部安全移出或为空) -func unequip_belt(main_inventory: Array) -> Dictionary: - if equipped_belt.is_empty(): - return {"ok": false, "reason": "NO_BELT_EQUIPPED", "msg": "当前未佩戴腰带!"} - - # 检查腰带内是否有物品 - var items_in_belt: Array = [] - for i in range(MAX_BELT_SLOTS): - if belt_inventory[i] != null: - items_in_belt.append({"belt_slot": i, "item": belt_inventory[i]}) - - if not items_in_belt.is_empty(): - # 统计主背包空位 - var empty_slots: Array = [] - for j in range(main_inventory.size()): - if main_inventory[j] == null: - empty_slots.append(j) - - if empty_slots.size() < items_in_belt.size(): - return { - "ok": false, - "reason": "BELT_NOT_EMPTY", - "items_count": items_in_belt.size(), - "empty_slots": empty_slots.size(), - "msg": "主背包空间不足以容纳腰带内的药水,无法卸下腰带!" - } - - # 移入主背包 - for k in range(items_in_belt.size()): - var m_slot: int = empty_slots[k] - var it = items_in_belt[k]["item"] - var b_slot: int = items_in_belt[k]["belt_slot"] - main_inventory[m_slot] = it - belt_inventory[b_slot] = null - - var old_belt := equipped_belt.duplicate(true) - equipped_belt.clear() - belt_unequipped.emit() - - return { - "ok": true, - "belt": old_belt, - "msg": "腰带已卸下!内部药水已全部归还至主背包。" - } - -# 存放药水至腰带专属背包 -func store_item_in_belt(slot_idx: int, item: Dictionary) -> Dictionary: - if equipped_belt.is_empty(): - return {"ok": false, "reason": "NO_BELT_EQUIPPED", "msg": "未佩戴腰带,无法使用专属药水栏!"} - - var unlocked := get_unlocked_slot_count() - if slot_idx < 0 or slot_idx >= unlocked: - return { - "ok": false, - "reason": "SLOT_LOCKED_OR_INVALID", - "unlocked_slots": unlocked, - "msg": "该腰带槽位尚未解锁或无效!当前腰带只开放了前 %d 格。" % unlocked - } - - # 药品类型校验 - var it_type: String = item.get("item_type", "") - var is_potion: bool = ALLOWED_CONSUMABLE_TYPES.has(it_type) or item.get("is_potion", false) - if not is_potion: - return { - "ok": false, - "reason": "NOT_A_POTION", - "msg": "腰带栏位专用于补给品,只能存放药水、灵药与神圣露水!" - } - - belt_inventory[slot_idx] = item.duplicate(true) - var iname: String = item.get("name", "药水") - belt_item_stored.emit(slot_idx, iname) - - return { - "ok": true, - "slot_idx": slot_idx, - "item": belt_inventory[slot_idx], - "msg": "已将【%s】置入腰带快捷栏!" % iname - } - -# 从腰带栏取出药水 -func take_item_from_belt(slot_idx: int) -> Dictionary: - if slot_idx < 0 or slot_idx >= MAX_BELT_SLOTS: - return {"ok": false, "reason": "INVALID_SLOT"} - - var item = belt_inventory[slot_idx] - if item == null: - return {"ok": false, "reason": "EMPTY_SLOT"} - - belt_inventory[slot_idx] = null - belt_item_removed.emit(slot_idx) - - return { - "ok": true, - "slot_idx": slot_idx, - "item": item - } diff --git a/project/belt_system.gd.uid b/project/belt_system.gd.uid deleted file mode 100644 index 69e9456c..00000000 --- a/project/belt_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://nde6dfrdg7vg diff --git a/project/bgm_jukebox_system.gd b/project/bgm_jukebox_system.gd deleted file mode 100644 index 69edfc88..00000000 --- a/project/bgm_jukebox_system.gd +++ /dev/null @@ -1,101 +0,0 @@ -# bgm_jukebox_system.gd —— 40250 客户端 BGM 点唱机与地图音乐配置系统 1:1 -# 严格对照: -# Client/Eternexus/root/uiselectmusic.py (SelectMusicWindow) -# Client/Eternexus/root/musicinfo.py -# ClientVS22/source/UserInterface/PythonSoundManagerModule.cpp -extends RefCounted - -const DEFAULT_THEME_TRACK := "enter_the_east.mp3" - -# 40250 官方标准曲目清单 (musicinfo.py) -const CANONICAL_TRACKS := [ - "enter_the_east.mp3", - "open_the_gate.mp3", - "characterselect.mp3", - "wedding.mp3", - "desert.mp3", - "mountain.mp3", - "temple.mp3", - "monkey_dungeon.mp3", - "flame_dungeon.mp3", - "catacomb.mp3", - "save_the_world.mp3", - "peaceful_valley.mp3", - "wonderland.mp3" -] - -# 各大地图官方默认 BGM 映射表 -const DEFAULT_MAP_BGM_MAP := { - "intro_login": "enter_the_east.mp3", - "intro_select": "characterselect.mp3", - "metin2_map_a1": "open_the_gate.mp3", - "metin2_map_b1": "open_the_gate.mp3", - "metin2_map_c1": "open_the_gate.mp3", - "metin2_map_n_desert_01": "desert.mp3", - "metin2_map_n_snowm_01": "mountain.mp3", - "metin2_map_milgyo": "temple.mp3", - "metin2_map_deviltower": "catacomb.mp3", - "metin2_map_wedding_01": "wedding.mp3", -} - -# 播放器运行时状态 -var current_track: String = "" -var is_playing: bool = false -var loop_enabled: bool = true -var volume: float = 0.8 # 0.0 ~ 1.0 (默认 80%) - -# 自定义地图偏好覆写: map_id -> track_name -var custom_map_overrides: Dictionary = {} - -func _init() -> void: - current_track = "" - is_playing = false - loop_enabled = true - volume = 0.8 - custom_map_overrides.clear() - -# 1. 播放指定曲目 (1:1 snd.PlayBGM) -func play_bgm(track_name: String, loop: bool = true) -> bool: - if not CANONICAL_TRACKS.has(track_name): - return false - current_track = track_name - is_playing = true - loop_enabled = loop - return true - -# 2. 停止背景音乐 (1:1 snd.StopBGM) -func stop_bgm() -> void: - is_playing = false - current_track = "" - -# 3. 调节音量 (1:1 snd.SetBGMVolume) -func set_volume(vol_pct: float) -> void: - volume = clamp(vol_pct, 0.0, 1.0) - -# 4. 获取指定地图应当播放的背景音乐 (自定义优先,其次默认) -func get_effective_map_bgm(map_id: String) -> String: - if custom_map_overrides.has(map_id): - return custom_map_overrides[map_id] - return DEFAULT_MAP_BGM_MAP.get(map_id, DEFAULT_THEME_TRACK) - -# 5. 进入地图自动切换音乐 -func on_enter_map(map_id: String) -> String: - var target_track = get_effective_map_bgm(map_id) - if target_track != current_track or not is_playing: - play_bgm(target_track, true) - return target_track - -# 6. 点唱机自定义地图专属曲目 (uiselectmusic.py: SelectMusic) -func set_custom_map_bgm(map_id: String, track_name: String) -> bool: - if not CANONICAL_TRACKS.has(track_name): - return false - custom_map_overrides[map_id] = track_name - return true - -# 7. 恢复地图默认曲目 -func reset_custom_map_bgm(map_id: String) -> void: - if custom_map_overrides.has(map_id): - custom_map_overrides.erase(map_id) - -func clear_all_custom_overrides() -> void: - custom_map_overrides.clear() diff --git a/project/bgm_jukebox_system.gd.uid b/project/bgm_jukebox_system.gd.uid deleted file mode 100644 index b85ffc1d..00000000 --- a/project/bgm_jukebox_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://co4sc1bl3n0sj diff --git a/project/biologist_chaegirab_research_system.gd b/project/biologist_chaegirab_research_system.gd deleted file mode 100644 index ca4266e2..00000000 --- a/project/biologist_chaegirab_research_system.gd +++ /dev/null @@ -1,270 +0,0 @@ -# biologist_chaegirab_research_system.gd —— Metin2 40250 官方生物学者车齿木全套调查研究链与永久属性嘉奖系统 1:1 -# 对照 40250 服务端 collect_quest_lv30.quest ~ collect_quest_lv94.quest, char.cpp -class_name BiologistChaegirabResearchSystem -extends RefCounted - -signal sample_submitted(item_name: String, success: bool, current: int, target: int) -signal capstone_submitted(stone_name: String, stage_name: String) -signal stage_completed(stage_name: String, rewards: Dictionary) - -const NPC_BIOLOGIST := 20084 # 生物学者车齿木 (Biologist Chaegirab) -const VNUM_RESEARCH_ELIXIR := 71035 # 贤者之药 (提高提交成功率至 90%) - -# 40250 官方 8 大里程碑阶段全量研究任务链 -const RESEARCH_STAGES: Array = [ - { - "stage_id": 1, - "title": "【Lv 30 研究】兽人牙齿调查", - "min_level": 30, - "sample_vnum": 30006, # 兽人牙齿 - "sample_name": "兽人牙齿", - "target_count": 10, - "soul_stone_vnum": 30220, # 兽人灵魂石 - "soul_stone_name": "兽人灵魂石", - "reward_stats": {"move_speed": 10}, - "desc": "永久移动速度 +10" - }, - { - "stage_id": 2, - "title": "【Lv 40 研究】诅咒之书调查", - "min_level": 40, - "sample_vnum": 30047, # 诅咒之书 - "sample_name": "诅咒之书", - "target_count": 15, - "soul_stone_vnum": 30221, # 诅咒灵魂石 - "soul_stone_name": "诅咒灵魂石", - "reward_stats": {"cast_speed_pct": 5}, - "desc": "永久施法速度 +5%" - }, - { - "stage_id": 3, - "title": "【Lv 50 研究】恶魔纪念物调查", - "min_level": 50, - "sample_vnum": 30015, # 恶魔纪念物 - "sample_name": "恶魔纪念物", - "target_count": 15, - "soul_stone_vnum": 30222, # 恶魔灵魂石 - "soul_stone_name": "恶魔灵魂石", - "reward_stats": {"defense": 60}, - "desc": "永久防御力 +60" - }, - { - "stage_id": 4, - "title": "【Lv 60 研究】冰之碎片调查", - "min_level": 60, - "sample_vnum": 30050, # 冰之碎片 - "sample_name": "冰之碎片", - "target_count": 20, - "soul_stone_vnum": 30223, # 冰霜灵魂石 - "soul_stone_name": "冰霜灵魂石", - "reward_stats": {"attack_power": 50}, - "desc": "永久物理攻击力 +50" - }, - { - "stage_id": 5, - "title": "【Lv 70 研究】幽灵树枝调查", - "min_level": 70, - "sample_vnum": 30165, # 幽灵树枝 - "sample_name": "幽灵树枝", - "target_count": 25, - "soul_stone_vnum": 30224, # 森林灵魂石 - "soul_stone_name": "森林灵魂石", - "reward_stats": {"move_speed": 11, "damage_reduction_pct": 10}, - "desc": "永久移动速度 +11, 伤害抗性 +10%" - }, - { - "stage_id": 6, - "title": "【Lv 80 研究】巨灵石碑调查", - "min_level": 80, - "sample_vnum": 30166, # 巨灵石碑 - "sample_name": "巨灵石碑", - "target_count": 30, - "soul_stone_vnum": 30225, # 巨灵灵魂石 - "soul_stone_name": "巨灵灵魂石", - "reward_stats": {"attack_speed": 6, "attack_pct": 10}, - "desc": "永久攻击速度 +6, 物理伤害 +10%" - }, - { - "stage_id": 7, - "title": "【Lv 85 研究】红木灵枝调查", - "min_level": 85, - "sample_vnum": 30167, # 红木灵枝 - "sample_name": "红木灵枝", - "target_count": 40, - "soul_stone_vnum": 30226, # 灵魂之石 - "soul_stone_name": "灵魂之石", - "reward_stats": {"all_resistance_pct": 10}, - "desc": "永久全属性抗性 +10%" - }, - { - "stage_id": 8, - "title": "【Lv 90 研究】首领领主证书调查", - "min_level": 90, - "sample_vnum": 30168, # 领主证书 - "sample_name": "首领领主证书", - "target_count": 50, - "soul_stone_vnum": 30227, # 首领灵魂石 - "soul_stone_name": "首领灵魂石", - "reward_stats": {"boss_damage_pct": 10}, - "desc": "永久对野外首领伤害 +10%" - } -] - -var current_stage_index: int = 0 -var current_samples: int = 0 -var waiting_for_soul_stone: bool = false -var completed_stages: Array = [] -var permanent_bonuses: Dictionary = {} - -# 获取当前正在进行的研究任务配置 -func get_current_stage() -> Dictionary: - if current_stage_index >= RESEARCH_STAGES.size(): - return {} # 已全部完成 - return RESEARCH_STAGES[current_stage_index].duplicate(true) - -# 交付样本材料 -func submit_sample(inventory: Array, slot_index: int, player_level: int, force_success: bool = false, use_elixir: bool = false) -> Dictionary: - var stage = get_current_stage() - if stage.is_empty(): - return {"ok": false, "reason": "ALL_RESEARCH_COMPLETED", "msg": "所有生物学者调查研究均已登峰造极!"} - - if player_level < int(stage["min_level"]): - return { - "ok": false, - "reason": "PLAYER_LEVEL_TOO_LOW", - "msg": "等级不足!开启该阶段研究需达到 %d 级。" % int(stage["min_level"]) - } - - if waiting_for_soul_stone: - return { - "ok": false, - "reason": "WAITING_FOR_SOUL_STONE", - "msg": "样本已全部收集齐!请交付【%s】以完成终极认证。" % stage["soul_stone_name"] - } - - if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null: - return {"ok": false, "reason": "INVALID_SLOT"} - - var item: Dictionary = inventory[slot_index] - var item_vnum := int(item.get("vnum", 0)) - if item_vnum != int(stage["sample_vnum"]): - return { - "ok": false, - "reason": "WRONG_SAMPLE_ITEM", - "msg": "车齿木摇了摇头:当前阶段需要研究的是【%s】!" % stage["sample_name"] - } - - # 消耗 1 件样本 - var cnt := int(item.get("count", 1)) - if cnt > 1: - item["count"] = cnt - 1 - else: - inventory[slot_index] = null - - # 判定成功率 (基础 60%,使用贤者之药 90%) - var success_chance := 0.90 if use_elixir else 0.60 - var is_success := force_success or (randf() <= success_chance) - - if is_success: - current_samples += 1 - sample_submitted.emit(stage["sample_name"], true, current_samples, stage["target_count"]) - - # 检查样本是否集齐 - if current_samples >= int(stage["target_count"]): - waiting_for_soul_stone = true - return { - "ok": true, - "success": true, - "current": current_samples, - "target": stage["target_count"], - "need_soul_stone": true, - "soul_stone_name": stage["soul_stone_name"], - "msg": "样本品质极高!%d 件样本已全部收集完毕,请寻找【%s】交付!" % [current_samples, stage["soul_stone_name"]] - } - - return { - "ok": true, - "success": true, - "current": current_samples, - "target": stage["target_count"], - "need_soul_stone": false, - "msg": "样本验证成功!研究进度:(%d / %d)。" % [current_samples, stage["target_count"]] - } - else: - sample_submitted.emit(stage["sample_name"], false, current_samples, stage["target_count"]) - return { - "ok": true, - "success": false, - "current": current_samples, - "target": stage["target_count"], - "need_soul_stone": false, - "msg": "学者车齿木叹息:这件样本受损严重,无法用于精炼分析。" - } - -# 交付关底灵魂石完成该阶段 -func submit_soul_stone(inventory: Array, slot_index: int, player_stats: Dictionary) -> Dictionary: - var stage = get_current_stage() - if stage.is_empty(): - return {"ok": false, "reason": "ALL_RESEARCH_COMPLETED"} - - if not waiting_for_soul_stone: - return {"ok": false, "reason": "SAMPLES_NOT_COMPLETED", "msg": "样本尚未收集齐,无法交付灵魂石!"} - - if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null: - return {"ok": false, "reason": "INVALID_SLOT"} - - var item: Dictionary = inventory[slot_index] - if int(item.get("vnum", 0)) != int(stage["soul_stone_vnum"]): - return { - "ok": false, - "reason": "WRONG_SOUL_STONE", - "msg": "需要交付的是专属的【%s】!" % stage["soul_stone_name"] - } - - # 消耗灵魂石 - var cnt := int(item.get("count", 1)) - if cnt > 1: - item["count"] = cnt - 1 - else: - inventory[slot_index] = null - - # 注入永久属性加成 - var rewards: Dictionary = stage.get("reward_stats", {}) - for k in rewards.keys(): - var val = int(rewards[k]) - player_stats[k] = int(player_stats.get(k, 0)) + val - permanent_bonuses[k] = int(permanent_bonuses.get(k, 0)) + val - - completed_stages.append(stage["stage_id"]) - current_stage_index += 1 - current_samples = 0 - waiting_for_soul_stone = false - - capstone_submitted.emit(stage["soul_stone_name"], stage["title"]) - stage_completed.emit(stage["title"], rewards) - - return { - "ok": true, - "completed_stage": stage["title"], - "rewards": rewards, - "permanent_bonuses": permanent_bonuses, - "msg": "【%s】圆满达成!永久获得属性加成:%s!" % [stage["title"], stage["desc"]] - } - -# 序列化 -func serialize() -> Dictionary: - return { - "current_stage_index": current_stage_index, - "current_samples": current_samples, - "waiting_for_soul_stone": waiting_for_soul_stone, - "completed_stages": completed_stages.duplicate(true), - "permanent_bonuses": permanent_bonuses.duplicate(true) - } - -# 反序列化 -func deserialize(data: Dictionary) -> void: - current_stage_index = int(data.get("current_stage_index", 0)) - current_samples = int(data.get("current_samples", 0)) - waiting_for_soul_stone = bool(data.get("waiting_for_soul_stone", false)) - completed_stages = data.get("completed_stages", []).duplicate(true) - permanent_bonuses = data.get("permanent_bonuses", {}).duplicate(true) diff --git a/project/biologist_chaegirab_research_system.gd.uid b/project/biologist_chaegirab_research_system.gd.uid deleted file mode 100644 index 83c5f932..00000000 --- a/project/biologist_chaegirab_research_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dvues2wv3bqb1 diff --git a/project/biologist_quest_system.gd b/project/biologist_quest_system.gd deleted file mode 100644 index 5ee3599a..00000000 --- a/project/biologist_quest_system.gd +++ /dev/null @@ -1,263 +0,0 @@ -# biologist_quest_system.gd —— Metin2 40250 生物学研究家任务链与永久属性加成系统 1:1 -# 对照 40250 服务端 collect_quest_lv30.quest ~ collect_quest_lv80.quest 及 char.cpp:ComputePoints -class_name BiologistQuestSystem -extends RefCounted - -const NPC_CHAEGIRAB := 20084 -const ITEM_SPIRAL_POTION := 71035 # 灵魂秘药 / 研究家的秘药 - -const STAGES: Array[Dictionary] = [ - { - "id": 0, - "name": "Lv30 兽人的牙齿研究", - "required_level": 30, - "collect_vnum": 30006, # 兽人齿 - "collect_name": "兽人齿", - "collect_count": 10, - "soul_stone_vnum": 30220, # 兽人灵魂石 - "soul_stone_name": "金龙之灵魂石", - "reward_type": "move_speed", - "reward_value": 10, - "reward_title": "永久移动速度 +10" - }, - { - "id": 1, - "name": "Lv40 诅咒之书研究", - "required_level": 40, - "collect_vnum": 30047, # 诅咒书 - "collect_name": "诅咒之书", - "collect_count": 15, - "soul_stone_vnum": 30221, # 诅咒灵魂石 - "soul_stone_name": "神殿之灵魂石", - "reward_type": "att_speed", - "reward_value": 5, - "reward_title": "永久攻击速度 +5" - }, - { - "id": 2, - "name": "Lv50 恶魔纪念物研究", - "required_level": 50, - "collect_vnum": 30015, # 恶魔纪念物 - "collect_name": "恶魔纪念物", - "collect_count": 15, - "soul_stone_vnum": 30222, # 恶魔灵魂石 - "soul_stone_name": "恶魔之灵魂石", - "reward_type": "def_grade", - "reward_value": 60, - "reward_title": "永久防御力 +60" - }, - { - "id": 3, - "name": "Lv60 冰球研究", - "required_level": 60, - "collect_vnum": 30050, # 冰球 - "collect_name": "冰球", - "collect_count": 20, - "soul_stone_vnum": 30223, # 冰球灵魂石 - "soul_stone_name": "雪山之灵魂石", - "reward_type": "att_grade", - "reward_value": 50, - "reward_title": "永久攻击力 +50" - }, - { - "id": 4, - "name": "Lv70 幽灵树枝研究", - "required_level": 70, - "collect_vnum": 30165, # 幽灵树枝 - "collect_name": "幽灵树枝", - "collect_count": 25, - "soul_stone_vnum": 30224, # 幽灵灵魂石 - "soul_stone_name": "鬼树之灵魂石", - "reward_type": "damage_reduction", - "reward_value": 10, - "reward_title": "受到伤害永久减少 10%" - }, - { - "id": 5, - "name": "Lv80 图基石板研究", - "required_level": 80, - "collect_vnum": 30166, # 图基石板 - "collect_name": "图基石板", - "collect_count": 30, - "soul_stone_vnum": 30225, # 图基灵魂石 - "soul_stone_name": "图基之灵魂石", - "reward_type": "att_grade_pct", - "reward_value": 10, - "reward_title": "物理攻击力永久提升 10%" - } -] - -var player_level: int = 1 -var current_stage_idx: int = -1 # -1: 未开始 -var delivered_count: int = 0 -var soul_stone_phase: bool = false -var completed_stages: Array[int] = [] -var cooldown_remaining: float = 0.0 - -var permanent_bonuses: Dictionary = { - "move_speed": 0, - "att_speed": 0, - "def_grade": 0, - "att_grade": 0, - "damage_reduction": 0, - "att_grade_pct": 0 -} - -func init_for_player(level: int) -> void: - player_level = level - _check_available_stage() - -func _check_available_stage() -> void: - if current_stage_idx >= 0 and current_stage_idx < STAGES.size(): - return # 正在进行中 - - for i in range(STAGES.size()): - if i not in completed_stages: - var stage: Dictionary = STAGES[i] - if player_level >= int(stage["required_level"]): - current_stage_idx = i - delivered_count = 0 - soul_stone_phase = false - break - -func get_current_stage() -> Dictionary: - if current_stage_idx >= 0 and current_stage_idx < STAGES.size(): - return STAGES[current_stage_idx] - return {} - -func can_submit() -> bool: - return current_stage_idx >= 0 and not soul_stone_phase and cooldown_remaining <= 0.0 - -func submit_item(inventory: Array, use_potion: bool = false, force_success: bool = false) -> Dictionary: - if current_stage_idx < 0 or current_stage_idx >= STAGES.size(): - return {"ok": false, "reason": "NO_ACTIVE_QUEST"} - if soul_stone_phase: - return {"ok": false, "reason": "NEED_SOUL_STONE"} - if cooldown_remaining > 0.0 and not use_potion: - return {"ok": false, "reason": "COOLDOWN_ACTIVE", "remaining": cooldown_remaining} - - var stage: Dictionary = STAGES[current_stage_idx] - var req_vnum: int = int(stage["collect_vnum"]) - - # 查找收集物槽位 - var item_slot := -1 - for i in range(inventory.size()): - var it = inventory[i] - if it != null and int(it.get("vnum", 0)) == req_vnum: - item_slot = i - break - - if item_slot == -1: - return {"ok": false, "reason": "MISSING_ITEM", "vnum": req_vnum} - - # 检查秘药 - var potion_consumed := false - if use_potion: - for i in range(inventory.size()): - var it = inventory[i] - if it != null and int(it.get("vnum", 0)) == ITEM_SPIRAL_POTION: - inventory[i] = null - potion_consumed = true - cooldown_remaining = 0.0 - break - - # 扣除收集物 - inventory[item_slot] = null - - # 成功判定 (默认 60%,秘药 90%) - var success_chance: float = 0.90 if potion_consumed else 0.60 - var roll: float = 0.0 if force_success else randf() - var success := roll < success_chance - - if not success: - cooldown_remaining = 0.0 if potion_consumed else 3600.0 # 1 小时 - return { - "ok": true, - "success": false, - "delivered": delivered_count, - "target": stage["collect_count"], - "msg": "样本已损坏,未能提取有效数据!" - } - - # 成功交付 - delivered_count += 1 - var target_count: int = int(stage["collect_count"]) - if delivered_count >= target_count: - soul_stone_phase = true - return { - "ok": true, - "success": true, - "delivered": delivered_count, - "target": target_count, - "stage_completed": true, - "msg": "研究样本收集完毕!接下来请获取专属灵魂石交付!" - } - else: - cooldown_remaining = 0.0 if potion_consumed else 3600.0 - return { - "ok": true, - "success": true, - "delivered": delivered_count, - "target": target_count, - "stage_completed": false, - "msg": "样本质量极佳!已记录样本数据。" - } - -func submit_soul_stone(inventory: Array) -> Dictionary: - if current_stage_idx < 0 or current_stage_idx >= STAGES.size(): - return {"ok": false, "reason": "NO_ACTIVE_QUEST"} - if not soul_stone_phase: - return {"ok": false, "reason": "NOT_IN_SOUL_STONE_PHASE"} - - var stage: Dictionary = STAGES[current_stage_idx] - var soul_vnum: int = int(stage["soul_stone_vnum"]) - - var found_slot := -1 - for i in range(inventory.size()): - var it = inventory[i] - if it != null and int(it.get("vnum", 0)) == soul_vnum: - found_slot = i - break - - if found_slot == -1: - return {"ok": false, "reason": "MISSING_SOUL_STONE", "vnum": soul_vnum} - - # 消耗灵魂石 - inventory[found_slot] = null - - # 结算阶段奖励 - var reward_type: String = String(stage["reward_type"]) - var reward_val: int = int(stage["reward_value"]) - permanent_bonuses[reward_type] = int(permanent_bonuses.get(reward_type, 0)) + reward_val - completed_stages.append(current_stage_idx) - - var finished_stage_id: int = current_stage_idx - current_stage_idx = -1 - delivered_count = 0 - soul_stone_phase = false - cooldown_remaining = 0.0 - - # 检查下一阶段 - _check_available_stage() - - return { - "ok": true, - "completed_stage": finished_stage_id, - "reward_title": stage["reward_title"], - "reward_type": reward_type, - "reward_value": reward_val, - "all_bonuses": permanent_bonuses.duplicate() - } - -func get_quest_status() -> Dictionary: - return { - "player_level": player_level, - "stage_idx": current_stage_idx, - "stage_info": get_current_stage(), - "delivered": delivered_count, - "target": int(get_current_stage().get("collect_count", 0)), - "soul_stone_phase": soul_stone_phase, - "cooldown_remaining": cooldown_remaining, - "completed_stages": completed_stages, - "permanent_bonuses": permanent_bonuses - } diff --git a/project/biologist_quest_system.gd.uid b/project/biologist_quest_system.gd.uid deleted file mode 100644 index d3b517c5..00000000 --- a/project/biologist_quest_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cqcq40dpplogo diff --git a/project/boss_summon_scroll_system.gd b/project/boss_summon_scroll_system.gd deleted file mode 100644 index 8df2b0d4..00000000 --- a/project/boss_summon_scroll_system.gd +++ /dev/null @@ -1,312 +0,0 @@ -# boss_summon_scroll_system.gd —— Metin2 40250 官方首领降临与魔石/矿脉召唤卷轴系统 1:1 -# 100% 对照 40250 服务端 special_item_group.txt, char_item.cpp (ITEM_GIFTBOX), char_manager.cpp -class_name BossSummonScrollSystem -extends RefCounted - -signal boss_summoned(mob_vnum: int, mob_name: String, spawn_pos: Vector2, is_aggressive: bool) -signal metin_summoned(metin_vnum: int, metin_name: String, spawn_pos: Vector2, level: int) -signal ore_vein_summoned(vein_vnum: int, vein_name: String, spawn_pos: Vector2) -signal summon_failed(reason: String) - -# ========================================== -# 40250 官方首领召唤卷轴 (Boss Summon Scrolls) -# 对应 special_item_group.txt Vnum 71036 ~ 71043 -# ========================================== -const BOSS_SCROLLS: Dictionary = { - 71036: { - "name": "兽人首领召唤卷轴", - "tr_name": "Kağıt-Ork Şefi", - "mob_vnum": 691, - "mob_name": "兽人首领 (Orc Chief)", - "level": 50, - "rank": 4, # BOSS - "is_aggressive": true - }, - 71037: { - "name": "密教首领召唤卷轴", - "tr_name": "Kağıt-Karanlık Lider", - "mob_vnum": 791, - "mob_name": "黑风教主/密教首领 (Dark Leader)", - "level": 54, - "rank": 4, - "is_aggressive": true - }, - 71038: { - "name": "蜘蛛女王召唤卷轴", - "tr_name": "Kağıt-Kraliçe Örümcek", - "mob_vnum": 2091, - "mob_name": "蜘蛛女王 (Spider Queen)", - "level": 60, - "rank": 4, - "is_aggressive": true - }, - 71039: { - "name": "沙漠巨龟召唤卷轴", - "tr_name": "Kağıt-Kaplumbağa", - "mob_vnum": 2191, - "mob_name": "沙漠巨龟 (Giant Desert Tortoise)", - "level": 67, - "rank": 4, - "is_aggressive": true - }, - 71040: { - "name": "烈焰魔王召唤卷轴", - "tr_name": "Kağıt-Alev Kral", - "mob_vnum": 2206, - "mob_name": "烈焰魔王 (Flame King)", - "level": 73, - "rank": 4, - "is_aggressive": true - }, - 71041: { - "name": "九尾狐召唤卷轴", - "tr_name": "Kağıt-Dokuz Kuyruk", - "mob_vnum": 1901, - "mob_name": "九尾狐仙 (Nine Tails)", - "level": 72, - "rank": 4, - "is_aggressive": true - }, - 71042: { - "name": "恶魔之王召唤卷轴", - "tr_name": "Kağıt-Şeytan Kral", - "mob_vnum": 1091, - "mob_name": "恶魔之王 (Demon King)", - "level": 75, - "rank": 4, - "is_aggressive": true - }, - 71043: { - "name": "虎灵妖兽召唤卷轴", - "tr_name": "Sarı Kaplan H. Kağıdı", - "mob_vnum": 1304, - "mob_name": "黄虎幽灵 (Tiger Ghost)", - "level": 75, - "rank": 4, - "is_aggressive": true - } -} - -# ========================================== -# 40250 官方魔石召唤卷轴 (Metin Summon Scrolls) -# 对应 special_item_group.txt Vnum 71080 ~ 71082 -# ========================================== -const METIN_SCROLLS: Dictionary = { - 71080: { - "name": "初级魔石召唤卷", - "tr_name": "Metin Taşı Çağırma Kağıdı (K)", - "pool": [ - {"vnum": 8001, "name": "悲哀之石 (Metin of Sorrow)", "level": 5}, - {"vnum": 8002, "name": "战斗之石 (Metin of Combat)", "level": 10}, - {"vnum": 8003, "name": "会战之石 (Metin of Battle)", "level": 15}, - {"vnum": 8004, "name": "贪婪之石 (Metin of Greed)", "level": 20}, - {"vnum": 8005, "name": "漆黑之石 (Metin of Black)", "level": 25} - ] - }, - 71081: { - "name": "中级魔石召唤卷", - "tr_name": "Metin Taşı Çağırma Kağıdı (O)", - "pool": [ - {"vnum": 8006, "name": "黑暗之石 (Metin of Darkness)", "level": 30}, - {"vnum": 8007, "name": "嫉妒之石 (Metin of Jealousy)", "level": 35}, - {"vnum": 8008, "name": "灵魂之石 (Metin of Soul)", "level": 40}, - {"vnum": 8009, "name": "阴影之石 (Metin of Shadow)", "level": 45}, - {"vnum": 8010, "name": "强韧之石 (Metin of Toughness)", "level": 50} - ] - }, - 71082: { - "name": "高级魔石召唤卷", - "tr_name": "Metin Taşı Çağırma Kağıdı (Z)", - "pool": [ - {"vnum": 8011, "name": "恶魔之石 (Metin of Devil)", "level": 55}, - {"vnum": 8012, "name": "陨落之石 (Metin of Fall)", "level": 60}, - {"vnum": 8013, "name": "死者之石 (Metin of Death)", "level": 65}, - {"vnum": 8014, "name": "屠杀之石 (Metin of Murder)", "level": 70}, - {"vnum": 8024, "name": "狂风之石 (Metin of Pung-Ma)", "level": 75}, - {"vnum": 8025, "name": "魔岩之石 (Metin of Ma-An)", "level": 80}, - {"vnum": 8026, "name": "斗击之石 (Metin of Tu-Gyuk)", "level": 85}, - {"vnum": 8027, "name": "绝望之石 (Metin of Jeon-Un)", "level": 90} - ] - } -} - -# ========================================== -# 40250 官方矿脉召唤卷轴 (Ore Vein Scrolls) -# 对应 special_item_group.txt Vnum 71057 ~ 71067 -# ========================================== -const ORE_VEIN_SCROLLS: Dictionary = { - 71057: {"mob_vnum": 20049, "name": "钻石矿脉 (Diamond Vein)"}, - 71058: {"mob_vnum": 20050, "name": "琥珀矿脉 (Amber Vein)"}, - 71059: {"mob_vnum": 20051, "name": "木化石矿脉 (Fossil Wood Vein)"}, - 71060: {"mob_vnum": 20052, "name": "铜矿脉 (Copper Vein)"}, - 71061: {"mob_vnum": 20053, "name": "银矿脉 (Silver Vein)"}, - 71062: {"mob_vnum": 20054, "name": "金矿脉 (Gold Vein)"}, - 71063: {"mob_vnum": 20055, "name": "翡翠矿脉 (Jade Vein)"}, - 71064: {"mob_vnum": 20056, "name": "黑檀木矿脉 (Ebony Vein)"}, - 71065: {"mob_vnum": 20057, "name": "珍珠矿脉 (Pearl Vein)"}, - 71066: {"mob_vnum": 20058, "name": "白金矿脉 (White Gold Vein)"}, - 71067: {"mob_vnum": 20059, "name": "水晶矿脉 (Crystal Vein)"} -} - -# 当前已召唤生成的实体记录列表 -var _active_summoned_entities: Array[Dictionary] = [] - -# 判断是否是 40250 原版召唤卷轴 -func is_summon_scroll(vnum: int) -> bool: - return BOSS_SCROLLS.has(vnum) or METIN_SCROLLS.has(vnum) or ORE_VEIN_SCROLLS.has(vnum) - -# 获取召唤卷轴类型类别 -func get_scroll_category(vnum: int) -> String: - if BOSS_SCROLLS.has(vnum): - return "boss" - elif METIN_SCROLLS.has(vnum): - return "metin" - elif ORE_VEIN_SCROLLS.has(vnum): - return "ore_vein" - return "" - -# 使用召唤卷轴 -func use_summon_scroll( - inventory: Array, - slot_index: int, - player_pos: Vector2, - is_in_safe_zone: bool, - forced_choice_idx: int = -1 -) -> Dictionary: - if slot_index < 0 or slot_index >= inventory.size(): - summon_failed.emit("INVALID_SLOT") - return {"ok": false, "reason": "INVALID_SLOT", "msg": "无效的背包物品格位!"} - - var item = inventory[slot_index] - if item == null: - summon_failed.emit("EMPTY_SLOT") - return {"ok": false, "reason": "EMPTY_SLOT", "msg": "指定格位没有任何道具!"} - - var vnum = int(item.get("vnum", 0)) - var category = get_scroll_category(vnum) - if category == "": - summon_failed.emit("NOT_A_SUMMON_SCROLL") - return {"ok": false, "reason": "NOT_A_SUMMON_SCROLL", "msg": "此物品并非怪物或魔石召唤卷轴!"} - - # 40250 原版规则:安全区内禁止召唤具有攻击性的首领与魔石 - if is_in_safe_zone and category != "ore_vein": - summon_failed.emit("CANNOT_SPAWN_IN_SAFEZONE") - return {"ok": false, "reason": "CANNOT_SPAWN_IN_SAFEZONE", "msg": "在安全区内无法召唤怪物!请移至野外危险区域。"} - - # 扣除卷轴消耗 (40250 item->SetCount(item->GetCount()-1)) - var current_count = int(item.get("count", 1)) - if current_count > 1: - item["count"] = current_count - 1 - else: - inventory[slot_index] = null - - # 生成随机偏移位置 (类似 40250 GetX() + number(-500, 500)) - var offset_x = randf_range(-120.0, 120.0) - var offset_y = randf_range(-120.0, 120.0) - # 确保不会生成在绝对重合点 - if abs(offset_x) < 30.0: - offset_x = 50.0 if offset_x >= 0.0 else -50.0 - if abs(offset_y) < 30.0: - offset_y = 50.0 if offset_y >= 0.0 else -50.0 - - var spawn_pos = Vector2(player_pos.x + offset_x, player_pos.y + offset_y) - var summoned_entity: Dictionary = {} - - match category: - "boss": - var boss_info = BOSS_SCROLLS[vnum] - summoned_entity = { - "vnum": int(boss_info["mob_vnum"]), - "name": str(boss_info["mob_name"]), - "level": int(boss_info["level"]), - "rank": int(boss_info["rank"]), - "pos": spawn_pos, - "is_aggressive": true, - "category": "boss", - "summoner_pos": player_pos - } - _active_summoned_entities.append(summoned_entity) - boss_summoned.emit(summoned_entity["vnum"], summoned_entity["name"], spawn_pos, true) - - "metin": - var metin_scroll = METIN_SCROLLS[vnum] - var pool: Array = metin_scroll["pool"] - var picked_idx = 0 - if forced_choice_idx >= 0 and forced_choice_idx < pool.size(): - picked_idx = forced_choice_idx - else: - picked_idx = randi() % pool.size() - - var picked_metin = pool[picked_idx] - summoned_entity = { - "vnum": int(picked_metin["vnum"]), - "name": str(picked_metin["name"]), - "level": int(picked_metin["level"]), - "rank": 5, # STONE - "pos": spawn_pos, - "is_aggressive": false, - "category": "metin", - "summoner_pos": player_pos - } - _active_summoned_entities.append(summoned_entity) - metin_summoned.emit(summoned_entity["vnum"], summoned_entity["name"], spawn_pos, summoned_entity["level"]) - - "ore_vein": - var vein_info = ORE_VEIN_SCROLLS[vnum] - summoned_entity = { - "vnum": int(vein_info["mob_vnum"]), - "name": str(vein_info["name"]), - "pos": spawn_pos, - "is_aggressive": false, - "category": "ore_vein", - "summoner_pos": player_pos - } - _active_summoned_entities.append(summoned_entity) - ore_vein_summoned.emit(summoned_entity["vnum"], summoned_entity["name"], spawn_pos) - - return { - "ok": true, - "category": category, - "entity": summoned_entity, - "msg": "卷轴散发出一道耀眼的光芒,强大的封印能量破空而出!" - } - -# 获取当前已召唤的所有活跃实体 -func get_active_summoned_entities() -> Array[Dictionary]: - return _active_summoned_entities - -# 清空/移除已击败或离开的实体 -func remove_summoned_entity(index: int) -> bool: - if index >= 0 and index < _active_summoned_entities.size(): - _active_summoned_entities.remove_at(index) - return true - return false - -func clear_summoned_entities() -> void: - _active_summoned_entities.clear() - -# 存档与读档支持 -func serialize() -> Dictionary: - var list_copy: Array = [] - for e in _active_summoned_entities: - var pos_dict = {"x": e["pos"].x, "y": e["pos"].y} - var copy_e = e.duplicate() - copy_e["pos"] = pos_dict - if copy_e.has("summoner_pos"): - copy_e["summoner_pos"] = {"x": e["summoner_pos"].x, "y": e["summoner_pos"].y} - list_copy.append(copy_e) - return { - "active_entities": list_copy - } - -func deserialize(data: Dictionary) -> void: - _active_summoned_entities.clear() - var raw_list = data.get("active_entities", []) - for item in raw_list: - var pos_data = item.get("pos", {"x": 0.0, "y": 0.0}) - var ent = item.duplicate() - ent["pos"] = Vector2(pos_data.get("x", 0.0), pos_data.get("y", 0.0)) - if ent.has("summoner_pos"): - var sp_data = ent.get("summoner_pos", {"x": 0.0, "y": 0.0}) - ent["summoner_pos"] = Vector2(sp_data.get("x", 0.0), sp_data.get("y", 0.0)) - _active_summoned_entities.append(ent) diff --git a/project/boss_summon_scroll_system.gd.uid b/project/boss_summon_scroll_system.gd.uid deleted file mode 100644 index 9b55a523..00000000 --- a/project/boss_summon_scroll_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://xjrr61n0rkui diff --git a/project/chat_hyperlink_dice_system.gd b/project/chat_hyperlink_dice_system.gd deleted file mode 100644 index 8e3ab7ca..00000000 --- a/project/chat_hyperlink_dice_system.gd +++ /dev/null @@ -1,147 +0,0 @@ -# chat_hyperlink_dice_system.gd —— 40250 聊天物品超链接与 /dice 掷骰系统 1:1 -# 严格对照: -# ClientVS22/source/EterLib/TextTag.cpp:32 (Hyperlink |Hitem:vnum:flags:socket:attr|h[%s]|h) -# Client/Eternexus/root/uichat.py & uitooltip.py (ItemHyperlink, SetHyperlinkItem) -# metin2/src/server/game/src/cmd_general.cpp:2392 (do_dice) -extends RefCounted - -const LINK_COLOR := "ffffc700" # 官方物品链接专用亮金黄色 - -# 1. 将物品数据编码为 40250 官方超链接格式 -# 格式: |cffffc700|Hitem:vnum:flags:s0:s1:s2:a0_t:a0_v:a1_t:a1_v:...|h[物品名]|h|r -static func encode_item_hyperlink(item_data: Dictionary) -> String: - var vnum: int = item_data.get("vnum", 0) - var name: String = item_data.get("name", "Unknown Item") - var flags: int = item_data.get("flags", 0) - - var sockets: Array = item_data.get("sockets", [0, 0, 0]) - while sockets.size() < 3: - sockets.append(0) - - var attrs: Array = item_data.get("attributes", []) - var attr_parts: Array = [] - for a in attrs: - attr_parts.append("%d:%d" % [a.get("type", 0), a.get("value", 0)]) - - var payload_parts := [ - str(vnum), - str(flags), - str(sockets[0]), - str(sockets[1]), - str(sockets[2]) - ] - if not attr_parts.is_empty(): - payload_parts.append((":").join(attr_parts)) - - var payload := (":").join(payload_parts) - return "|c%s|Hitem:%s|h[%s]|h|r" % [LINK_COLOR, payload, name] - -# 2. 从超链接中解析提取物品完整数据结构 (1:1 TextTag::FindHyperlink) -static func decode_item_hyperlink(hyperlink_text: String) -> Dictionary: - var res := { - "valid": false, - "vnum": 0, - "name": "", - "flags": 0, - "sockets": [0, 0, 0], - "attributes": [] - } - - # 正则或字符串匹配提取 |Hitem:(.+?)\|h\[(.*?)\]\|h - var h_start := hyperlink_text.find("|Hitem:") - if h_start == -1: - return res - - var payload_start := h_start + 7 # len("|Hitem:") - var payload_end := hyperlink_text.find("|h[", payload_start) - if payload_end == -1: - return res - - var payload := hyperlink_text.substr(payload_start, payload_end - payload_start) - - var name_start := payload_end + 3 # len("|h[") - var name_end := hyperlink_text.find("]|h", name_start) - if name_end == -1: - return res - - var display_name := hyperlink_text.substr(name_start, name_end - name_start) - res["name"] = display_name - - var tokens := payload.split(":") - if tokens.size() < 5: - return res - - res["vnum"] = tokens[0].to_int() - res["flags"] = tokens[1].to_int() - res["sockets"] = [tokens[2].to_int(), tokens[3].to_int(), tokens[4].to_int()] - - # 解析属性对 (type:value) - var attrs: Array = [] - var idx := 5 - while idx + 1 < tokens.size(): - var at_type := tokens[idx].to_int() - var at_val := tokens[idx + 1].to_int() - attrs.append({ "type": at_type, "value": at_val }) - idx += 2 - - res["attributes"] = attrs - res["valid"] = true - return res - -# 3. 模拟 Tooltip 生成(悬停时渲染内容) -static func build_tooltip_text(item_dict: Dictionary) -> String: - if not item_dict.get("valid", false): - return "无效的物品链接" - - var lines: Array = [] - lines.append("=== %s (Vnum: %d) ===" % [item_dict["name"], item_dict["vnum"]]) - - # 宝石孔位信息 - var sockets: Array = item_dict.get("sockets", []) - for i in range(sockets.size()): - var s: int = sockets[i] - if s > 0: - lines.append("孔位 %d: 镶嵌宝石 [Vnum %d]" % [i + 1, s]) - elif s == 1: # 官方孔位开孔标记 - lines.append("孔位 %d: 空孔" % [i + 1]) - - # 附加属性信息 (1~7 条) - var attrs: Array = item_dict.get("attributes", []) - for a in attrs: - lines.append("附加属性: 类型 %d +%d" % [a["type"], a["value"]]) - - return ("\n").join(lines) - -# 4. 40250 官方 /dice 掷骰系统 (1:1 ACMD(do_dice)) -static func execute_dice(player_name: String, args: Array = [], roll_override: int = -1) -> Dictionary: - var start_val := 1 - var end_val := 100 - - if args.size() >= 2: - start_val = int(args[0]) - end_val = int(args[1]) - elif args.size() == 1: - start_val = 1 - end_val = int(args[0]) - - # 40250 规则: end = MAX(start, end); start = MIN(start, end); - var real_start := mini(start_val, end_val) - var real_end := maxi(start_val, end_val) - - var roll_result: int = 0 - if roll_override >= 0: - roll_result = clamp(roll_override, real_start, real_end) - else: - roll_result = randi() % (real_end - real_start + 1) + real_start - - var broadcast_msg := "%s 投掷了骰子,点数为 %d (%d-%d)" % [ - player_name, roll_result, real_start, real_end - ] - - return { - "player": player_name, - "roll": roll_result, - "start": real_start, - "end": real_end, - "message": broadcast_msg - } diff --git a/project/chat_hyperlink_dice_system.gd.uid b/project/chat_hyperlink_dice_system.gd.uid deleted file mode 100644 index 8fc9d856..00000000 --- a/project/chat_hyperlink_dice_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bnktthkdod33w diff --git a/project/class_specialization_system.gd b/project/class_specialization_system.gd deleted file mode 100644 index 0f9069ba..00000000 --- a/project/class_specialization_system.gd +++ /dev/null @@ -1,233 +0,0 @@ -# class_specialization_system.gd —— Metin2 40250 角色 30 级双流派转职与无损洗髓系统 1:1 -# 对照 40250 服务端 quest/skill_group.quest, char_skill.cpp, cmd_general.cpp -class_name ClassSpecializationSystem -extends RefCounted - -signal specialization_chosen(job: int, spec_id: int, spec_name: String) -signal skill_point_allocated(skill_vnum: int, new_level: int, remaining_points: int) -signal specialization_reset(refunded_points: int) - -const VNUM_RESET_SCROLL := 71100 # 洗髓卷轴 / 重新分配技能书 (Scroll of Redistribution) -const REQUIRED_LEVEL := 30 # 转职等级门槛 - -const SPEC_TREE: Dictionary = { - 0: { # 战士 - 1: { - "name": "战狂流派 (Arahan)", - "desc": "极度偏向狂暴输出与高速连斩,兼具霸体与破坏力", - "skills": { - 1: "三连斩", - 2: "旋风斩", - 3: "狂战士", - 4: "剑气", - 5: "冲锋" - } - }, - 2: { - "name": "圣战流派 (Partisan)", - "desc": "坚若磐石的钢铁之躯,以强身术与精神打击粉碎强敌", - "skills": { - 16: "强身术", - 17: "精神打击", - 18: "宝剑斩", - 19: "震地波", - 20: "剑气斩" - } - } - }, - 1: { # 刺客 - 1: { - "name": "近战刺客流派 (Dagger)", - "desc": "隐身于阴影之中,擅长淬毒与致命近身背刺", - "skills": { - 31: "伏击", - 32: "绝杀", - 33: "匕首旋风", - 34: "隐身术", - 35: "毒刃" - } - }, - 2: { - "name": "远程弓箭流派 (Archer)", - "desc": "千里之外取敌首级,漫天箭雨与火炎穿透射击", - "skills": { - 46: "连射", - 47: "箭雨", - 48: "火炎箭", - 49: "疾风步", - 50: "毒箭" - } - } - }, - 2: { # 修罗 - 1: { - "name": "魔剑修罗流派 (Weapon)", - "desc": "附魔之刃附体,吸血噬魂与破魔剑气并存", - "skills": { - 61: "附魔之刃", - 62: "附魔之甲", - 63: "惧意", - 64: "龙卷风", - 65: "手指戳击" - } - }, - 2: { - "name": "黑魔法修罗流派 (Black Magic)", - "desc": "掌控幽冥暗焰,幽暗之盾抵消伤害,黑暗之球远程轰杀", - "skills": { - 76: "幽冥一击", - 77: "烈焰一击", - 78: "黑暗之球", - 79: "幽暗之盾", - 80: "鬼影步" - } - } - }, - 3: { # 萨满 - 1: { - "name": "巨龙神圣辅助流派 (Dragon)", - "desc": "掌控巨龙图腾之力,为团队提供巨龙之赐与龙之协助光环", - "skills": { - 91: "飞龙射击", - 92: "巨龙射击", - 93: "巨龙咆哮", - 94: "巨龙之赐", - 95: "龙之协助" - } - }, - 2: { - "name": "雷电神圣治疗流派 (Lightning)", - "desc": "引动九天雷电轰杀群敌,施展神圣治愈术与疾风迅捷", - "skills": { - 106: "雷电箭", - 107: "召唤雷电", - 108: "雷电之爪", - 109: "治愈术", - 110: "迅捷步" - } - } - } -} - -# 获取职业可选流派 -func get_available_specializations(job: int) -> Dictionary: - return SPEC_TREE.get(job, {}) - -# 进行 30 级转职选定流派 -func choose_specialization(spec_id: int, player_data: Dictionary) -> Dictionary: - var job: int = int(player_data.get("job", 0)) - var lvl: int = int(player_data.get("level", 1)) - - if lvl < REQUIRED_LEVEL: - return {"ok": false, "reason": "LEVEL_TOO_LOW", "msg": "转职需要角色等级达到 30 级!当前等级: %d" % lvl} - - var cur_group: int = int(player_data.get("skill_group", 0)) - if cur_group != 0: - return {"ok": false, "reason": "ALREADY_SPECIALIZED", "msg": "你已选定了流派!若需变更,请先使用【洗髓卷轴】。"} - - if not SPEC_TREE.has(job) or not SPEC_TREE[job].has(spec_id): - return {"ok": false, "reason": "INVALID_SPEC_ID", "msg": "无效的流派代码!"} - - var spec = SPEC_TREE[job][spec_id] - player_data["skill_group"] = spec_id - - # 初始化该流派专属 5 大技能 - if not player_data.has("skills"): - player_data["skills"] = {} - - for svnum in spec["skills"].keys(): - player_data["skills"][svnum] = { - "name": spec["skills"][svnum], - "level": 0, - "master_type": 0 - } - - specialization_chosen.emit(job, spec_id, spec["name"]) - - return { - "ok": true, - "job": job, - "spec_id": spec_id, - "spec_name": spec["name"], - "skills_count": spec["skills"].size(), - "msg": "恭喜!成功完成 30 级转职,踏入【%s】流派之道!" % spec["name"] - } - -# 学习与加点技能 -func allocate_skill_point(skill_vnum: int, player_data: Dictionary) -> Dictionary: - var cur_group: int = int(player_data.get("skill_group", 0)) - if cur_group == 0: - return {"ok": false, "reason": "NOT_SPECIALIZED", "msg": "尚未进行 30 级转职,无法修习流派绝学!"} - - var pts: int = int(player_data.get("skill_points", 0)) - if pts <= 0: - return {"ok": false, "reason": "NO_SKILL_POINTS", "msg": "技能点数不足!"} - - var skills: Dictionary = player_data.get("skills", {}) - if not skills.has(skill_vnum): - return {"ok": false, "reason": "SKILL_NOT_AVAILABLE", "msg": "此技能不属于当前专精流派!"} - - var sk = skills[skill_vnum] - var cur_lvl: int = int(sk.get("level", 0)) - if cur_lvl >= 20: - return {"ok": false, "reason": "MAX_NORMAL_LEVEL", "msg": "该技能基础等级已满 (20),进阶需使用技能秘籍!"} - - sk["level"] = cur_lvl + 1 - player_data["skill_points"] = pts - 1 - - skill_point_allocated.emit(skill_vnum, sk["level"], player_data["skill_points"]) - - return { - "ok": true, - "skill_vnum": skill_vnum, - "new_level": sk["level"], - "remaining_points": player_data["skill_points"], - "msg": "【%s】提升至等级 %d!" % [sk.get("name", "技能"), sk["level"]] - } - -# 使用洗髓卷轴 (71100) 无损重置流派与返还技能点 -func reset_specialization(inventory: Array, player_data: Dictionary, free_reset: bool = false) -> Dictionary: - var cur_group: int = int(player_data.get("skill_group", 0)) - if cur_group == 0: - return {"ok": false, "reason": "NOT_SPECIALIZED", "msg": "当前尚未选定任何专精流派,无需洗髓!"} - - # 卷轴消耗检查 - var scroll_slot = -1 - if not free_reset: - for i in range(inventory.size()): - if inventory[i] != null and int(inventory[i].get("vnum", 0)) == VNUM_RESET_SCROLL: - scroll_slot = i - break - if scroll_slot == -1: - return {"ok": false, "reason": "NEED_RESET_SCROLL", "msg": "需要【洗髓卷轴】(71100) 才能重置专精流派!"} - - # 统计已投入的所有技能点数 - var skills: Dictionary = player_data.get("skills", {}) - var total_invested: int = 0 - for svnum in skills.keys(): - var sk = skills[svnum] - total_invested += int(sk.get("level", 0)) - - # 消耗卷轴 - if scroll_slot != -1: - var sc_item = inventory[scroll_slot] - var sc_cnt = int(sc_item.get("count", 1)) - if sc_cnt > 1: - sc_item["count"] = sc_cnt - 1 - else: - inventory[scroll_slot] = null - - # 全额返还技能点 - var cur_pts: int = int(player_data.get("skill_points", 0)) - player_data["skill_points"] = cur_pts + total_invested - player_data["skill_group"] = 0 - player_data["skills"] = {} - - specialization_reset.emit(total_invested) - - return { - "ok": true, - "refunded_points": total_invested, - "total_skill_points": player_data["skill_points"], - "msg": "洗髓成功!经脉重铸,%d 点技能点数全额返还,可重新选定双分支流派!" % total_invested - } diff --git a/project/class_specialization_system.gd.uid b/project/class_specialization_system.gd.uid deleted file mode 100644 index 9a1612a0..00000000 --- a/project/class_specialization_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cxlbtf4m6ejc4 diff --git a/project/classic_pet_system.gd b/project/classic_pet_system.gd deleted file mode 100644 index a2ebbd94..00000000 --- a/project/classic_pet_system.gd +++ /dev/null @@ -1,203 +0,0 @@ -# classic_pet_system.gd —— Metin2 40250 经典封印宠物跟随与守护系统 1:1 -# 对照 40250 服务端 PetSystem.cpp, PetSystem.h, questlua_pet.cpp -class_name ClassicPetSystem -extends RefCounted - -signal pet_summoned(mob_vnum: int, pet_name: String, buffs: Dictionary) -signal pet_unsummoned(mob_vnum: int, pet_name: String) -signal pet_moved(current_pos: Vector2, is_teleport: bool) - -# 40250 官方经典宠物召唤 Mob Vnum -const MOB_BABY_BOAR := 34001 # 幼野猪 -const MOB_BABY_WOLF := 34002 # 幼狼 -const MOB_BABY_TIGER := 34003 # 幼虎 -const MOB_BABY_LION := 34004 # 幼狮 -const MOB_BABY_FIREDRAKE := 34005 # 幼火龙 -const MOB_ICE_PHOENIX := 34006 # 冰凤凰 -const MOB_BABY_AZRAEL := 34009 # 幼小阿兹瑞尔 - -# 40250 官方经典封印物品 Vnum -const SEAL_BABY_BOAR := 53001 # 幼野猪封印 -const SEAL_BABY_FIREDRAKE := 53002 # 幼火龙封印 -const SEAL_ICE_PHOENIX := 53003 # 冰凤凰封印 -const SEAL_BABY_LION := 53004 # 幼狮封印 -const SEAL_BABY_AZRAEL := 53005 # 幼小阿兹瑞尔封印 - -# 40250 AI 跟随与瞬移距离参数 -const MIN_FOLLOW_DIST := 2.0 # 最小跟随停步距离 (米) -const MAX_TELEPORT_DIST := 15.0 # 最大跟随超距,超过直接瞬移到身边 (米) -const FOLLOW_SPEED := 6.0 # 跟随移动速度 - -# 40250 宠物封印配置表 -const PET_SEAL_CONFIGS: Dictionary = { - SEAL_BABY_BOAR: { - "mob_vnum": MOB_BABY_BOAR, - "name": "幼野猪", - "buffs": {"monster_damage_pct": 15}, - "dungeon_only": false - }, - SEAL_BABY_FIREDRAKE: { - "mob_vnum": MOB_BABY_FIREDRAKE, - "name": "幼火龙", - "buffs": {"max_hp": 1500, "attack_pct": 15}, - "dungeon_only": false - }, - SEAL_ICE_PHOENIX: { - "mob_vnum": MOB_ICE_PHOENIX, - "name": "冰凤凰", - "buffs": {"max_hp": 1500, "defense_pct": 15}, - "dungeon_only": false - }, - SEAL_BABY_LION: { - "mob_vnum": MOB_BABY_LION, - "name": "幼狮", - "buffs": {"crit_pct": 10}, - "dungeon_only": false - }, - SEAL_BABY_AZRAEL: { - "mob_vnum": MOB_BABY_AZRAEL, - "name": "小阿兹瑞尔", - "buffs": {"dungeon_attack_pct": 20, "max_hp": 1000}, - "dungeon_only": true # 40250 源码判定:若不在地牢中,专属地牢增益不生效 - } -} - -# 当前宠物出战状态 -var is_summoned: bool = false -var active_mob_vnum: int = 0 -var active_seal_vnum: int = 0 -var active_pet_name: String = "" -var active_buffs: Dictionary = {} -var pet_pos: Vector2 = Vector2.ZERO - -# 检查物品是否为 40250 宠物封印 -static func is_pet_seal(vnum: int) -> bool: - return PET_SEAL_CONFIGS.has(vnum) - -# 获取宠物封印配置 -static func get_pet_config_by_seal(seal_vnum: int) -> Dictionary: - return PET_SEAL_CONFIGS.get(seal_vnum, {}).duplicate(true) - -# 召唤经典封印宠物 (40250 CPetSystem::Summon) -func summon_pet(seal_item: Dictionary, player_stats: Dictionary, is_in_dungeon: bool = false, owner_pos: Vector2 = Vector2.ZERO) -> Dictionary: - var seal_vnum := int(seal_item.get("vnum", 0)) - if not PET_SEAL_CONFIGS.has(seal_vnum): - return {"ok": false, "reason": "NOT_A_PET_SEAL", "msg": "该物品不是有效的宠物封印!"} - - # 若已有宠物出战,先解散旧宠物 (40250 规则:同时只出战一只) - if is_summoned: - unsummon_pet(player_stats) - - var conf: Dictionary = PET_SEAL_CONFIGS[seal_vnum] - active_mob_vnum = conf["mob_vnum"] - active_seal_vnum = seal_vnum - active_pet_name = conf["name"] - pet_pos = owner_pos + Vector2(1.5, 0.0) # 出生在身侧 - - # 计算注入的 Buff (40250 GiveBuff 规则) - var buffs_to_apply: Dictionary = {} - var base_buffs: Dictionary = conf.get("buffs", {}) - var is_dungeon_only: bool = bool(conf.get("dungeon_only", false)) - - for stat_key in base_buffs.keys(): - var val = base_buffs[stat_key] - # 40250 特殊判定:小阿兹瑞尔等特定宠物仅在地牢中激活特定地牢 Buff - if stat_key == "dungeon_attack_pct": - if is_in_dungeon: - buffs_to_apply[stat_key] = val - else: - buffs_to_apply[stat_key] = val - - # 应用属性加成至玩家 - _apply_buffs_to_player(buffs_to_apply, player_stats, 1) - active_buffs = buffs_to_apply - is_summoned = true - - pet_summoned.emit(active_mob_vnum, active_pet_name, active_buffs) - - return { - "ok": true, - "mob_vnum": active_mob_vnum, - "pet_name": active_pet_name, - "buffs": active_buffs, - "pos": pet_pos, - "msg": "成功召唤宠物【%s】!守护属性已激活。" % active_pet_name - } - -# 召回/解散宠物 (40250 CPetSystem::Unsummon / ClearBuff) -func unsummon_pet(player_stats: Dictionary) -> Dictionary: - if not is_summoned: - return {"ok": false, "reason": "NO_PET_SUMMONED", "msg": "当前没有出战的宠物!"} - - # 清除施加在玩家身上的属性 - _apply_buffs_to_player(active_buffs, player_stats, -1) - - var old_mob := active_mob_vnum - var old_name := active_pet_name - - is_summoned = false - active_mob_vnum = 0 - active_seal_vnum = 0 - active_pet_name = "" - active_buffs.clear() - pet_pos = Vector2.ZERO - - pet_unsummoned.emit(old_mob, old_name) - - return { - "ok": true, - "mob_vnum": old_mob, - "pet_name": old_name, - "msg": "宠物【%s】已召回,守护属性已解除。" % old_name - } - -# 跟随 AI 逻辑步进 (40250 CPetActor::_UpdateFollowAI) -func update_follow_ai(owner_pos: Vector2, delta: float) -> Dictionary: - if not is_summoned: - return {"action": "none"} - - var dist := pet_pos.distance_to(owner_pos) - - # 超过最大瞬移距离,直接瞬移回主人身边 - if dist > MAX_TELEPORT_DIST: - pet_pos = owner_pos + Vector2(1.5, 0.0) - pet_moved.emit(pet_pos, true) - return {"action": "teleport", "pos": pet_pos, "dist": dist} - - # 超过最小跟随距离,平滑朝主人位移 - if dist > MIN_FOLLOW_DIST: - var move_dist: float = minf(FOLLOW_SPEED * delta, dist - MIN_FOLLOW_DIST) - pet_pos = pet_pos.move_toward(owner_pos, move_dist) - pet_moved.emit(pet_pos, false) - return {"action": "follow", "pos": pet_pos, "dist": pet_pos.distance_to(owner_pos)} - - # 处于舒适距离,保持待机 - return {"action": "idle", "pos": pet_pos, "dist": dist} - -# 属性加成注入或反向扣减 -func _apply_buffs_to_player(buffs: Dictionary, player_stats: Dictionary, factor: int) -> void: - for stat_key in buffs.keys(): - var val = int(buffs[stat_key]) * factor - var cur = int(player_stats.get(stat_key, 0)) - player_stats[stat_key] = cur + val - -# 序列化 -func serialize() -> Dictionary: - return { - "is_summoned": is_summoned, - "active_mob_vnum": active_mob_vnum, - "active_seal_vnum": active_seal_vnum, - "active_pet_name": active_pet_name, - "active_buffs": active_buffs.duplicate(true), - "pos_x": pet_pos.x, - "pos_y": pet_pos.y - } - -# 反序列化 -func deserialize(data: Dictionary) -> void: - is_summoned = bool(data.get("is_summoned", false)) - active_mob_vnum = int(data.get("active_mob_vnum", 0)) - active_seal_vnum = int(data.get("active_seal_vnum", 0)) - active_pet_name = str(data.get("active_pet_name", "")) - active_buffs = data.get("active_buffs", {}).duplicate(true) - pet_pos = Vector2(float(data.get("pos_x", 0.0)), float(data.get("pos_y", 0.0))) diff --git a/project/classic_pet_system.gd.uid b/project/classic_pet_system.gd.uid deleted file mode 100644 index e6de9192..00000000 --- a/project/classic_pet_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://brl1ecflh4ugi diff --git a/project/client_phase_integration_system.gd b/project/client_phase_integration_system.gd deleted file mode 100644 index 37a86275..00000000 --- a/project/client_phase_integration_system.gd +++ /dev/null @@ -1,192 +0,0 @@ -# client_phase_integration_system.gd —— 40250 客户端全状态机与全流程一体化集成系统 1:1 -# 严格对照: -# Client/Eternexus/root/networkmodule.py, interfacemodule.py, game.py -# ClientVS22/source/UserInterface/PythonNetworkStream.h (PHASE_*) -extends RefCounted - -const IntroLoginPhase = preload("res://intro_login_phase.gd") -const IntroEmpirePhase = preload("res://intro_empire_phase.gd") -const IntroSelectPhase = preload("res://intro_select_phase.gd") -const IntroCreatePhase = preload("res://intro_create_phase.gd") -const IntroLoadingPhase = preload("res://intro_loading_phase.gd") - -const MessengerSystem = preload("res://messenger_system.gd") -const WhisperChatSystem = preload("res://whisper_chat_system.gd") -const AtlasNavigationSystem = preload("res://atlas_navigation_system.gd") -const EquipmentInspectSystem = preload("res://equipment_inspect_system.gd") -const BgmJukeboxSystem = preload("res://bgm_jukebox_system.gd") - -# 官方 Phase 枚举 (PythonNetworkStream.h) -enum ClientPhase { - PHASE_OFFLINE, - PHASE_LOGIN, - PHASE_EMPIRE, - PHASE_SELECT, - PHASE_CREATE, - PHASE_LOADING, - PHASE_GAME -} - -var current_phase: int = ClientPhase.PHASE_LOGIN - -# 阶段子模块实例 -var login_phase: RefCounted = null -var empire_phase: RefCounted = null -var select_phase: RefCounted = null -var create_phase: RefCounted = null -var loading_phase: RefCounted = null - -# 游戏内核心交互模块实例 -var messenger: RefCounted = null -var whisper: RefCounted = null -var atlas: RefCounted = null -var equip_inspect: RefCounted = null -var jukebox: RefCounted = null - -# 活跃角色数据 -var active_character: Dictionary = {} -var active_account: String = "" - -# 热键弹窗焦点栈 (用于 Escape 键自顶向下关闭) -var dialog_focus_stack: Array = [] - -func _init() -> void: - current_phase = ClientPhase.PHASE_LOGIN - active_character = {} - active_account = "" - dialog_focus_stack.clear() - - # 初始化各阶段系统 - login_phase = IntroLoginPhase.new() - empire_phase = IntroEmpirePhase.new() - select_phase = IntroSelectPhase.new() - create_phase = IntroCreatePhase.new() - loading_phase = IntroLoadingPhase.new() - - # 初始化游戏内交互系统 - messenger = MessengerSystem.new() - whisper = WhisperChatSystem.new() - atlas = AtlasNavigationSystem.new() - equip_inspect = EquipmentInspectSystem.new() - jukebox = BgmJukeboxSystem.new() - -# 1. 登录跳转流 (Login -> Select / Empire) -func process_login(account: String, password_input: String) -> Dictionary: - var r = login_phase.attempt_login(account, password_input) - if not r["success"]: - return r - - active_account = account - if r.get("has_characters", false): - current_phase = ClientPhase.PHASE_SELECT - r["next_phase"] = ClientPhase.PHASE_SELECT - else: - current_phase = ClientPhase.PHASE_EMPIRE - r["next_phase"] = ClientPhase.PHASE_EMPIRE - - return r - -# 2. 阵营抉择流 (Empire -> Create) -func process_empire_select(empire_id: int) -> Dictionary: - if current_phase != ClientPhase.PHASE_EMPIRE: - return { "success": false, "error": "NOT_IN_EMPIRE_PHASE" } - - empire_phase.select_empire(empire_id) - var conf = empire_phase.confirm_empire_selection() - current_phase = ClientPhase.PHASE_CREATE - conf["next_phase"] = ClientPhase.PHASE_CREATE - return conf - -# 3. 角色塑形流 (Create -> Select) -func process_character_create(name: String, empire_id: int = 1) -> Dictionary: - if current_phase != ClientPhase.PHASE_CREATE: - return { "success": false, "error": "NOT_IN_CREATE_PHASE" } - - var r = create_phase.create_character(name, empire_id) - if not r["success"]: - return r - - # 存入角色选择大厅首个空槽位 - select_phase.set_character_slot(0, r["character"]) - current_phase = ClientPhase.PHASE_SELECT - r["next_phase"] = ClientPhase.PHASE_SELECT - return r - -# 4. 选择角色出战流 (Select -> Loading -> Game) -func process_character_start(slot_idx: int) -> Dictionary: - if current_phase != ClientPhase.PHASE_SELECT: - return { "success": false, "error": "NOT_IN_SELECT_PHASE" } - - select_phase.select_slot(slot_idx) - var r = select_phase.start_game() - if not r["success"]: - return r - - active_character = r["character"] - - # 启动过场加载 - loading_phase.start_loading() - current_phase = ClientPhase.PHASE_LOADING - - # 完成 4 阶段加载 - for i in range(5): - loading_phase.advance_loading_step() - - if loading_phase.is_completed(): - current_phase = ClientPhase.PHASE_GAME - jukebox.on_enter_map("metin2_map_a1") - messenger.main_player_name = active_character.get("name", "Hero") - whisper.main_player_name = active_character.get("name", "Hero") - r["next_phase"] = ClientPhase.PHASE_GAME - - return r - -# 5. 全局热键路由与窗口调度 (1:1 interfacemodule.py) -func handle_input_hotkey(key_name: String) -> Dictionary: - if current_phase != ClientPhase.PHASE_GAME: - return { "handled": false, "phase": current_phase } - - match key_name: - "Alt+M": # 好友面板 - var is_showing: bool = (dialog_focus_stack.has("MESSENGER")) - if is_showing: - dialog_focus_stack.erase("MESSENGER") - return { "handled": true, "action": "CLOSE_MESSENGER" } - else: - dialog_focus_stack.append("MESSENGER") - return { "handled": true, "action": "OPEN_MESSENGER" } - - "M": # Atlas 全景大地图 - var visible = atlas.toggle_atlas() - if visible: - dialog_focus_stack.append("ATLAS") - return { "handled": true, "action": "OPEN_ATLAS" } - else: - dialog_focus_stack.erase("ATLAS") - return { "handled": true, "action": "CLOSE_ATLAS" } - - "Escape": # 自顶向下关闭弹窗或呼出系统菜单 - if not dialog_focus_stack.is_empty(): - var top_dialog = dialog_focus_stack.pop_back() - if top_dialog == "ATLAS": - atlas.hide_atlas() - return { "handled": true, "action": "CLOSED_TOP_DIALOG", "dialog": top_dialog } - else: - return { "handled": true, "action": "OPEN_SYSTEM_MENU" } - - return { "handled": false } - -# 6. 登出游戏 (Game -> Select / Login) -func logout_to_select() -> void: - current_phase = ClientPhase.PHASE_SELECT - active_character = {} - dialog_focus_stack.clear() - atlas.hide_atlas() - jukebox.on_enter_map("intro_select") - -func logout_to_login() -> void: - current_phase = ClientPhase.PHASE_LOGIN - active_character = {} - active_account = "" - dialog_focus_stack.clear() - jukebox.on_enter_map("intro_login") diff --git a/project/client_phase_integration_system.gd.uid b/project/client_phase_integration_system.gd.uid deleted file mode 100644 index ffdd1e04..00000000 --- a/project/client_phase_integration_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://n55fydu6qdrj diff --git a/project/combat_replay_snapshot_system.gd b/project/combat_replay_snapshot_system.gd deleted file mode 100644 index 56cd5b45..00000000 --- a/project/combat_replay_snapshot_system.gd +++ /dev/null @@ -1,170 +0,0 @@ -# combat_replay_snapshot_system.gd —— Metin2 40250 战斗高光战报与首领击杀瞬间快照 1:1 -# 对照 40250 服务端 battle.cpp, packet.h, char_battle.cpp -class_name CombatReplaySnapshotSystem -extends RefCounted - -signal combat_session_started(target_name: String) -signal combat_highlight_generated(card: Dictionary) -signal new_personal_record(target_name: String, record_type: String, new_val: int) - -const MAX_STORED_CARDS := 20 - -var is_in_session: bool = false -var session_id: int = 1 -var target_vnum: int = 0 -var target_name: String = "" -var target_max_hp: int = 0 - -var session_start_time: float = 0.0 -var elapsed_time: float = 0.0 - -var total_damage_dealt: int = 0 -var total_damage_taken: int = 0 -var total_attacks: int = 0 -var total_skills_used: int = 0 -var max_single_hit: int = 0 -var critical_hits_count: int = 0 - -# 历史高光战报归档 -var highlight_cards: Array = [] -# 个人最高纪录:target_name -> { "best_dps": int, "fastest_time": float, "highest_hit": int } -var personal_bests: Dictionary = {} - -# 开始一场战斗记录 -func start_combat_session(vnum: int, tname: String, max_hp: int) -> Dictionary: - is_in_session = true - target_vnum = vnum - target_name = tname - target_max_hp = max_hp - - session_start_time = Time.get_unix_time_from_system() - elapsed_time = 0.0 - - total_damage_dealt = 0 - total_damage_taken = 0 - total_attacks = 0 - total_skills_used = 0 - max_single_hit = 0 - critical_hits_count = 0 - - combat_session_started.emit(target_name) - - return { - "ok": true, - "session_id": session_id, - "target_name": target_name, - "target_hp": target_max_hp - } - -# 记录一次攻击命中 -func record_hit(dmg: int, is_crit: bool, skill_name: String = "") -> void: - if not is_in_session: - return - - total_damage_dealt += dmg - total_attacks += 1 - - if is_crit: - critical_hits_count += 1 - - if not skill_name.is_empty(): - total_skills_used += 1 - - if dmg > max_single_hit: - max_single_hit = dmg - -# 记录受到的伤害 -func record_damage_taken(dmg: int) -> void: - if not is_in_session: - return - total_damage_taken += dmg - -# 帧心跳更新战斗时长 -func update(delta: float) -> void: - if is_in_session: - elapsed_time += delta - -# 结束战斗并生成结构化高光快照战报 -func end_combat_session(is_kill: bool, drops: Array = []) -> Dictionary: - if not is_in_session: - return {"ok": false, "reason": "NO_ACTIVE_SESSION"} - - is_in_session = false - var duration = max(0.5, elapsed_time) - var dps = int(float(total_damage_dealt) / duration) - - # 战斗评级评估 (S / A / B / C) - var rank = "C" - if is_kill: - if dps >= 4000: - rank = "S" - elif dps >= 2500: - rank = "A" - elif dps >= 1200: - rank = "B" - else: - rank = "C" - - var card: Dictionary = { - "session_id": session_id, - "target_vnum": target_vnum, - "target_name": target_name, - "is_kill": is_kill, - "rank": rank, - "duration": duration, - "total_damage": total_damage_dealt, - "dps": dps, - "max_single_hit": max_single_hit, - "crit_count": critical_hits_count, - "damage_taken": total_damage_taken, - "skills_used": total_skills_used, - "drops": drops.duplicate(true), - "datetime": Time.get_datetime_string_from_system() - } - - session_id += 1 - highlight_cards.push_front(card) - if highlight_cards.size() > MAX_STORED_CARDS: - highlight_cards.pop_back() - - # 刷新个人纪录判定 - _update_personal_bests(card) - - combat_highlight_generated.emit(card) - return card - -# 检查并更新个人生涯纪录 -func _update_personal_bests(card: Dictionary) -> void: - if not card["is_kill"]: - return - - var tname: String = card["target_name"] - if not personal_bests.has(tname): - personal_bests[tname] = { - "best_dps": card["dps"], - "fastest_time": card["duration"], - "highest_hit": card["max_single_hit"] - } - new_personal_record.emit(tname, "FIRST_KILL", card["dps"]) - return - - var pb = personal_bests[tname] - if card["dps"] > pb["best_dps"]: - pb["best_dps"] = card["dps"] - new_personal_record.emit(tname, "BEST_DPS", card["dps"]) - - if card["duration"] < pb["fastest_time"]: - pb["fastest_time"] = card["duration"] - new_personal_record.emit(tname, "FASTEST_TIME", int(card["duration"])) - - if card["max_single_hit"] > pb["highest_hit"]: - pb["highest_hit"] = card["max_single_hit"] - new_personal_record.emit(tname, "HIGHEST_HIT", card["max_single_hit"]) - -# 查询最近战报 -func get_recent_highlights() -> Array: - return highlight_cards - -# 查询特定目标的最佳战报纪录 -func get_personal_best(tname: String) -> Dictionary: - return personal_bests.get(tname, {}) diff --git a/project/combat_replay_snapshot_system.gd.uid b/project/combat_replay_snapshot_system.gd.uid deleted file mode 100644 index b6756692..00000000 --- a/project/combat_replay_snapshot_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cxu88rt8qo1o5 diff --git a/project/combat_status_effect_system.gd b/project/combat_status_effect_system.gd deleted file mode 100644 index 35ab98dc..00000000 --- a/project/combat_status_effect_system.gd +++ /dev/null @@ -1,249 +0,0 @@ -# combat_status_effect_system.gd —— Metin2 40250 官方战斗状态异常与五大负面减益结算系统 1:1 -# 100% 对照 40250 服务端 battle.cpp, char_resist.cpp, affect.h, char.h (POINT_POISON_PCT, IMMUNE_STUN, IMMUNE_SLOW) -class_name CombatStatusEffectSystem -extends RefCounted - -signal status_applied(target_id: int, effect_type: String, duration: float) -signal status_resisted(target_id: int, effect_type: String, reason: String) -signal status_ticked(target_id: int, effect_type: String, damage: int, remaining_ticks: int) -signal status_cleared(target_id: int, effect_type: String) - -# 40250 官方核心状态异常类型 -const EFFECT_POISON := "poison" # 中毒 (AFF_POISON) -const EFFECT_STUN := "stun" # 眩晕 (AFF_STUN) -const EFFECT_SLOW := "slow" # 减速 (AFF_SLOW) -const EFFECT_FIRE := "fire" # 烈焰灼烧 (AFF_FIRE) -const EFFECT_BLEEDING := "bleeding" # 流血虚弱 (AFF_BLEEDING) - -# 等级压制中毒调整表 (40250 char_resist.cpp: poison_level_adjust) -const POISON_LEVEL_ADJUST: Array[int] = [100, 90, 80, 70, 60, 50, 40, 30, 20] - -# 状态记录表: target_id -> { effect_type -> { "duration": float, "timer": float, "tick_interval": float, "ticks_left": int, "param": int } } -var _target_effects: Dictionary = {} - -# ========================================== -# 1. 状态异常施加与判定 (battle.cpp: NormalAttackAffect) -# ========================================== - -# 尝试施加攻击异常效果 (通用结算) -func try_apply_normal_attack_effects( - attacker: Dictionary, - victim: Dictionary, - forced_rolls: Dictionary = {} -) -> Dictionary: - var victim_id = int(victim.get("id", 0)) - var applied: Array[String] = [] - var resisted: Array[String] = [] - - var is_attacker_pc = bool(attacker.get("is_pc", true)) - var is_victim_pc = bool(victim.get("is_pc", true)) - var attacker_level = int(attacker.get("level", 1)) - var victim_level = int(victim.get("level", 1)) - - # 1. 中毒判定 (POINT_POISON_PCT) - var poison_pct = int(attacker.get("poison_pct", 0)) - if poison_pct > 0 and not has_effect(victim_id, EFFECT_POISON): - # 怪物只中毒一次检查 (char_resist.cpp: m_bHasPoisoned && !IsPC()) - var already_poisoned_once = bool(victim.get("has_been_poisoned", false)) - if not is_victim_pc and already_poisoned_once: - resisted.append("poison_already_poisoned_once") - else: - var roll = int(forced_rolls.get("poison_pct_roll", randi_range(1, 100))) - if roll <= poison_pct: - # 等级差距惩罚检查 - var can_poison := true - if attacker_level < victim_level: - var delta = min(victim_level - attacker_level, 8) - var level_roll = int(forced_rolls.get("poison_level_roll", randi_range(1, 100))) - if level_roll > POISON_LEVEL_ADJUST[delta]: - can_poison = false - resisted.append("poison_level_penalty") - - if can_poison: - apply_poison(victim, attacker) - applied.append(EFFECT_POISON) - - # 2. 眩晕判定 (POINT_STUN_PCT) - var stun_pct = int(attacker.get("stun_pct", 0)) - if stun_pct > 0 and not has_effect(victim_id, EFFECT_STUN): - # 免疫眩晕判定 (IMMUNE_STUN) - if bool(victim.get("immune_stun", false)): - status_resisted.emit(victim_id, EFFECT_STUN, "IMMUNE_STUN") - resisted.append("immune_stun") - else: - var roll = int(forced_rolls.get("stun_pct_roll", randi_range(1, 100))) - if roll <= stun_pct: - var stun_dur = 4.0 if (is_attacker_pc and not is_victim_pc) else 2.0 - apply_stun(victim_id, stun_dur) - applied.append(EFFECT_STUN) - - # 3. 减速判定 (POINT_SLOW_PCT) - var slow_pct = int(attacker.get("slow_pct", 0)) - if slow_pct > 0 and not has_effect(victim_id, EFFECT_SLOW): - # 免疫减速判定 (IMMUNE_SLOW) - if bool(victim.get("immune_slow", false)): - status_resisted.emit(victim_id, EFFECT_SLOW, "IMMUNE_SLOW") - resisted.append("immune_slow") - else: - var roll = int(forced_rolls.get("slow_pct_roll", randi_range(1, 100))) - if roll <= slow_pct: - apply_slow(victim_id, 20.0, 30) # 40250 官方减速 20 秒,移速 -30 - applied.append(EFFECT_SLOW) - - return { - "applied": applied, - "resisted": resisted - } - -# ========================================== -# 2. 状态具体应用 (char_resist.cpp) -# ========================================== - -# 施加中毒: 10 次跳伤害,每次间隔 3 秒,总持续 30 秒 (POISON_LENGTH + 1) -func apply_poison(victim: Dictionary, attacker: Dictionary = {}) -> void: - var victim_id = int(victim.get("id", 0)) - victim["has_been_poisoned"] = true - - _add_raw_effect(victim_id, EFFECT_POISON, { - "duration": 30.0, - "timer": 3.0, - "tick_interval": 3.0, - "ticks_left": 10, - "param": int(victim.get("poison_reduce", 0)), # 抗毒减免比例 (0~100) - "max_hp": int(victim.get("max_hp", 1000)), - "attacker_id": int(attacker.get("id", 0)) - }) - status_applied.emit(victim_id, EFFECT_POISON, 30.0) - -# 施加眩晕: 期间无法移动和释放技能 -func apply_stun(victim_id: int, duration: float = 2.0) -> void: - _add_raw_effect(victim_id, EFFECT_STUN, { - "duration": duration, - "timer": duration, - "tick_interval": 0.0, - "ticks_left": 0, - "param": 0 - }) - status_applied.emit(victim_id, EFFECT_STUN, duration) - -# 施加减速: 移动速度扣除 speed_reduction 点 -func apply_slow(victim_id: int, duration: float = 20.0, speed_reduction: int = 30) -> void: - _add_raw_effect(victim_id, EFFECT_SLOW, { - "duration": duration, - "timer": duration, - "tick_interval": 0.0, - "ticks_left": 0, - "param": speed_reduction - }) - status_applied.emit(victim_id, EFFECT_SLOW, duration) - -# 施加烈焰灼烧: 固定火伤每 3 秒跳一次 -func apply_fire(victim_id: int, ticks: int = 5, damage_per_tick: int = 50) -> void: - _add_raw_effect(victim_id, EFFECT_FIRE, { - "duration": float(ticks * 3), - "timer": 3.0, - "tick_interval": 3.0, - "ticks_left": ticks, - "param": damage_per_tick - }) - status_applied.emit(victim_id, EFFECT_FIRE, float(ticks * 3)) - -# 解除特定状态 (例如解毒药水 RemovePoison) -func clear_effect(victim_id: int, effect_type: String) -> bool: - if not _target_effects.has(victim_id): - return false - if _target_effects[victim_id].has(effect_type): - _target_effects[victim_id].erase(effect_type) - status_cleared.emit(victim_id, effect_type) - return true - return false - -# 检查目标是否具有特定异常状态 -func has_effect(victim_id: int, effect_type: String) -> bool: - if not _target_effects.has(victim_id): - return false - return _target_effects[victim_id].has(effect_type) - -# 检查目标是否处于无法行动状态 (眩晕) -func is_stunned(victim_id: int) -> bool: - return has_effect(victim_id, EFFECT_STUN) - -# 获取减速移速惩罚值 -func get_slow_reduction(victim_id: int) -> int: - if not has_effect(victim_id, EFFECT_SLOW): - return 0 - return int(_target_effects[victim_id][EFFECT_SLOW].get("param", 30)) - -# 检查自然回血是否被阻断 (40250 char.cpp:2453 中毒状态禁止自然回血) -func is_natural_hp_regen_blocked(victim_id: int) -> bool: - return has_effect(victim_id, EFFECT_POISON) - -# ========================================== -# 3. 状态心跳更新与跳伤害结算 (char_resist.cpp: poison_event, fire_event) -# ========================================== -func update(delta: float, targets_map: Dictionary = {}) -> void: - for vid in _target_effects.keys().duplicate(): - var effects: Dictionary = _target_effects[vid] - var to_remove: Array[String] = [] - - for eff_type in effects.keys(): - var data = effects[eff_type] - data["duration"] -= delta - - if data["tick_interval"] > 0.0: - # 周期跳伤害逻辑 (中毒/灼烧) - data["timer"] -= delta - while data["timer"] <= 0.0 and data["ticks_left"] > 0: - data["timer"] += data["tick_interval"] - data["ticks_left"] -= 1 - - var dmg := 0 - if eff_type == EFFECT_POISON: - # 40250 官方: GetMaxHP() * 5 / 100 * (100 - poison_reduce) / 100 - var max_hp = int(data.get("max_hp", 1000)) - var reduce = int(data.get("param", 0)) - var base_dmg = int(floor(max_hp * 0.05)) - dmg = max(1, int(floor(base_dmg * (100 - reduce) / 100.0))) - elif eff_type == EFFECT_FIRE: - dmg = int(data.get("param", 50)) - - # 扣减目标生命 (不可致死,留 1 点 HP) - if targets_map.has(vid): - var tgt = targets_map[vid] - var cur_hp = int(tgt.get("hp", 100)) - tgt["hp"] = max(1, cur_hp - dmg) - - status_ticked.emit(vid, eff_type, dmg, data["ticks_left"]) - - if data["ticks_left"] <= 0: - to_remove.append(eff_type) - break - else: - # 持续时间型 (眩晕/减速) - if data["duration"] <= 0.0: - to_remove.append(eff_type) - - for eff_type in to_remove: - effects.erase(eff_type) - status_cleared.emit(vid, eff_type) - - if effects.is_empty(): - _target_effects.erase(vid) - -func _add_raw_effect(victim_id: int, effect_type: String, data: Dictionary) -> void: - if not _target_effects.has(victim_id): - _target_effects[victim_id] = {} - _target_effects[victim_id][effect_type] = data - -# 获取目标全部当前状态 -func get_target_effects(victim_id: int) -> Dictionary: - return _target_effects.get(victim_id, {}).duplicate(true) - -# 序列化与反序列化 -func serialize() -> Dictionary: - return { - "target_effects": _target_effects.duplicate(true) - } - -func deserialize(data: Dictionary) -> void: - _target_effects = data.get("target_effects", {}).duplicate(true) diff --git a/project/combat_status_effect_system.gd.uid b/project/combat_status_effect_system.gd.uid deleted file mode 100644 index 63cb2dd1..00000000 --- a/project/combat_status_effect_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bo2ad4sxgu07r diff --git a/project/compass_treasure_hunt_system.gd b/project/compass_treasure_hunt_system.gd deleted file mode 100644 index 87973a49..00000000 --- a/project/compass_treasure_hunt_system.gd +++ /dev/null @@ -1,192 +0,0 @@ -# compass_treasure_hunt_system.gd —— Metin2 40250 官方魔石地脉探测罗盘与寻宝系统 1:1 -# 对照 40250 服务端 questlua_item.cpp, locale_en/itemdesc.txt:27989 -class_name CompassTreasureHuntSystem -extends RefCounted - -signal compass_scanned(target_name: String, distance: float, angle_deg: float, proximity: String, charges_left: int) -signal treasure_excavated(chest_vnum: int, chest_name: String) -signal treasure_opened(chest_name: String, rewards: Array) - -# 40250 官方核心道具 Vnum -const VNUM_COMPASS_OF_METIN := 27989 # 魔石探测罗盘 (Compass for Metin Stones, 6次充能) -const VNUM_GOLD_CHEST := 50005 # 黄金宝箱 -const VNUM_SILVER_CHEST := 50006 # 白银宝箱 -const VNUM_GOLD_KEY := 50007 # 黄金钥匙 -const VNUM_SILVER_KEY := 50008 # 白银钥匙 - -const MAX_CHARGES := 6 - -# 探测距离等级 -const PROXIMITY_FAR := "FAR" # > 100m ("微弱微光 - 遥远") -const PROXIMITY_MEDIUM := "MEDIUM" # 50m ~ 100m ("泛起银光 - 渐近") -const PROXIMITY_NEAR := "NEAR" # 20m ~ 50m ("强烈明光 - 附近") -const PROXIMITY_VERY_CLOSE := "VERY_CLOSE" # < 20m ("璀璨耀光 - 极近") - -# 激活魔石探测罗盘扫描最近的魔石或地脉宝藏 -func scan_nearest_metin(player_pos: Vector2, target_list: Array, inventory: Array, slot_index: int) -> Dictionary: - if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null: - return {"ok": false, "reason": "INVALID_SLOT"} - - var compass: Dictionary = inventory[slot_index] - if int(compass.get("vnum", 0)) != VNUM_COMPASS_OF_METIN: - return {"ok": false, "reason": "NOT_A_COMPASS", "msg": "该物品不是魔石探测罗盘!"} - - var charges: int = int(compass.get("charges", MAX_CHARGES)) - if charges <= 0: - inventory[slot_index] = null - return {"ok": false, "reason": "COMPASS_EXHAUSTED", "msg": "罗盘灵力已耗尽,化作飞灰!"} - - if target_list.is_empty(): - return {"ok": false, "reason": "NO_TARGETS_FOUND", "msg": "当前地图未探测到任何魔石波动!"} - - # 寻找最近的目标 - var nearest_target: Dictionary = {} - var min_dist := INF - for t in target_list: - var pos: Vector2 = t.get("pos", Vector2.ZERO) - var d := player_pos.distance_to(pos) - if d < min_dist: - min_dist = d - nearest_target = t - - # 扣除 1 次充能计数 (40250 官方 6 次限制) - charges -= 1 - compass["charges"] = charges - if charges <= 0: - inventory[slot_index] = null # 充能用完销毁 - - # 计算方位角与距离等级 - var target_pos: Vector2 = nearest_target.get("pos", Vector2.ZERO) - var dir_vec: Vector2 = (target_pos - player_pos).normalized() - var angle := rad_to_deg(dir_vec.angle()) - - var proximity := PROXIMITY_FAR - var desc := "微弱微光(遥远)" - if min_dist < 20.0: - proximity = PROXIMITY_VERY_CLOSE - desc = "璀璨耀光(极近)" - elif min_dist < 50.0: - proximity = PROXIMITY_NEAR - desc = "强烈明光(附近)" - elif min_dist < 100.0: - proximity = PROXIMITY_MEDIUM - desc = "泛起银光(渐近)" - - var target_name: String = str(nearest_target.get("name", "魔石")) - compass_scanned.emit(target_name, min_dist, angle, proximity, charges) - - return { - "ok": true, - "target_name": target_name, - "target_pos": nearest_target.get("pos", Vector2.ZERO), - "distance": min_dist, - "angle_deg": angle, - "proximity": proximity, - "proximity_desc": desc, - "charges_left": charges, - "msg": "罗盘指针震颤!锁定【%s】,距离 %.1f 米,光芒等级:%s,剩余使用次数:%d。" % [target_name, min_dist, desc, charges] - } - -# 在地脉极近坐标处挖掘秘宝 -func excavate_treasure(player_pos: Vector2, treasure_nodes: Array, inventory: Array) -> Dictionary: - var target_idx := -1 - for i in range(treasure_nodes.size()): - var node_pos: Vector2 = treasure_nodes[i].get("pos", Vector2.ZERO) - if player_pos.distance_to(node_pos) <= 5.0: # 5米范围内可挖掘 - target_idx = i - break - - if target_idx == -1: - return {"ok": false, "reason": "NO_TREASURE_NEARBY", "msg": "周围 5 米内未探测到地脉埋藏的宝箱!"} - - var node: Dictionary = treasure_nodes[target_idx] - var is_gold: bool = bool(node.get("is_gold", false)) - var chest_vnum = VNUM_GOLD_CHEST if is_gold else VNUM_SILVER_CHEST - var chest_name = "黄金宝箱" if is_gold else "白银宝箱" - - # 寻找背包空格放入宝箱 - var free_slot := -1 - for i in range(inventory.size()): - if inventory[i] == null: - free_slot = i - break - - if free_slot == -1: - return {"ok": false, "reason": "INVENTORY_FULL", "msg": "背包已满,无法容纳挖出的宝箱!"} - - inventory[free_slot] = { - "vnum": chest_vnum, - "name": chest_name, - "count": 1 - } - - treasure_nodes.remove_at(target_idx) - treasure_excavated.emit(chest_vnum, chest_name) - - return { - "ok": true, - "chest_vnum": chest_vnum, - "chest_name": chest_name, - "slot": free_slot, - "msg": "破土而出!在地脉深处成功发掘出【%s】!" % chest_name - } - -# 使用对应的黄金钥匙或白银钥匙开启宝箱 -func open_treasure_chest(chest_slot: int, key_slot: int, inventory: Array) -> Dictionary: - if chest_slot < 0 or chest_slot >= inventory.size() or inventory[chest_slot] == null: - return {"ok": false, "reason": "INVALID_CHEST_SLOT"} - if key_slot < 0 or key_slot >= inventory.size() or inventory[key_slot] == null: - return {"ok": false, "reason": "INVALID_KEY_SLOT"} - - var chest: Dictionary = inventory[chest_slot] - var key: Dictionary = inventory[key_slot] - - var chest_vnum := int(chest.get("vnum", 0)) - var key_vnum := int(key.get("vnum", 0)) - - var match_ok := false - if chest_vnum == VNUM_GOLD_CHEST and key_vnum == VNUM_GOLD_KEY: - match_ok = true - elif chest_vnum == VNUM_SILVER_CHEST and key_vnum == VNUM_SILVER_KEY: - match_ok = true - - if not match_ok: - return {"ok": false, "reason": "KEY_MISMATCH", "msg": "钥匙与宝箱类型不匹配!黄金宝箱需用黄金钥匙开启。"} - - # 消耗钥匙与宝箱 - inventory[chest_slot] = null - var key_cnt := int(key.get("count", 1)) - if key_cnt > 1: - key["count"] = key_cnt - 1 - else: - inventory[key_slot] = null - - # 生成高额宝物奖励 - var rewards: Array = [] - if chest_vnum == VNUM_GOLD_CHEST: - rewards = [ - {"vnum": 25040, "name": "祝福卷轴", "count": 3}, - {"vnum": 51501, "name": "龙石原石", "count": 2}, - {"vnum": 28430, "name": "+4 灵石", "count": 1} - ] - else: - rewards = [ - {"vnum": 25040, "name": "祝福卷轴", "count": 1}, - {"vnum": 50300, "name": "技能书", "count": 1} - ] - - # 放入背包 - for r in rewards: - for i in range(inventory.size()): - if inventory[i] == null: - inventory[i] = r.duplicate(true) - break - - treasure_opened.emit(chest.get("name", "宝箱"), rewards) - - return { - "ok": true, - "chest_name": chest.get("name", "宝箱"), - "rewards": rewards, - "msg": "清脆的开锁声响起!开启宝箱,获得珍稀秘宝!" - } diff --git a/project/compass_treasure_hunt_system.gd.uid b/project/compass_treasure_hunt_system.gd.uid deleted file mode 100644 index f6b83af2..00000000 --- a/project/compass_treasure_hunt_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bau7tkdeg2823 diff --git a/project/costume_attr_transfer_system.gd b/project/costume_attr_transfer_system.gd deleted file mode 100644 index fa672a09..00000000 --- a/project/costume_attr_transfer_system.gd +++ /dev/null @@ -1,244 +0,0 @@ -# costume_attr_transfer_system.gd —— Metin2 40250 官方时装属性转移与增益附魔系统 1:1 -# 100% 对照 40250 服务端 char_item.cpp (ITEM_COSTUME, 70063, 70064), item_proto.txt -class_name CostumeAttrTransferSystem -extends RefCounted - -signal costume_transformed(costume_vnum: int, new_attrs: Array) -signal costume_enchanted(costume_vnum: int, new_attrs: Array) -signal costume_attributes_transferred(src_vnum: int, dst_vnum: int, attrs: Array) -signal costume_error(reason: String) - -# 40250 官方时装重铸与附魔道具 Vnum -const VNUM_COSTUME_TRANSFORM := 70063 # 时装重铸卷轴 (Kostümü dönüştür - 随机 1~3 条新属性) -const VNUM_COSTUME_ENCHANT := 70064 # 时装附魔石 (Kostümü efsunla - 保留条数重洗属性) -const VNUM_ATTR_TRANSFER := 72325 # 时装属性转移符 - -# 时装最大词条数 -const MAX_COSTUME_ATTRS := 3 - -# 40250 官方时装属性词条候选池 -const COSTUME_ATTR_POOL: Array[Dictionary] = [ - {"type": "max_hp", "name": "生命上限", "min": 500, "max": 2000}, - {"type": "max_sp", "name": "法力上限", "min": 200, "max": 1000}, - {"type": "crit_pct", "name": "暴击几率 (%)", "min": 5, "max": 10}, - {"type": "penetrate_pct", "name": "穿刺伤害几率 (%)", "min": 5, "max": 10}, - {"type": "att_speed", "name": "攻击速度", "min": 5, "max": 10}, - {"type": "mov_speed", "name": "移动速度", "min": 5, "max": 10}, - {"type": "cast_speed", "name": "施法速度", "min": 5, "max": 10}, - {"type": "hp_regen", "name": "生命自然恢复 (%)", "min": 10, "max": 30} -] - -# 校验是否为时装物品 -func is_costume_item(item: Dictionary) -> bool: - if item == null: - return false - var subtype = str(item.get("costume_subtype", "")) - var is_costume = bool(item.get("is_costume", false)) - var vnum = int(item.get("vnum", 0)) - # 40250 官方区间:时装铠甲 (41001~44000), 时装发型 (45001~46000), 时装武器 (40101~) - if is_costume or subtype != "": - return true - if (vnum >= 41001 and vnum <= 46000) or (vnum >= 40101 and vnum <= 40999): - return true - return false - -# ========================================== -# 1. 时装重铸 (70063 Kostümü dönüştür) -# ========================================== -# 移除所有既有属性,随机赋予 1~3 条全新的时装属性 -func transform_costume( - inventory: Array, - scroll_slot: int, - costume_slot: int, - forced_count: int = -1, - forced_attrs: Array = [] -) -> Dictionary: - if not _validate_slots(inventory, scroll_slot, costume_slot): - costume_error.emit("INVALID_SLOT") - return {"ok": false, "reason": "INVALID_SLOT", "msg": "格位无效或道具为空!"} - - var scroll = inventory[scroll_slot] - var costume = inventory[costume_slot] - - if int(scroll.get("vnum", 0)) != VNUM_COSTUME_TRANSFORM: - costume_error.emit("NOT_TRANSFORM_SCROLL") - return {"ok": false, "reason": "NOT_TRANSFORM_SCROLL", "msg": "该道具不是时装重铸卷轴!"} - - if not is_costume_item(costume): - costume_error.emit("NOT_COSTUME") - return {"ok": false, "reason": "NOT_COSTUME", "msg": "只有时装物品可以使用时装重铸卷轴!"} - - if bool(costume.get("is_equipped", false)): - costume_error.emit("EQUIPPED_ITEM") - return {"ok": false, "reason": "EQUIPPED_ITEM", "msg": "无法对穿戴中的时装进行重铸,请先卸下!"} - - # 消耗 1 个重铸卷轴 - _consume_item_one(inventory, scroll_slot) - - # 决定新属性条数 (1~3 条) - var count = forced_count if forced_count in [1, 2, 3] else randi_range(1, 3) - var new_attrs: Array = [] - - if not forced_attrs.is_empty(): - new_attrs = forced_attrs.duplicate(true) - else: - new_attrs = _roll_random_attributes(count) - - costume["attrs"] = new_attrs - costume_transformed.emit(int(costume.get("vnum", 0)), new_attrs) - - return { - "ok": true, - "attrs_count": new_attrs.size(), - "attrs": new_attrs, - "msg": "时装周身流光溢彩!重铸成功,获得了 %d 条全新的时装属性。" % new_attrs.size() - } - -# ========================================== -# 2. 时装附魔 (70064 Kostümü efsunla) -# ========================================== -# 保留当前属性条数,重新随机洗练词条类型与数值 -func enchant_costume( - inventory: Array, - scroll_slot: int, - costume_slot: int, - forced_attrs: Array = [] -) -> Dictionary: - if not _validate_slots(inventory, scroll_slot, costume_slot): - costume_error.emit("INVALID_SLOT") - return {"ok": false, "reason": "INVALID_SLOT", "msg": "格位无效或道具为空!"} - - var scroll = inventory[scroll_slot] - var costume = inventory[costume_slot] - - if int(scroll.get("vnum", 0)) != VNUM_COSTUME_ENCHANT: - costume_error.emit("NOT_ENCHANT_SCROLL") - return {"ok": false, "reason": "NOT_ENCHANT_SCROLL", "msg": "该道具不是时装附魔石!"} - - if not is_costume_item(costume): - costume_error.emit("NOT_COSTUME") - return {"ok": false, "reason": "NOT_COSTUME", "msg": "只有时装物品可以使用时装附魔石!"} - - if bool(costume.get("is_equipped", false)): - costume_error.emit("EQUIPPED_ITEM") - return {"ok": false, "reason": "EQUIPPED_ITEM", "msg": "无法对穿戴中的时装进行附魔,请先卸下!"} - - var existing_attrs: Array = costume.get("attrs", []) - if existing_attrs.is_empty(): - costume_error.emit("NO_ATTRS_TO_ENCHANT") - return {"ok": false, "reason": "NO_ATTRS_TO_ENCHANT", "msg": "此时装没有任何附加属性!请先使用时装重铸卷轴赋予属性。"} - - # 消耗 1 个附魔石 - _consume_item_one(inventory, scroll_slot) - - var count = existing_attrs.size() - var new_attrs: Array = [] - if not forced_attrs.is_empty(): - new_attrs = forced_attrs.duplicate(true) - else: - new_attrs = _roll_random_attributes(count) - - costume["attrs"] = new_attrs - costume_enchanted.emit(int(costume.get("vnum", 0)), new_attrs) - - return { - "ok": true, - "attrs_count": new_attrs.size(), - "attrs": new_attrs, - "msg": "附魔灵石融入织物!保留原有 %d 条属性槽位,洗练出了全新的强力词条。" % count - } - -# ========================================== -# 3. 时装属性转移 (Costume Attribute Transfer) -# ========================================== -func transfer_costume_attributes( - inventory: Array, - scroll_slot: int, - src_slot: int, - dst_slot: int -) -> Dictionary: - if scroll_slot < 0 or scroll_slot >= inventory.size() or inventory[scroll_slot] == null: - costume_error.emit("INVALID_SLOT") - return {"ok": false, "reason": "INVALID_SLOT", "msg": "转移卷轴格位无效!"} - - if src_slot < 0 or src_slot >= inventory.size() or inventory[src_slot] == null: - costume_error.emit("INVALID_SLOT") - return {"ok": false, "reason": "INVALID_SLOT", "msg": "源时装格位无效!"} - - if dst_slot < 0 or dst_slot >= inventory.size() or inventory[dst_slot] == null: - costume_error.emit("INVALID_SLOT") - return {"ok": false, "reason": "INVALID_SLOT", "msg": "目标时装格位无效!"} - - var scroll = inventory[scroll_slot] - var src_item = inventory[src_slot] - var dst_item = inventory[dst_slot] - - if int(scroll.get("vnum", 0)) != VNUM_ATTR_TRANSFER: - costume_error.emit("NOT_TRANSFER_SCROLL") - return {"ok": false, "reason": "NOT_TRANSFER_SCROLL", "msg": "该物品不是时装属性转移符!"} - - if not is_costume_item(src_item) or not is_costume_item(dst_item): - costume_error.emit("NOT_COSTUME") - return {"ok": false, "reason": "NOT_COSTUME", "msg": "源道具与目标道具都必须是时装!"} - - var src_type = str(src_item.get("costume_subtype", "body")) - var dst_type = str(dst_item.get("costume_subtype", "body")) - if src_type != dst_type: - costume_error.emit("TYPE_MISMATCH") - return {"ok": false, "reason": "TYPE_MISMATCH", "msg": "时装类型不匹配!时装铠甲只能转移至时装铠甲,发型只能转移至发型。"} - - var src_attrs: Array = src_item.get("attrs", []) - if src_attrs.is_empty(): - costume_error.emit("NO_ATTRS_TO_TRANSFER") - return {"ok": false, "reason": "NO_ATTRS_TO_TRANSFER", "msg": "源时装没有任何可转移的属性!"} - - # 消耗 1 张转移符 - _consume_item_one(inventory, scroll_slot) - - # 执行无损属性迁移 - var transferred = src_attrs.duplicate(true) - dst_item["attrs"] = transferred - src_item["attrs"] = [] # 源时装词条清空 - - costume_attributes_transferred.emit(int(src_item.get("vnum", 0)), int(dst_item.get("vnum", 0)), transferred) - return { - "ok": true, - "transferred_attrs": transferred, - "msg": "乾坤倒转!成功将 %d 条极品属性从旧时装完整迁移至新时装。" % transferred.size() - } - -# 内部随机属性抽取工具 -func _roll_random_attributes(count: int) -> Array: - var result: Array = [] - var pool_indices: Array[int] = [] - for i in range(COSTUME_ATTR_POOL.size()): - pool_indices.append(i) - pool_indices.shuffle() - - var actual_count = min(count, pool_indices.size()) - for i in range(actual_count): - var def = COSTUME_ATTR_POOL[pool_indices[i]] - var val = randi_range(int(def["min"]), int(def["max"])) - result.append({ - "type": def["type"], - "name": def["name"], - "value": val - }) - return result - -func _validate_slots(inventory: Array, s1: int, s2: int) -> bool: - if s1 < 0 or s1 >= inventory.size() or inventory[s1] == null: - return false - if s2 < 0 or s2 >= inventory.size() or inventory[s2] == null: - return false - return true - -func _consume_item_one(inventory: Array, slot: int) -> void: - if slot < 0 or slot >= inventory.size() or inventory[slot] == null: - return - var item = inventory[slot] - var count = int(item.get("count", 1)) - if count > 1: - item["count"] = count - 1 - else: - inventory[slot] = null diff --git a/project/costume_attr_transfer_system.gd.uid b/project/costume_attr_transfer_system.gd.uid deleted file mode 100644 index 050cbb67..00000000 --- a/project/costume_attr_transfer_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cd5g767aqt3ui diff --git a/project/costume_system.gd b/project/costume_system.gd deleted file mode 100644 index 59c98de7..00000000 --- a/project/costume_system.gd +++ /dev/null @@ -1,126 +0,0 @@ -# costume_system.gd —— Metin2 40250 独立时装发型与武器衣柜系统 1:1 -# 对照 40250 服务端 costume.cpp, char_item.cpp, uiinventory.py, item_data.h -class_name CostumeSystem -extends RefCounted - -signal costume_equipped(slot_type: int, item: Dictionary) -signal costume_unequipped(slot_type: int) -signal costume_expired(slot_type: int, item_name: String) - -enum CostumeSlot { - COSTUME_BODY = 0, # 时装衣服 - COSTUME_HAIR = 1, # 时装发型 - COSTUME_WEAPON = 2 # 时装武器外观 -} - -const DEFAULT_COSTUME_DURATION := 604800.0 # 默认 7 天 (7 * 86400) - -var equipped_costumes: Dictionary = { - CostumeSlot.COSTUME_BODY: null, - CostumeSlot.COSTUME_HAIR: null, - CostumeSlot.COSTUME_WEAPON: null -} - -# 穿戴时装 -func equip_costume(slot_type: int, costume_item: Dictionary) -> Dictionary: - if not equipped_costumes.has(slot_type): - return {"ok": false, "reason": "INVALID_SLOT", "msg": "无效的时装栏位!"} - - if costume_item.is_empty(): - return {"ok": false, "reason": "EMPTY_ITEM", "msg": "时装物品数据为空!"} - - # 校验时装部位匹配 - var c_type: String = costume_item.get("costume_type", "") - match slot_type: - CostumeSlot.COSTUME_BODY: - if c_type != "body": - return {"ok": false, "reason": "SLOT_TYPE_MISMATCH", "msg": "此物品不是时装衣服!"} - CostumeSlot.COSTUME_HAIR: - if c_type != "hair": - return {"ok": false, "reason": "SLOT_TYPE_MISMATCH", "msg": "此物品不是时装发型!"} - CostumeSlot.COSTUME_WEAPON: - if c_type != "weapon": - return {"ok": false, "reason": "SLOT_TYPE_MISMATCH", "msg": "此物品不是时装武器外观!"} - - var old_item = equipped_costumes[slot_type] - var item_to_equip = costume_item.duplicate(true) - - if not item_to_equip.has("remaining_time"): - item_to_equip["remaining_time"] = DEFAULT_COSTUME_DURATION - - equipped_costumes[slot_type] = item_to_equip - - costume_equipped.emit(slot_type, item_to_equip) - - return { - "ok": true, - "slot_type": slot_type, - "item": item_to_equip, - "old_item": old_item, - "msg": "时装【%s】已成功穿戴!" % item_to_equip.get("name", "时装") - } - -# 卸下时装 -func unequip_costume(slot_type: int) -> Dictionary: - if not equipped_costumes.has(slot_type): - return {"ok": false, "reason": "INVALID_SLOT", "msg": "无效的时装栏位!"} - - var item = equipped_costumes[slot_type] - if item == null: - return {"ok": false, "reason": "SLOT_EMPTY", "msg": "该时装栏位未穿戴时装!"} - - equipped_costumes[slot_type] = null - costume_unequipped.emit(slot_type) - - return { - "ok": true, - "slot_type": slot_type, - "item": item, - "msg": "已卸下时装【%s】。" % item.get("name", "时装") - } - -# 汇总所有已穿戴时装的战力属性 -func get_costume_bonuses() -> Dictionary: - var total_bonuses: Dictionary = {} - for slot in equipped_costumes.keys(): - var it = equipped_costumes[slot] - if it == null: - continue - var bonuses: Dictionary = it.get("bonuses", {}) - for b_key in bonuses.keys(): - var val: int = int(bonuses[b_key]) - total_bonuses[b_key] = total_bonuses.get(b_key, 0) + val - return total_bonuses - -# 获取 3D 外观覆盖模型 ID -func get_appearance_overrides() -> Dictionary: - var overrides: Dictionary = { - "body_model": 0, - "hair_model": 0, - "weapon_model": 0 - } - if equipped_costumes[CostumeSlot.COSTUME_BODY] != null: - overrides["body_model"] = int(equipped_costumes[CostumeSlot.COSTUME_BODY].get("model_id", 0)) - if equipped_costumes[CostumeSlot.COSTUME_HAIR] != null: - overrides["hair_model"] = int(equipped_costumes[CostumeSlot.COSTUME_HAIR].get("model_id", 0)) - if equipped_costumes[CostumeSlot.COSTUME_WEAPON] != null: - overrides["weapon_model"] = int(equipped_costumes[CostumeSlot.COSTUME_WEAPON].get("model_id", 0)) - return overrides - -# 逐帧时效衰减更新 -func update(dt: float) -> Array: - var expired_list: Array = [] - for slot in equipped_costumes.keys(): - var it = equipped_costumes[slot] - if it == null: - continue - - it["remaining_time"] = maxf(0.0, float(it.get("remaining_time", 0.0)) - dt) - if it["remaining_time"] <= 0.0: - var iname: String = it.get("name", "时装") - expired_list.append({"slot": slot, "item": it}) - equipped_costumes[slot] = null - costume_expired.emit(slot, iname) - costume_unequipped.emit(slot) - - return expired_list diff --git a/project/costume_system.gd.uid b/project/costume_system.gd.uid deleted file mode 100644 index 738968fa..00000000 --- a/project/costume_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bas2h6u0g5rsg diff --git a/project/cube_crafting_system.gd b/project/cube_crafting_system.gd deleted file mode 100644 index f50086cb..00000000 --- a/project/cube_crafting_system.gd +++ /dev/null @@ -1,293 +0,0 @@ -# cube_crafting_system.gd —— Metin2 40250 官方魔方炼金合成台系统 1:1 -# 100% 对照 40250 服务端 cube.cpp, cube.h, cube.txt, quest/cube.quest -class_name CubeCraftingSystem -extends RefCounted - -signal cube_opened(npc_vnum: int, npc_name: String) -signal cube_closed() -signal craft_succeeded(reward_vnum: int, count: int, reward_name: String) -signal craft_failed(recipe_name: String) -signal cube_error(reason: String) - -# 40250 官方四大魔方炼金 NPC -const NPC_BAEK_GO := 20018 # 药剂师 白高 (草药汁提炼) -const NPC_YU_HWAN := 20017 # 游侠助手 刘焕 (中级药水配制) -const NPC_HUAHN_SO := 20022 # 隐者 幻素 (高级露水炼制) -const NPC_JAE_SEON_KIM:= 20383 # 顶级工匠 金在善 (神武/神甲熔铸) - -const NPC_NAMES: Dictionary = { - 20018: "药剂师 白高 (Baek-Go)", - 20017: "游侠助手 刘焕 (Yu-Hwan)", - 20022: "隐者 幻素 (Huahn-So)", - 20383: "神级工匠 金在善 (Jae-Seon Kim)" -} - -# 40250 经典 cube.txt 配方定义表 -const RECIPES: Array[Dictionary] = [ - # --- 药剂师 白高 (20018) & 刘焕 (20017) 基础草药汁 --- - { - "id": "herb_peach_flower", - "name": "桃花汁提炼", - "npcs": [20018, 20017], - "materials": [{"vnum": 50721, "count": 1}], # 桃花 - "reward": {"vnum": 50801, "count": 1, "name": "桃花汁 (Peach Flower Juice)"}, - "gold": 0, - "percent": 100 - }, - { - "id": "herb_campanula", - "name": "风铃草汁提炼", - "npcs": [20018, 20017], - "materials": [{"vnum": 50722, "count": 1}], # 风铃草 - "reward": {"vnum": 50802, "count": 1, "name": "风铃草汁 (Campanula Juice)"}, - "gold": 0, - "percent": 100 - }, - { - "id": "herb_lilac", - "name": "紫丁香汁批量提炼", - "npcs": [20018, 20017], - "materials": [{"vnum": 50723, "count": 1}], # 紫丁香 - "reward": {"vnum": 50803, "count": 100, "name": "紫丁香汁 x100"}, - "gold": 0, - "percent": 100 - }, - { - "id": "herb_persimmon", - "name": "柿子花汁批量提炼", - "npcs": [20018, 20017], - "materials": [{"vnum": 50724, "count": 1}], # 柿子花 - "reward": {"vnum": 50804, "count": 100, "name": "柿子花汁 x100"}, - "gold": 0, - "percent": 100 - }, - - # --- 隐者 幻素 (20022) 露水药水炼制 (Dew Crafting) --- - { - "id": "dew_peach_flower", - "name": "桃花露水炼制", - "npcs": [20022], - "materials": [ - {"vnum": 50801, "count": 10}, # 桃花汁 x10 - {"vnum": 70031, "count": 10} # 空瓶 (Empty Bottle) x10 - ], - "reward": {"vnum": 50821, "count": 1, "name": "纯净桃花露 (Peach Flower Dew)"}, - "gold": 10000, - "percent": 100 - }, - { - "id": "dew_campanula", - "name": "风铃草露水炼制", - "npcs": [20022], - "materials": [ - {"vnum": 50802, "count": 10}, # 风铃草汁 x10 - {"vnum": 70031, "count": 10} # 空瓶 x10 - ], - "reward": {"vnum": 50822, "count": 1, "name": "纯净风铃露 (Campanula Dew)"}, - "gold": 10000, - "percent": 100 - }, - - # --- 工匠 金在善 (20383) 顶级神兵海神剑 (Triton Sword) 熔铸 --- - { - "id": "craft_triton_low", - "name": "海神剑试炼熔铸 (低阶+5)", - "npcs": [20383], - "materials": [ - {"vnum": 145, "count": 1}, # 战神之剑+5 - {"vnum": 30500, "count": 10},# 能量晶石 x10 - {"vnum": 30505, "count": 10} # 熔铸秘石 x10 - ], - "reward": {"vnum": 460, "count": 1, "name": "海神之剑+0 (Triton Sword)"}, - "gold": 100000, - "percent": 5 # 40250 原版 5% 概率 - }, - { - "id": "craft_triton_master", - "name": "海神剑巅峰熔铸 (满阶+9)", - "npcs": [20383], - "materials": [ - {"vnum": 149, "count": 1}, # 战神之剑+9 - {"vnum": 30500, "count": 10},# 能量晶石 x10 - {"vnum": 30505, "count": 10} # 熔铸秘石 x10 - ], - "reward": {"vnum": 460, "count": 1, "name": "海神之剑+0 (Triton Sword)"}, - "gold": 100000, - "percent": 100 # 40250 原版 +9 保底 100% - } -] - -# 当前魔方状态 -var current_active_npc: int = 0 -var is_cube_open: bool = false -var crafting_history: Array[Dictionary] = [] - -# 打开魔方窗口 -func open_cube(npc_vnum: int) -> bool: - if not NPC_NAMES.has(npc_vnum): - cube_error.emit("INVALID_NPC") - return false - current_active_npc = npc_vnum - is_cube_open = true - cube_opened.emit(npc_vnum, NPC_NAMES[npc_vnum]) - return true - -# 关闭魔方窗口 -func close_cube() -> void: - current_active_npc = 0 - is_cube_open = false - cube_closed.emit() - -# 根据放入槽位的材料与当前 NPC 匹配配方 (对照 cube.cpp: FN_find_cube) -func find_matching_recipe(cube_slots: Array, npc_vnum: int) -> Dictionary: - # 统计放入槽位的物品 Vnum 与数量 - var inputs: Dictionary = {} - for it in cube_slots: - if it != null: - var v = int(it.get("vnum", 0)) - var c = int(it.get("count", 1)) - inputs[v] = inputs.get(v, 0) + c - - if inputs.is_empty(): - return {} - - for rec in RECIPES: - var npcs: Array = rec["npcs"] - if not npcs.has(npc_vnum): - continue - - var req_materials: Array = rec["materials"] - if req_materials.size() != inputs.size(): - continue - - var matched := true - for mat in req_materials: - var mv = int(mat["vnum"]) - var mc = int(mat["count"]) - if inputs.get(mv, -1) != mc: - matched = false - break - - if matched: - return rec - - return {} - -# 执行魔方制造 (对照 cube.cpp: Cube_make) -func craft_item( - cube_slots: Array, - inventory: Array, - player_gold: int, - forced_roll: int = -1 -) -> Dictionary: - if not is_cube_open: - cube_error.emit("CUBE_NOT_OPEN") - return {"ok": false, "reason": "CUBE_NOT_OPEN", "msg": "制造窗口尚未打开!"} - - var recipe = find_matching_recipe(cube_slots, current_active_npc) - if recipe.is_empty(): - cube_error.emit("NO_MATCHING_RECIPE") - return {"ok": false, "reason": "NO_MATCHING_RECIPE", "msg": "材料不足或配方不存在!"} - - var fee = int(recipe.get("gold", 0)) - if player_gold < fee: - cube_error.emit("NOT_ENOUGH_GOLD") - return { - "ok": false, - "reason": "NOT_ENOUGH_GOLD", - "msg": "金币不足!本次合成需要 %d 金币,你只有 %d 金币。" % [fee, player_gold] - } - - # 扣除材料 (清空魔方放置槽) - for i in range(cube_slots.size()): - cube_slots[i] = null - - # 扣除手续费金币 - var remaining_gold = player_gold - fee - - # 判定成功概率 - var pct = int(recipe.get("percent", 100)) - var roll = forced_roll if forced_roll > 0 else randi_range(1, 100) - - if roll <= pct: - # 合成成功 (cube success) - var rew = recipe["reward"] - var reward_vnum = int(rew["vnum"]) - var reward_count = int(rew.get("count", 1)) - var reward_name = str(rew.get("name", "制造道具")) - - _give_or_stack_item(inventory, reward_vnum, reward_count) - - var record = { - "recipe_id": recipe["id"], - "success": true, - "reward_vnum": reward_vnum, - "reward_count": reward_count, - "fee": fee - } - crafting_history.append(record) - craft_succeeded.emit(reward_vnum, reward_count, reward_name) - - return { - "ok": true, - "success": true, - "reward_vnum": reward_vnum, - "reward_count": reward_count, - "remaining_gold": remaining_gold, - "msg": "叮!伴随着绚烂的灵光,你成功制造出了【%s】!" % reward_name - } - else: - # 合成失败 (cube fail) - var record = { - "recipe_id": recipe["id"], - "success": false, - "fee": fee - } - crafting_history.append(record) - craft_failed.emit(recipe["name"]) - - return { - "ok": true, - "success": false, - "remaining_gold": remaining_gold, - "msg": "制造失败了!魔方中传出一阵焦黑的青烟,材料化为了灰烬。" - } - -# 获取当前 NPC 允许合成的所有配方清单 -func get_available_recipes(npc_vnum: int) -> Array[Dictionary]: - var list: Array[Dictionary] = [] - for rec in RECIPES: - if rec["npcs"].has(npc_vnum): - list.append(rec) - return list - -# 历史记录 -func get_history() -> Array[Dictionary]: - return crafting_history - -func clear_history() -> void: - crafting_history.clear() - -# 内部发放道具堆叠 -func _give_or_stack_item(inventory: Array, vnum: int, count: int) -> void: - for it in inventory: - if it != null and int(it.get("vnum", 0)) == vnum: - it["count"] = int(it.get("count", 1)) + count - return - for i in range(inventory.size()): - if inventory[i] == null: - inventory[i] = {"vnum": vnum, "count": count} - return - inventory.append({"vnum": vnum, "count": count}) - -# 序列化与反序列化 -func serialize() -> Dictionary: - return { - "history": crafting_history.duplicate(true) - } - -func deserialize(data: Dictionary) -> void: - crafting_history.clear() - var raw = data.get("history", []) - for it in raw: - crafting_history.append(it.duplicate(true)) diff --git a/project/cube_crafting_system.gd.uid b/project/cube_crafting_system.gd.uid deleted file mode 100644 index 0d82ffd6..00000000 --- a/project/cube_crafting_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dxkngqnfky42a diff --git a/project/cursed_monolith_spire_system.gd b/project/cursed_monolith_spire_system.gd deleted file mode 100644 index 4b0bed9a..00000000 --- a/project/cursed_monolith_spire_system.gd +++ /dev/null @@ -1,196 +0,0 @@ -# cursed_monolith_spire_system.gd —— Metin2 40250 野外魔石破除之【封印魔塔】限时防守挑战 1:1 -# 对照 40250 服务端 regen.cpp, dungeon.cpp, char_battle.cpp, item_manager.cpp -class_name CursedMonolithSpireSystem -extends RefCounted - -signal spire_emerged(pos: Vector3, time_limit: float) -signal wave_spawned(wave_index: int, wave_name: String, mob_count: int) -signal trial_completed(success: bool, time_used: float) -signal reward_chest_spawned(chest_vnum: int, pos: Vector3) - -const TRIAL_TIME_LIMIT: float = 180.0 # 3 分钟倒计时 -const CHEST_VNUM: int = 50130 # 上古魔塔宝箱 (Ancient Spire Chest) - -const WAVE_CONFIG: Dictionary = { - 1: { - "name": "第 1 波:狂暴魔兵突袭", - "mob_vnum": 701, # 恶魔士兵 - "count": 6 - }, - 2: { - "name": "第 2 波:双生魔将合围", - "mob_vnum": 1092, # 恶魔大将 - "count": 2 - }, - 3: { - "name": "第 3 波:上古守墓魔王", - "mob_vnum": 1093, # 恶魔死神幻影 - "count": 1 - } -} - -var is_active: bool = false -var current_wave: int = 0 -var time_remaining: float = 0.0 -var spire_pos: Vector3 = Vector3.ZERO -var difficulty: int = 50 - -var active_mobs: Dictionary = {} # mob_id -> { "vnum", "alive" } -var next_mob_id: int = 1 -var is_victory: bool = false -var chest_available: bool = false - -# 启动封印魔塔挑战 -func start_trial(pos: Vector3, diff_level: int = 50) -> Dictionary: - if is_active: - return {"ok": false, "reason": "TRIAL_ALREADY_ACTIVE", "msg": "封印魔塔挑战正在进行中!"} - - is_active = true - is_victory = false - chest_available = false - spire_pos = pos - difficulty = diff_level - time_remaining = TRIAL_TIME_LIMIT - current_wave = 0 - active_mobs.clear() - next_mob_id = 1 - - spire_emerged.emit(pos, TRIAL_TIME_LIMIT) - - # 立即召唤第 1 波 - _spawn_wave(1) - - return { - "ok": true, - "pos": pos, - "time_limit": TRIAL_TIME_LIMIT, - "msg": "大地剧烈震颤!【诅咒的封印黑石魔塔】破土升起!请在 180 秒内清除所有魔潮!" - } - -# 产生指定阶段怪潮 -func _spawn_wave(wave_idx: int) -> void: - current_wave = wave_idx - var cfg = WAVE_CONFIG[wave_idx] - var count: int = cfg["count"] - var vnum: int = cfg["mob_vnum"] - - active_mobs.clear() - for i in range(count): - var mid := next_mob_id - next_mob_id += 1 - active_mobs[mid] = { - "id": mid, - "vnum": vnum, - "alive": true - } - - wave_spawned.emit(wave_idx, cfg["name"], count) - -# 击杀魔潮怪物 -func kill_mob(mob_id: int) -> Dictionary: - if not is_active: - return {"ok": false, "reason": "TRIAL_NOT_ACTIVE"} - - if not active_mobs.has(mob_id) or not active_mobs[mob_id]["alive"]: - return {"ok": false, "reason": "MOB_NOT_FOUND"} - - active_mobs[mob_id]["alive"] = false - active_mobs.erase(mob_id) - - # 检查当前波次是否全清 - if active_mobs.is_empty(): - if current_wave < 3: - # 进入下一波 - var next_w = current_wave + 1 - _spawn_wave(next_w) - return { - "ok": true, - "wave_cleared": current_wave - 1, - "next_wave": next_w, - "msg": "当前波次肃清!更加凶险的魔物正在凝聚!" - } - else: - # 第 3 波首领击破 -> 大获全胜! - return _finish_trial(true) - - return { - "ok": true, - "remaining_in_wave": active_mobs.size() - } - -# 帧心跳更新限时倒计时 -func update(delta: float) -> Dictionary: - if not is_active: - return {"status": "inactive"} - - time_remaining -= delta - if time_remaining <= 0.0: - time_remaining = 0.0 - return _finish_trial(false) - - return { - "status": "running", - "wave": current_wave, - "remaining_mobs": active_mobs.size(), - "time_remaining": time_remaining - } - -# 结算挑战胜负 -func _finish_trial(success: bool) -> Dictionary: - is_active = false - is_victory = success - var time_used = TRIAL_TIME_LIMIT - time_remaining - - if success: - chest_available = true - reward_chest_spawned.emit(CHEST_VNUM, spire_pos) - trial_completed.emit(true, time_used) - return { - "ok": true, - "victory": true, - "time_used": time_used, - "msg": "【封印魔塔】封印彻底瓦解!降下【上古魔塔宝箱】!耗时: %.1f 秒" % time_used - } - else: - active_mobs.clear() - trial_completed.emit(false, TRIAL_TIME_LIMIT) - return { - "ok": true, - "victory": false, - "time_used": TRIAL_TIME_LIMIT, - "msg": "时间耗尽!封印黑石魔塔重新沉入深渊地底,挑战失败!" - } - -# 开启上古宝箱获取掉落 -func open_reward_chest() -> Array: - if not chest_available: - return [] - - chest_available = false - var drops: Array = [ - {"vnum": 71003, "name": "祝福卷轴", "count": 2}, - {"vnum": 50513, "name": "灵魂之石", "count": 1}, - {"vnum": 70024, "name": "祝福宝珠", "count": 1}, - {"vnum": 1, "name": "金币", "count": 3500000} - ] - return drops - -# 取消/中断挑战 -func cancel_trial() -> Dictionary: - if not is_active: - return {"ok": false, "reason": "NOT_ACTIVE"} - - is_active = false - active_mobs.clear() - return {"ok": true, "msg": "挑战已提前终止。"} - -# 获取状态 -func get_trial_state() -> Dictionary: - return { - "is_active": is_active, - "is_victory": is_victory, - "current_wave": current_wave, - "remaining_mobs": active_mobs.size(), - "time_remaining": time_remaining, - "chest_available": chest_available - } diff --git a/project/cursed_monolith_spire_system.gd.uid b/project/cursed_monolith_spire_system.gd.uid deleted file mode 100644 index 21a9b11e..00000000 --- a/project/cursed_monolith_spire_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://mklhwerocdlu diff --git a/project/demon_tower_system.gd b/project/demon_tower_system.gd deleted file mode 100644 index 297a994a..00000000 --- a/project/demon_tower_system.gd +++ /dev/null @@ -1,225 +0,0 @@ -# demon_tower_system.gd —— 恶魔之塔 1~9 层单机爬塔挑战循环与塔顶免费铁匠(40250 官方 1:1 对齐) -# 对齐源码: -# - 40250 server/share/locale/english/quest/deviltower_zone.quest -# - 40250 server/game/src/dungeon.cpp -# - 40250 server/game/src/char_battle.cpp -class_name DemonTowerSystem -extends RefCounted - -signal floor_changed(floor_num: int, floor_name: String) -signal floor_completed(floor_num: int) -signal tower_completed -signal tower_failed(reason: String) -signal blacksmith_spawned(blacksmith_vnum: int, blacksmith_name: String) - -# 核心怪物与 NPC Vnum (40250 官方标准) -const VNUM_METIN_FLOOR1 := 8015 # 坚韧之石 (1层入口封印) -const VNUM_DEMON_KING := 1091 # 恶魔王 (3层 BOSS) -const VNUM_METIN_FLOOR4 := 8016 # 恶魔封印石 (4层真伪封印石) -const VNUM_ELITE_DEMON_KING := 1092 # 精英恶魔王 (6层 BOSS) -const VNUM_METIN_DEATH := 8017 # 死亡之石 (7层) -const VNUM_DEATH_REAPER := 1093 # 死神 (9层 最终 BOSS) - -# 塔顶免费铁匠 NPC -const NPC_TOWER_WEAPON_SMITH := 20074 # 恶魔之塔武器铁匠 -const NPC_TOWER_ARMOR_SMITH := 20075 # 恶魔之塔防具铁匠 -const NPC_TOWER_JEWELRY_SMITH := 20076 # 恶魔之塔首饰铁匠 - -# 核心任务道具 -const VNUM_ZIN_KEY_FLOOR5 := 50084 # 解除 5 层石碑钥匙 -const VNUM_ZIN_MAP_FLOOR7 := 30302 # 7 层恶魔封印地图 -const VNUM_ZIN_KEY_FLOOR8 := 30304 # 8 层通往塔顶钥匙 -const VNUM_DEATH_REAPER_CHEST := 50082 # 9 层死神宝箱 - -const TOTAL_FLOORS := 9 -const FLOOR_NAMES := { - 1: "第 1 层:恶魔入口", - 2: "第 2 层:恶魔集结所", - 3: "第 3 层:恶魔王大殿", - 4: "第 4 层:迷幻封印之阵", - 5: "第 5 层:五柱封印禁地", - 6: "第 6 层:恶魔之塔顶峰", - 7: "第 7 层:死神迷宫前厅", - 8: "第 8 层:死神结界禁地", - 9: "第 9 层:死神王座决战", -} - -# 运行期状态 -var current_floor := 1 -var is_active := false -var floor_time_left := 1200.0 # 20 分钟单层时限 -var floor4_real_metin_idx := -1 -var floor5_unlocked_pillars := 0 -var floor6_blacksmith_vnum := 0 -var floor6_free_refine_used := false - -func start_tower() -> void: - current_floor = 1 - is_active = true - floor_time_left = 1200.0 - floor4_real_metin_idx = -1 - floor5_unlocked_pillars = 0 - floor6_blacksmith_vnum = 0 - floor6_free_refine_used = false - floor_changed.emit(1, FLOOR_NAMES[1]) - -func get_floor() -> int: - return current_floor - -func get_floor_name() -> String: - return FLOOR_NAMES.get(current_floor, "未知层") - -## 第 1 层判定:击碎封印石 8015 即可通关 -func on_floor1_metin_destroyed(vnum: int) -> bool: - if current_floor != 1 or not is_active: - return false - if vnum == VNUM_METIN_FLOOR1: - _advance_floor() - return true - return false - -## 第 2 层判定:清理全图怪物 -func on_floor2_mobs_cleared(mob_count: int) -> bool: - if current_floor != 2 or not is_active: - return false - if mob_count <= 0: - _advance_floor() - return true - return false - -## 第 3 层判定:击杀恶魔王 1091 -func on_floor3_boss_killed(boss_vnum: int) -> bool: - if current_floor != 3 or not is_active: - return false - if boss_vnum == VNUM_DEMON_KING: - _advance_floor() - return true - return false - -## 第 4 层判定:初始化 7 座封印石并在打破真石时通关 -func setup_floor4_metins() -> void: - # 7 座封印石 (索引 0..6),随机指定 1 座为真石 - floor4_real_metin_idx = randi() % 7 - -func on_floor4_metin_hit(stone_idx: int) -> Dictionary: - if current_floor != 4 or not is_active: - return {"ok": false, "msg": "当前不在第 4 层!"} - if stone_idx == floor4_real_metin_idx: - _advance_floor() - return {"ok": true, "is_real": true, "msg": "找到了真实的恶魔封印石!通往第 5 层的传送门开启!"} - else: - return {"ok": true, "is_real": false, "msg": "这是一座虚假的封印石,迷雾尚未散开!"} - -## 第 5 层判定:使用钥匙解除 5 座恶魔石柱 -func on_floor5_unlock_pillar(key_vnum: int) -> Dictionary: - if current_floor != 5 or not is_active: - return {"ok": false, "msg": "当前不在第 5 层!"} - if key_vnum != VNUM_ZIN_KEY_FLOOR5: - return {"ok": false, "msg": "钥匙不匹配!"} - - floor5_unlocked_pillars += 1 - var remain := 5 - floor5_unlocked_pillars - if floor5_unlocked_pillars >= 5: - _advance_floor() - return {"ok": true, "completed": true, "msg": "5 座恶魔石碑全部解除封印!传送至第 6 层!"} - else: - return {"ok": true, "completed": false, "remain": remain, "msg": "成功解除了一座石碑封印,还剩 %d 座!" % remain} - -## 第 6 层判定:击杀精英恶魔王 1092 并召唤免费铁匠 -func on_floor6_boss_killed(boss_vnum: int) -> Dictionary: - if current_floor != 6 or not is_active: - return {"ok": false, "msg": "当前不在第 6 层!"} - if boss_vnum == VNUM_ELITE_DEMON_KING: - # 随机召唤三位塔顶铁匠之一 - var smiths := [ - {"vnum": NPC_TOWER_WEAPON_SMITH, "name": "恶魔之塔武器铁匠"}, - {"vnum": NPC_TOWER_ARMOR_SMITH, "name": "恶魔之塔防具铁匠"}, - {"vnum": NPC_TOWER_JEWELRY_SMITH, "name": "恶魔之塔首饰铁匠"} - ] - var chosen: Dictionary = smiths[randi() % smiths.size()] - floor6_blacksmith_vnum = chosen["vnum"] - floor6_free_refine_used = false - blacksmith_spawned.emit(chosen["vnum"], chosen["name"]) - return { - "ok": true, - "blacksmith_vnum": chosen["vnum"], - "blacksmith_name": chosen["name"], - "msg": "击杀了精英恶魔王!塔顶现身了一位【%s】!" % chosen["name"] - } - return {"ok": false, "msg": "目标不是精英恶魔王!"} - -## 第 6 层塔顶免费铁匠强化 (无需材料,单人爬塔经典福利) -func perform_tower_free_refine(item: Dictionary, item_type: int) -> Dictionary: - if current_floor != 6 or floor6_blacksmith_vnum == 0: - return {"ok": false, "code": "NO_BLACKSMITH", "msg": "塔顶铁匠尚未出现!"} - if floor6_free_refine_used: - return {"ok": false, "code": "ALREADY_USED", "msg": "恶魔之塔铁匠本次只能免费强化一次!"} - - # 校验铁匠类型与物品类型匹配 - if floor6_blacksmith_vnum == NPC_TOWER_WEAPON_SMITH and item_type != 1: - return {"ok": false, "code": "TYPE_MISMATCH", "msg": "武器铁匠只能强化武器!"} - if floor6_blacksmith_vnum == NPC_TOWER_ARMOR_SMITH and item_type != 2: - return {"ok": false, "code": "TYPE_MISMATCH", "msg": "防具铁匠只能强化防具!"} - - floor6_free_refine_used = true - var cur_vnum := int(item.get("vnum", 0)) - var new_vnum := cur_vnum + 1 - item["vnum"] = new_vnum - return { - "ok": true, - "code": "SUCCESS", - "old_vnum": cur_vnum, - "new_vnum": new_vnum, - "msg": "【%s】为你施展了恶魔神工,免费强化成功!装备晋升为 #%d!" % [ - "武器铁匠" if floor6_blacksmith_vnum == NPC_TOWER_WEAPON_SMITH else "防具铁匠", - new_vnum - ] - } - -## 从第 6 层前往第 7 层 (要求角色等级 >= 75) -func advance_to_floor7(player_level: int) -> Dictionary: - if current_floor != 6: - return {"ok": false, "msg": "不在第 6 层!"} - if player_level < 75: - return {"ok": false, "code": "LEVEL_TOO_LOW", "msg": "只有等级达到 75 级的真正勇者,才能踏入恶魔之塔第 7 层禁地!"} - _advance_floor() - return {"ok": true, "msg": "踏入了死神禁地第 7 层!"} - -## 第 7 层判定:使用恶魔地图 30302 通关 -func on_floor7_map_used(map_vnum: int) -> bool: - if current_floor != 7 or not is_active: - return false - if map_vnum == VNUM_ZIN_MAP_FLOOR7: - _advance_floor() - return true - return false - -## 第 8 层判定:使用钥匙 30304 通关 -func on_floor8_key_used(key_vnum: int) -> bool: - if current_floor != 8 or not is_active: - return false - if key_vnum == VNUM_ZIN_KEY_FLOOR8: - _advance_floor() - return true - return false - -## 第 9 层终极对决:击杀死神 1093 -func on_floor9_boss_killed(boss_vnum: int) -> Dictionary: - if current_floor != 9 or not is_active: - return {"ok": false, "msg": "当前不在第 9 层!"} - if boss_vnum == VNUM_DEATH_REAPER: - is_active = false - tower_completed.emit() - return { - "ok": true, - "code": "TOWER_CLEAR", - "drop_vnum": VNUM_DEATH_REAPER_CHEST, - "msg": "壮举!你成功斩杀了死神,恶魔之塔被彻底征服!获得了【死神宝箱】!" - } - return {"ok": false, "msg": "目标不是死神!"} - -func _advance_floor() -> void: - floor_completed.emit(current_floor) - current_floor += 1 - floor_time_left = 1200.0 - floor_changed.emit(current_floor, FLOOR_NAMES.get(current_floor, "第 %d 层" % current_floor)) diff --git a/project/demon_tower_system.gd.uid b/project/demon_tower_system.gd.uid deleted file mode 100644 index 211d6cf9..00000000 --- a/project/demon_tower_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://berjrd53seoek diff --git a/project/devils_catacomb_system.gd b/project/devils_catacomb_system.gd deleted file mode 100644 index 4c35e487..00000000 --- a/project/devils_catacomb_system.gd +++ /dev/null @@ -1,326 +0,0 @@ -# devils_catacomb_system.gd —— Metin2 40250 地下墓穴 7 层与冥王阿兹瑞尔挑战副本 1:1 -# 对照 40250 服务端 devils_catacomb.quest, dungeon.cpp, char_battle.cpp, mob 2598, 2597, 2596 -class_name DevilsCatacombSystem -extends RefCounted - -signal floor_advanced(floor: int) -signal metin_destroyed(is_correct: bool) -signal tartaros_killed(drop_item: Dictionary) -signal obelisk_activated() -signal charon_killed() -signal azrael_hp_changed(current_hp: int, max_hp: int) -signal catacomb_cleared(drops: Array) -signal catacomb_failed(reason: String) - -const NPC_CATACOMB_GUARD := 20367 -const ITEM_SOUL_CRYSTAL_KEY := 30311 # 灵魂水晶钥匙 (进入与 1 层解锁) -const ITEM_GRIM_SOUL_STONE := 30312 # 冷酷之石 (5 层塔耳塔洛斯掉落) -const MIN_LEVEL := 75 -const TIME_LIMIT := 3600.0 # 60 分钟限时 - -# 首领属性 -const MOB_TARTAROS := 2597 # 5 层守卫 塔耳塔洛斯 -const MOB_CHARON := 2596 # 6 层将军 卡隆 -const MOB_AZRAEL := 2598 # 7 层冥界之王 阿兹瑞尔 - -const AZRAEL_MAX_HP := 900000 -const AZRAEL_BASE_ATK := 2200 -const AZRAEL_BASE_DEF := 680 - -var is_active: bool = false -var current_floor: int = 0 -var remaining_time: float = 0.0 - -# 3 层复仇魔石状态 -var correct_metin_id: int = 3 -var metins_broken: int = 0 - -# 5 层方尖碑状态 -var has_grim_stone: bool = false -var is_obelisk_unlocked: bool = false - -# 7 层阿兹瑞尔状态 -var azrael_hp: int = 0 -var azrael_max_hp: int = AZRAEL_MAX_HP -var is_azrael_alive: bool = false -var azrael_minions_summoned: bool = false - -# 进入地下墓穴 (通过 NPC 20367) -func enter_catacomb(player_level: int, inventory: Array) -> Dictionary: - if player_level < MIN_LEVEL: - return { - "ok": false, - "reason": "LEVEL_TOO_LOW", - "msg": "你的等级不足 75 级,无法承受地下墓穴的死灵死气!" - } - - # 检查 1 层必需的灵魂水晶钥匙 (30311) - var key_slot := -1 - for i in range(inventory.size()): - var item = inventory[i] - if item != null and int(item.get("vnum", 0)) == ITEM_SOUL_CRYSTAL_KEY: - key_slot = i - break - - if key_slot == -1: - return { - "ok": false, - "reason": "MISSING_SOUL_KEY", - "msg": "进入地下墓穴需要持有【灵魂水晶钥匙】!" - } - - # 初始化副本状态进入 1 层 - is_active = true - current_floor = 1 - remaining_time = TIME_LIMIT - correct_metin_id = randi_range(1, 5) - metins_broken = 0 - has_grim_stone = false - is_obelisk_unlocked = false - is_azrael_alive = false - - floor_advanced.emit(1) - - return { - "ok": true, - "floor": 1, - "time_limit": remaining_time, - "msg": "成功踏入地下墓穴一层!限时 60 分钟!" - } - -# 第 1 层:使用灵魂水晶钥匙开启石门封印进入 2 层 -func unlock_floor_1(inventory: Array) -> Dictionary: - if not is_active or current_floor != 1: - return {"ok": false, "reason": "WRONG_FLOOR"} - - var key_slot := -1 - for i in range(inventory.size()): - var item = inventory[i] - if item != null and int(item.get("vnum", 0)) == ITEM_SOUL_CRYSTAL_KEY: - key_slot = i - break - - if key_slot == -1: - return {"ok": false, "reason": "MISSING_KEY", "msg": "背包中缺少灵魂水晶钥匙!"} - - # 消耗钥匙 - inventory[key_slot] = null - current_floor = 2 - floor_advanced.emit(2) - - return { - "ok": true, - "floor": 2, - "msg": "灵魂水晶钥匙已插入封印,石门轰然开启!进入地下墓穴二层!" - } - -# 第 2 层:突破恶灵迷宫进入 3 层 -func complete_floor_2() -> Dictionary: - if not is_active or current_floor != 2: - return {"ok": false, "reason": "WRONG_FLOOR"} - - current_floor = 3 - floor_advanced.emit(3) - return { - "ok": true, - "floor": 3, - "msg": "已突破恶灵迷宫!进入地下墓穴三层!" - } - -# 第 3 层:击碎复仇魔石 (8035) -func break_floor_3_metin(metin_idx: int) -> Dictionary: - if not is_active or current_floor != 3: - return {"ok": false, "reason": "WRONG_FLOOR"} - - metins_broken += 1 - if metin_idx == correct_metin_id: - current_floor = 4 - metin_destroyed.emit(true) - floor_advanced.emit(4) - return { - "ok": true, - "correct": true, - "floor": 4, - "msg": "击碎了真正的复仇魔石!封印瓦解,传送至地下墓穴四层!" - } - else: - metin_destroyed.emit(false) - return { - "ok": true, - "correct": false, - "floor": 3, - "msg": "这是虚假的伪装魔石!周围唤醒了冥界诅咒之魂!" - } - -# 第 4 层:玄武岩石柱迷宫寻路进入 5 层 -func complete_floor_4() -> Dictionary: - if not is_active or current_floor != 4: - return {"ok": false, "reason": "WRONG_FLOOR"} - - current_floor = 5 - floor_advanced.emit(5) - return { - "ok": true, - "floor": 5, - "msg": "成功找到玄武岩迷宫真出口!进入地下墓穴五层!" - } - -# 第 5 层:击败塔耳塔洛斯 (2597) 并获得冷酷之石 (30312) -func defeat_tartaros(inventory: Array) -> Dictionary: - if not is_active or current_floor != 5: - return {"ok": false, "reason": "WRONG_FLOOR"} - - # 放入背包 - var empty_slot := -1 - for i in range(inventory.size()): - if inventory[i] == null: - empty_slot = i - break - - if empty_slot == -1: - return {"ok": false, "reason": "INVENTORY_FULL", "msg": "背包已满,无法拾取冷酷之石!"} - - var grim_stone = {"vnum": ITEM_GRIM_SOUL_STONE, "name": "冷酷之石", "count": 1} - inventory[empty_slot] = grim_stone - has_grim_stone = true - - tartaros_killed.emit(grim_stone) - - return { - "ok": true, - "drop": grim_stone, - "msg": "击败了守卫【塔耳塔洛斯】,获得了【冷酷之石】!请前往方尖碑镶嵌!" - } - -# 第 5 层:将冷酷之石插入玄武岩方尖碑,开启 6 层通道 -func insert_grim_stone_to_obelisk(inventory: Array) -> Dictionary: - if not is_active or current_floor != 5: - return {"ok": false, "reason": "WRONG_FLOOR"} - - var stone_slot := -1 - for i in range(inventory.size()): - var item = inventory[i] - if item != null and int(item.get("vnum", 0)) == ITEM_GRIM_SOUL_STONE: - stone_slot = i - break - - if stone_slot == -1: - return {"ok": false, "reason": "MISSING_STONE", "msg": "背包中缺少冷酷之石!"} - - inventory[stone_slot] = null - is_obelisk_unlocked = true - current_floor = 6 - obelisk_activated.emit() - floor_advanced.emit(6) - - return { - "ok": true, - "floor": 6, - "msg": "冷酷之石完美嵌入方尖碑!冥界封印开启,进入地下墓穴六层!" - } - -# 第 6 层:击败冥界将军卡隆 (2596) 进入 7 层阿兹瑞尔王座 -func defeat_charon() -> Dictionary: - if not is_active or current_floor != 6: - return {"ok": false, "reason": "WRONG_FLOOR"} - - current_floor = 7 - # 初始化 7 层 Boss 阿兹瑞尔 - azrael_hp = AZRAEL_MAX_HP - azrael_max_hp = AZRAEL_MAX_HP - is_azrael_alive = true - azrael_minions_summoned = false - - charon_killed.emit() - floor_advanced.emit(7) - - return { - "ok": true, - "floor": 7, - "boss_hp": azrael_hp, - "msg": "冥界将军【卡隆】已被斩灭!传送至七层【冥王阿兹瑞尔王座】!" - } - -# 第 7 层:对冥王阿兹瑞尔造成伤害 -func attack_azrael(raw_dmg: int) -> Dictionary: - if not is_active or current_floor != 7 or not is_azrael_alive: - return {"ok": false, "reason": "BOSS_NOT_AVAILABLE"} - - var net_dmg: int = max(1, raw_dmg - AZRAEL_BASE_DEF) - azrael_hp = max(0, azrael_hp - net_dmg) - - azrael_hp_changed.emit(azrael_hp, azrael_max_hp) - - # 半血召唤死灵护卫 - var triggered_minions := false - if azrael_hp <= int(AZRAEL_MAX_HP * 0.5) and not azrael_minions_summoned: - azrael_minions_summoned = true - triggered_minions = true - - # 击败阿兹瑞尔 - if azrael_hp <= 0: - is_azrael_alive = false - is_active = false - var drops: Array = _generate_azrael_drops() - catacomb_cleared.emit(drops) - return { - "ok": true, - "killed": true, - "net_dmg": net_dmg, - "remaining_hp": 0, - "drops": drops, - "msg": "【冥王阿兹瑞尔】轰然倒下!地下墓穴已被彻底征服!" - } - - return { - "ok": true, - "killed": false, - "net_dmg": net_dmg, - "remaining_hp": azrael_hp, - "summoned_minions": triggered_minions - } - -# 逐帧倒计时 -func update(dt: float) -> void: - if not is_active: - return - - remaining_time -= dt - if remaining_time <= 0.0: - is_active = false - is_azrael_alive = false - catacomb_failed.emit("TIME_OUT") - -# 生成阿兹瑞尔掉落物 -func _generate_azrael_drops() -> Array: - var drops: Array = [] - - # 1. 必掉阿兹瑞尔专属宝箱 (50187) - drops.append({ - "vnum": 50187, - "name": "阿兹瑞尔宝箱", - "count": 1 - }) - - # 2. 必掉灵魂之石 (50513) - drops.append({ - "vnum": 50513, - "name": "灵魂之石", - "count": 1 - }) - - # 3. 终极泰坦之盾 (13149) 概率掉落 - drops.append({ - "vnum": 13149, - "name": "泰坦之盾+0", - "count": 1 - }) - - # 4. 大额冥界金币 - drops.append({ - "vnum": 1, - "name": "金币", - "count": randi_range(200000, 400000) - }) - - return drops diff --git a/project/devils_catacomb_system.gd.uid b/project/devils_catacomb_system.gd.uid deleted file mode 100644 index b1ad1fc7..00000000 --- a/project/devils_catacomb_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dyqcl4bpdcalf diff --git a/project/dragon_lair_system.gd b/project/dragon_lair_system.gd deleted file mode 100644 index ae7e8409..00000000 --- a/project/dragon_lair_system.gd +++ /dev/null @@ -1,257 +0,0 @@ -# dragon_lair_system.gd —— Metin2 40250 冰龙贝兰-塞陶挑战巢穴 1:1 -# 对照 40250 服务端 dragon_lair.quest, dungeon.cpp, char_battle.cpp, mob 2493, NPC 30121 -class_name DragonLairSystem -extends RefCounted - -signal dungeon_started(time_limit: float) -signal metin_destroyed(metin_vnum: int, metin_name: String) -signal minions_spawned(count: int, mob_vnum: int) -signal dragon_hp_changed(current_hp: int, max_hp: int) -signal dragon_defeated(drops: Array) -signal dungeon_failed(reason: String) - -const NPC_GHOST_OF_SURA := 30121 # 修罗鬼魂 -const ITEM_TWISTED_KEY := 30179 # 扭曲钥匙 -const KEY_REQUIRED_COUNT := 3 -const MIN_LEVEL := 75 -const TIME_LIMIT := 3600.0 # 60 分钟限时 - -const BOSS_VNUM := 2493 # 贝兰-塞陶 (Beran-Setaou) -const BOSS_MAX_HP := 800000 -const BOSS_BASE_ATK := 1800 -const BOSS_BASE_DEF := 650 - -const METIN_TABLE: Dictionary = { - 8031: {"name": "傲慢之石", "desc": "提供 30% 物理抗性减免"}, - 8032: {"name": "寂寞之石", "desc": "提供生命自然恢复"}, - 8033: {"name": "平静之石", "desc": "提供 30% 魔法抗性减免"}, - 8034: {"name": "复仇之石", "desc": "提供 40% 攻击力增幅"} -} - -var is_active: bool = false -var remaining_time: float = 0.0 -var dragon_hp: int = 0 -var dragon_max_hp: int = BOSS_MAX_HP -var is_dragon_alive: bool = false -var active_metins: Dictionary = {} # vnum -> bool (true = alive) -var active_minions: Array = [] -var regen_timer: float = 0.0 - -# 进入冰龙巢穴 (通过 NPC 30121 验证) -func enter_lair(player_level: int, inventory: Array) -> Dictionary: - if player_level < MIN_LEVEL: - return { - "ok": false, - "reason": "LEVEL_TOO_LOW", - "msg": "你的等级不足 75 级,无法承受冰龙巢穴的严寒!" - } - - # 检查背包中的 3 把扭曲钥匙 (30179) - var key_slots: Array = [] - var total_keys: int = 0 - for i in range(inventory.size()): - var item = inventory[i] - if item != null and int(item.get("vnum", 0)) == ITEM_TWISTED_KEY: - var cnt: int = int(item.get("count", 1)) - total_keys += cnt - key_slots.append({"slot": i, "count": cnt}) - - if total_keys < KEY_REQUIRED_COUNT: - return { - "ok": false, - "reason": "MISSING_KEYS", - "current_keys": total_keys, - "required_keys": KEY_REQUIRED_COUNT, - "msg": "进入冰龙巢穴需要 3 把扭曲钥匙!你当前拥有 %d 把。" % total_keys - } - - # 扣除 3 把扭曲钥匙 - var needed: int = KEY_REQUIRED_COUNT - for ks in key_slots: - var slot_idx: int = ks["slot"] - var available: int = ks["count"] - if available <= needed: - needed -= available - inventory[slot_idx] = null - else: - inventory[slot_idx]["count"] = available - needed - needed = 0 - if needed <= 0: - break - - # 初始化巢穴状态 - is_active = true - remaining_time = TIME_LIMIT - dragon_hp = BOSS_MAX_HP - dragon_max_hp = BOSS_MAX_HP - is_dragon_alive = true - active_minions.clear() - regen_timer = 0.0 - - # 刷新 4 块龙神魔石 - active_metins.clear() - for vnum in METIN_TABLE.keys(): - active_metins[vnum] = true - - dungeon_started.emit(remaining_time) - - return { - "ok": true, - "time_limit": remaining_time, - "dragon_hp": dragon_hp, - "metins": active_metins.duplicate(), - "msg": "已使用 3 把扭曲钥匙,成功开启冰龙巢穴!限时 60 分钟!" - } - -# 获取当前冰龙实际攻击力 (受 8034 复仇之石影响) -func get_dragon_atk() -> int: - var atk := BOSS_BASE_ATK - if active_metins.get(8034, false): - atk = int(atk * 1.4) # +40% ATK - return atk - -# 获取当前冰龙防御力 -func get_dragon_def() -> int: - return BOSS_BASE_DEF - -# 击破魔石机制 -func destroy_metin(metin_vnum: int) -> Dictionary: - if not is_active: - return {"ok": false, "reason": "DUNGEON_NOT_ACTIVE"} - - if not active_metins.get(metin_vnum, false): - return {"ok": false, "reason": "METIN_ALREADY_DESTROYED"} - - active_metins[metin_vnum] = false - var mname: String = METIN_TABLE[metin_vnum]["name"] - - # 击破魔石唤醒 4 只塞陶龙宫护卫 (vnum: 2401) - var spawn_count := 4 - for i in range(spawn_count): - active_minions.append({"vnum": 2401, "hp": 30000}) - - metin_destroyed.emit(metin_vnum, mname) - minions_spawned.emit(spawn_count, 2401) - - return { - "ok": true, - "metin_vnum": metin_vnum, - "metin_name": mname, - "remaining_metins": get_alive_metin_count(), - "spawned_minions": spawn_count, - "msg": "【%s】已被击破!对应龙神加成已破除,但唤醒了龙宫护卫!" % mname - } - -# 获取存活魔石数量 -func get_alive_metin_count() -> int: - var count := 0 - for v in active_metins.values(): - if v: - count += 1 - return count - -# 对冰龙造成伤害 -func apply_damage(raw_dmg: int, is_magic: bool = false) -> Dictionary: - if not is_active or not is_dragon_alive: - return {"ok": false, "reason": "DRAGON_NOT_ALIVE"} - - var def := get_dragon_def() - var net_dmg: float = max(1, raw_dmg - def) - - # 8031 傲慢之石减免 30% 物理伤害 - if not is_magic and active_metins.get(8031, false): - net_dmg *= 0.70 - - # 8033 平静之石减免 30% 魔法伤害 - if is_magic and active_metins.get(8033, false): - net_dmg *= 0.70 - - var final_dmg := int(round(net_dmg)) - dragon_hp = max(0, dragon_hp - final_dmg) - - dragon_hp_changed.emit(dragon_hp, dragon_max_hp) - - # 击杀冰龙判定 - if dragon_hp <= 0: - is_dragon_alive = false - is_active = false - var drops := _generate_dragon_drops() - dragon_defeated.emit(drops) - return { - "ok": true, - "killed": true, - "net_dmg": final_dmg, - "remaining_hp": 0, - "drops": drops, - "msg": "【冰龙贝兰-塞陶】已被彻底击败!龙鳞与龙爪散落一地!" - } - - return { - "ok": true, - "killed": false, - "net_dmg": final_dmg, - "remaining_hp": dragon_hp, - "hp_ratio": float(dragon_hp) / float(dragon_max_hp) - } - -# 逐帧更新 (计时与 8032 寂寞之石生命回复) -func update(dt: float) -> void: - if not is_active: - return - - remaining_time -= dt - if remaining_time <= 0.0: - is_active = false - is_dragon_alive = false - dungeon_failed.emit("TIME_OUT") - return - - # 8032 寂寞之石:每 5 秒为冰龙回复 2% 最大生命 (16000 HP) - if is_dragon_alive and active_metins.get(8032, false): - regen_timer += dt - if regen_timer >= 5.0: - regen_timer = 0.0 - var heal_amount := int(BOSS_MAX_HP * 0.02) - dragon_hp = mini(BOSS_MAX_HP, dragon_hp + heal_amount) - dragon_hp_changed.emit(dragon_hp, dragon_max_hp) - -# 生成冰龙掉落物 (71123 龙鳞, 71129 龙爪, 50186 水龙宝箱) -func _generate_dragon_drops() -> Array: - var drops: Array = [] - - # 1. 必掉史诗升阶材料:龙鳞 (71123) - drops.append({ - "vnum": 71123, - "name": "龙鳞", - "count": randi_range(1, 2) - }) - - # 2. 必掉史诗升阶材料:龙爪 (71129) - drops.append({ - "vnum": 71129, - "name": "龙爪", - "count": randi_range(1, 2) - }) - - # 3. 必掉水龙专属宝箱 (50186) - drops.append({ - "vnum": 50186, - "name": "贝兰-塞陶宝箱", - "count": 1 - }) - - # 4. 必掉灵魂之石 (50513) - drops.append({ - "vnum": 50513, - "name": "灵魂之石", - "count": 1 - }) - - # 5. 大额金币掉落 - drops.append({ - "vnum": 1, - "name": "金币", - "count": randi_range(150000, 300000) - }) - - return drops diff --git a/project/dragon_lair_system.gd.uid b/project/dragon_lair_system.gd.uid deleted file mode 100644 index 98990c74..00000000 --- a/project/dragon_lair_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bp1qvx4apa2no diff --git a/project/dragon_soul_system.gd b/project/dragon_soul_system.gd deleted file mode 100644 index 5592d56b..00000000 --- a/project/dragon_soul_system.gd +++ /dev/null @@ -1,215 +0,0 @@ -# dragon_soul_system.gd —— Metin2 40250 龙神炼金石系统 1:1 -# 对照 40250 服务端 DragonSoul.cpp, char_dragonsoul.cpp 及客户端 uidragonsoul.py -class_name DragonSoulSystem -extends RefCounted - -signal deck_activated(deck_idx: int) -signal deck_deactivated(deck_idx: int) - -# 6 种官方龙神石类型 (DragonSoul.cpp:EDSStoneTypes) -enum DragonStoneType { - NONE = 0, - DIAMOND = 1, # 钻石: INT, 魔法抗性 - RUBY = 2, # 红宝石: STR, 物理攻击力, 防御力 - JADE = 3, # 玉石: Max HP, HP 恢复 - SAPPHIRE = 4,# 蓝宝石: DEX, 职业减伤 - GARNET = 5, # 石榴石: CON, Max SP - ONYX = 6 # 缟玛瑙: 格挡, 反弹, 穿透抵抗 -} - -# 5 档品阶 (Grade) -enum Grade { - ROUGH = 0, # 粗糙 - CUT = 1, # 切割 - RARE = 2, # 稀有 - ANCIENT = 3, # 古代 - LEGENDARY = 4 # 传奇 -} - -# 5 档净度 (Clarity) -enum Clarity { - MATT = 0, # 哑光 - CLEAR = 1, # 清晰 - FLAWLESS = 2, # 无瑕 - BRILLIANT = 3, # 辉煌 - EXCELLENT = 4 # 卓越 -} - -const DECK_SKY := 0 # 天空卡槽 -const DECK_EARTH := 1 # 大地卡槽 -const DECK_COUNT := 2 -const SLOTS_PER_DECK := 6 # 每卡槽 6 个神石槽位 - -# 双卡槽数据结构: decks[deck_idx][slot_idx] = stone_dict or null -var decks: Array[Array] = [[], []] -var active_deck: int = -1 # -1: 未激活, 0: 天空卡槽, 1: 大地卡槽 - -func _init() -> void: - for d in range(DECK_COUNT): - var slot_list: Array = [] - for s in range(SLOTS_PER_DECK): - slot_list.append(null) - decks[d] = slot_list - -# 创建一颗龙神石 -static func create_dragon_stone(type: int, grade: int = 0, clarity: int = 0, refine: int = 0, duration: float = 86400.0) -> Dictionary: - var stone: Dictionary = { - "stone_type": type, - "grade": clampi(grade, 0, 4), - "clarity": clampi(clarity, 0, 4), - "refine": clampi(refine, 0, 6), - "duration": duration, # 激活消耗时间 (秒) - "total_duration": duration - } - stone["attributes"] = calculate_stone_attributes(stone) - return stone - -# 计算单颗神石属性 (对齐 40250 DragonSoulTable.txt) -static func calculate_stone_attributes(stone: Dictionary) -> Dictionary: - var type: int = int(stone.get("stone_type", 0)) - var gr: int = int(stone.get("grade", 0)) - var cl: int = int(stone.get("clarity", 0)) - var rf: int = int(stone.get("refine", 0)) - - var attrs: Dictionary = {} - - match type: - DragonStoneType.DIAMOND: - attrs["int_bonus"] = 5 + (gr * 3) + (cl * 2) + rf - attrs["magic_defense"] = 2 + (gr * 2) + (cl * 2) + (rf * 2) - DragonStoneType.RUBY: - attrs["str_bonus"] = 5 + (gr * 3) + (cl * 2) + rf - attrs["att_grade"] = 20 + (gr * 15) + (cl * 10) + (rf * 5) - attrs["def_grade"] = 15 + (gr * 10) + (cl * 5) + (rf * 4) - DragonStoneType.JADE: - attrs["max_hp"] = 300 + (gr * 250) + (cl * 150) + (rf * 100) - attrs["hp_recovery_pct"] = 3 + gr + cl + rf - DragonStoneType.SAPPHIRE: - attrs["dex_bonus"] = 5 + (gr * 3) + (cl * 2) + rf - attrs["resist_warrior"] = 2 + gr + cl + rf - attrs["resist_assassin"] = 2 + gr + cl + rf - DragonStoneType.GARNET: - attrs["con_bonus"] = 5 + (gr * 3) + (cl * 2) + rf - attrs["max_sp"] = 200 + (gr * 150) + (cl * 100) + (rf * 50) - DragonStoneType.ONYX: - attrs["block_pct"] = 2 + (gr * 2) + cl + rf - attrs["reflect_pct"] = 2 + (gr * 2) + cl + rf - - return attrs - -# 装备神石到卡槽 -func equip_stone(deck_idx: int, stone: Dictionary) -> Dictionary: - if deck_idx < 0 or deck_idx >= DECK_COUNT: - return {"ok": false, "reason": "INVALID_DECK"} - - var type: int = int(stone.get("stone_type", 0)) - if type < 1 or type > 6: - return {"ok": false, "reason": "INVALID_STONE_TYPE"} - - var slot_idx: int = type - 1 # 1~6 映射到 0~5 号槽位 - decks[deck_idx][slot_idx] = stone - - return { - "ok": true, - "deck": deck_idx, - "slot": slot_idx, - "stone": stone - } - -# 卸下神石 -func unequip_stone(deck_idx: int, slot_idx: int) -> Dictionary: - if deck_idx < 0 or deck_idx >= DECK_COUNT: - return {"ok": false, "reason": "INVALID_DECK"} - if slot_idx < 0 or slot_idx >= SLOTS_PER_DECK: - return {"ok": false, "reason": "INVALID_SLOT"} - - var removed = decks[deck_idx][slot_idx] - decks[deck_idx][slot_idx] = null - return {"ok": true, "stone": removed} - -# 激活/停用卡槽 -func toggle_deck(deck_idx: int) -> bool: - if active_deck == deck_idx: - active_deck = -1 - deck_deactivated.emit(deck_idx) - return false - else: - if active_deck != -1: - deck_deactivated.emit(active_deck) - active_deck = deck_idx - deck_activated.emit(deck_idx) - return true - -# 计算当前已激活卡槽的总战力属性 -func get_active_bonuses() -> Dictionary: - var total_bonuses: Dictionary = {} - if active_deck < 0 or active_deck >= DECK_COUNT: - return total_bonuses - - var deck: Array = decks[active_deck] - for slot in deck: - if slot != null and float(slot.get("duration", 0.0)) > 0.0: - var attrs: Dictionary = slot.get("attributes", {}) - for key in attrs: - total_bonuses[key] = int(total_bonuses.get(key, 0)) + int(attrs[key]) - - return total_bonuses - -# 逐帧消耗激活中的龙神石能量时间 -func update(dt: float) -> Array: - var expired_stones: Array = [] - if active_deck < 0 or active_deck >= DECK_COUNT: - return expired_stones - - var deck: Array = decks[active_deck] - for i in range(deck.size()): - var stone = deck[i] - if stone != null: - var dur: float = float(stone.get("duration", 0.0)) - dur -= dt - if dur <= 0.0: - dur = 0.0 - expired_stones.append({"deck": active_deck, "slot": i, "stone": stone}) - stone["duration"] = dur - - return expired_stones - -# 升阶 (Grade Refine): 2 颗同种类、同品阶神石融合升级 -static func refine_grade(stone_a: Dictionary, stone_b: Dictionary, force_success: bool = false) -> Dictionary: - var ta: int = int(stone_a.get("stone_type", 0)) - var tb: int = int(stone_b.get("stone_type", 0)) - var ga: int = int(stone_a.get("grade", 0)) - var gb: int = int(stone_b.get("grade", 0)) - - if ta != tb or ga != gb: - return {"ok": false, "reason": "MISMATCHED_STONES", "msg": "只有同种类且同品阶的神石才能融合升阶!"} - if ga >= Grade.LEGENDARY: - return {"ok": false, "reason": "MAX_GRADE", "msg": "该神石已达到最高传奇品阶!"} - - # 成功率 60% - var success: bool = force_success or randf() < 0.60 - if success: - var new_stone = create_dragon_stone(ta, ga + 1, 0, 0) - return {"ok": true, "success": true, "stone": new_stone, "msg": "神石品阶提升成功!"} - else: - return {"ok": true, "success": false, "msg": "升阶失败,融合材料已碎裂。"} - -# 升净度 (Clarity Refine): 2 颗同种类、同品阶、同净度神石融合升级 -static func refine_clarity(stone_a: Dictionary, stone_b: Dictionary, force_success: bool = false) -> Dictionary: - var ta: int = int(stone_a.get("stone_type", 0)) - var tb: int = int(stone_b.get("stone_type", 0)) - var ca: int = int(stone_a.get("clarity", 0)) - var cb: int = int(stone_b.get("clarity", 0)) - - if ta != tb or ca != cb: - return {"ok": false, "reason": "MISMATCHED_STONES", "msg": "只有同种类且同净度的神石才能融合提升净度!"} - if ca >= Clarity.EXCELLENT: - return {"ok": false, "reason": "MAX_CLARITY", "msg": "该神石已达到最高卓越净度!"} - - var success: bool = force_success or randf() < 0.50 - if success: - var ga: int = int(stone_a.get("grade", 0)) - var new_stone = create_dragon_stone(ta, ga, ca + 1, 0) - return {"ok": true, "success": true, "stone": new_stone, "msg": "神石净度提升成功!"} - else: - return {"ok": true, "success": false, "msg": "净度提升失败,辅助材料碎裂。"} diff --git a/project/dragon_soul_system.gd.uid b/project/dragon_soul_system.gd.uid deleted file mode 100644 index 565c5d7e..00000000 --- a/project/dragon_soul_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ia8t4ldbjqhp diff --git a/project/dungeon_ranking_system.gd b/project/dungeon_ranking_system.gd deleted file mode 100644 index 19ab9c2e..00000000 --- a/project/dungeon_ranking_system.gd +++ /dev/null @@ -1,133 +0,0 @@ -# dungeon_ranking_system.gd —— Metin2 40250 地牢信息面板与最速通关记录系统 1:1 -# 对照 40250 服务端与客户端 dungeon_info.cpp, dungeon.cpp, uidungeoninfo.py -class_name DungeonRankingSystem -extends RefCounted - -signal record_updated(dungeon_id: String, fastest_time: float, total_clears: int) -signal new_fastest_record(dungeon_id: String, new_time: float, old_time: float) - -# 40250 官方经典地牢登记表 -const DUNGEON_REGISTRY: Dictionary = { - "demon_tower": { - "name": "恶魔之塔", - "min_level": 40, - "ticket_vnum": 0, - "ticket_count": 0, - "ticket_name": "无", - "cooldown_seconds": 0.0 - }, - "spider_dungeon_3": { - "name": "蜘蛛洞穴三层", - "min_level": 50, - "ticket_vnum": 30327, - "ticket_count": 1, - "ticket_name": "蜘蛛钥匙", - "cooldown_seconds": 1800.0 # 30 分钟 - }, - "dragon_lair": { - "name": "冰龙贝兰巢穴", - "min_level": 75, - "ticket_vnum": 30179, - "ticket_count": 3, - "ticket_name": "扭曲钥匙", - "cooldown_seconds": 3600.0 # 60 分钟 - }, - "devils_catacomb": { - "name": "地下墓穴", - "min_level": 75, - "ticket_vnum": 30311, - "ticket_count": 1, - "ticket_name": "灵魂水晶钥匙", - "cooldown_seconds": 3600.0 # 60 分钟 - } -} - -# 个人通关数据字典: dungeon_id -> Dictionary -var dungeon_records: Dictionary = {} - -func _init() -> void: - for d_id in DUNGEON_REGISTRY.keys(): - dungeon_records[d_id] = { - "total_clears": 0, - "fastest_clear_time": 999999.0, # 初始极大值 - "highest_peak_damage": 0, - "last_clear_timestamp": 0.0 - } - -# 记录通关结算 -func record_clear( - dungeon_id: String, - clear_time_sec: float, - peak_damage: int, - current_timestamp: float = 0.0 -) -> Dictionary: - if not DUNGEON_REGISTRY.has(dungeon_id): - return {"ok": false, "reason": "INVALID_DUNGEON_ID", "msg": "未知的地牢!"} - - var rec = dungeon_records[dungeon_id] - rec["total_clears"] += 1 - rec["last_clear_timestamp"] = current_timestamp - - # 检查并更新最高单次输出伤害 - if peak_damage > int(rec["highest_peak_damage"]): - rec["highest_peak_damage"] = peak_damage - - # 检查是否刷新最速通关记录 - var is_new_record := false - var old_time: float = float(rec["fastest_clear_time"]) - if clear_time_sec < old_time: - is_new_record = true - rec["fastest_clear_time"] = clear_time_sec - new_fastest_record.emit(dungeon_id, clear_time_sec, old_time) - - record_updated.emit(dungeon_id, rec["fastest_clear_time"], rec["total_clears"]) - - var dname: String = DUNGEON_REGISTRY[dungeon_id]["name"] - return { - "ok": true, - "dungeon_id": dungeon_id, - "dungeon_name": dname, - "clear_time": clear_time_sec, - "is_new_record": is_new_record, - "fastest_time": rec["fastest_clear_time"], - "total_clears": rec["total_clears"], - "highest_peak_damage": rec["highest_peak_damage"], - "msg": "【%s】通关成功!耗时 %.1f 秒%s!" % [dname, clear_time_sec, " (刷新最快纪录!)" if is_new_record else ""] - } - -# 获取地牢详情与入场/冷却状态 -func get_dungeon_info(dungeon_id: String, current_timestamp: float = 0.0) -> Dictionary: - if not DUNGEON_REGISTRY.has(dungeon_id): - return {"ok": false, "reason": "INVALID_DUNGEON_ID"} - - var cfg = DUNGEON_REGISTRY[dungeon_id] - var rec = dungeon_records[dungeon_id] - - # 判定冷却时间 - var cd_sec: float = float(cfg["cooldown_seconds"]) - var last_ts: float = float(rec["last_clear_timestamp"]) - var elapsed := current_timestamp - last_ts - var is_on_cooldown := (cd_sec > 0.0 and last_ts > 0.0 and elapsed < cd_sec) - var remaining_cd := maxf(0.0, cd_sec - elapsed) if is_on_cooldown else 0.0 - - return { - "ok": true, - "dungeon_id": dungeon_id, - "name": cfg["name"], - "min_level": cfg["min_level"], - "ticket_vnum": cfg["ticket_vnum"], - "ticket_count": cfg["ticket_count"], - "ticket_name": cfg["ticket_name"], - "is_on_cooldown": is_on_cooldown, - "remaining_cooldown": remaining_cd, - "total_clears": rec["total_clears"], - "fastest_clear_time": rec["fastest_clear_time"] if rec["fastest_clear_time"] < 999999.0 else 0.0, - "highest_peak_damage": rec["highest_peak_damage"] - } - -# 获取全部地牢摘要速览列表 -func get_all_dungeon_summaries(current_timestamp: float = 0.0) -> Array: - var list: Array = [] - for d_id in DUNGEON_REGISTRY.keys(): - list.append(get_dungeon_info(d_id, current_timestamp)) - return list diff --git a/project/dungeon_ranking_system.gd.uid b/project/dungeon_ranking_system.gd.uid deleted file mode 100644 index 6b4b560d..00000000 --- a/project/dungeon_ranking_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c4msy78ir4a3v diff --git a/project/dynamic_bounty_event_system.gd b/project/dynamic_bounty_event_system.gd deleted file mode 100644 index 15b163b5..00000000 --- a/project/dynamic_bounty_event_system.gd +++ /dev/null @@ -1,163 +0,0 @@ -# dynamic_bounty_event_system.gd —— Metin2 40250 单人单机动态事件与赏金通缉令系统 1:1 -# 对照 40250 服务端 questmanager.cpp, char.cpp, quest.h -class_name DynamicBountyEventSystem -extends RefCounted - -signal bounties_refreshed(bounty_list: Array) -signal bounty_progress_updated(bounty_id: String, current: int, target: int) -signal bounty_completed(bounty_id: String, title: String) -signal bounty_reward_claimed(bounty_id: String, exp: int, gold: int) - -const BOUNTY_TYPE_HUNT := "hunt" # 怪物狩猎 -const BOUNTY_TYPE_METIN := "metin" # 魔石破除 -const BOUNTY_TYPE_BOSS := "boss" # 首领讨伐 - -const BOUNTY_CHEST_VNUM := 50180 # 赏金猎人宝箱 (Bounty Chest) - -# 活跃悬赏任务字典: id -> Dictionary -var active_bounties: Dictionary = {} -var total_completed_count: int = 0 - -# 每日自动生成/刷新 3 条特色赏金任务 -func refresh_daily_bounties() -> Array: - active_bounties.clear() - - # 1. 狩猎任务 (野蛮士兵 501, 目标 25 只) - var b1: Dictionary = { - "id": "bounty_hunt_barbarian", - "type": BOUNTY_TYPE_HUNT, - "title": "【通缉令】清剿荒原野蛮士兵", - "target_vnum": 501, - "target_count": 25, - "current_count": 0, - "is_completed": false, - "is_claimed": false, - "rewards": { - "exp": 150000, - "gold": 500000, - "items": [{"vnum": 27003, "name": "红色药水(特大)", "count": 100}] - } - } - - # 2. 破灭魔石任务 (嫉妒之石 8008, 目标 2 颗) - var b2: Dictionary = { - "id": "bounty_metin_jealousy", - "type": BOUNTY_TYPE_METIN, - "title": "【天灾通缉】破灭野外嫉妒魔石", - "target_vnum": 8008, - "target_count": 2, - "current_count": 0, - "is_completed": false, - "is_claimed": false, - "rewards": { - "exp": 300000, - "gold": 1200000, - "items": [{"vnum": 50300, "name": "技能书", "count": 1}] - } - } - - # 3. 讨伐世界首领 (九尾妖狐 1901, 目标 1 次) - var b3: Dictionary = { - "id": "bounty_boss_ninetails", - "type": BOUNTY_TYPE_BOSS, - "title": "【大统领诛杀令】斩落雪山九尾妖狐", - "target_vnum": 1901, - "target_count": 1, - "current_count": 0, - "is_completed": false, - "is_claimed": false, - "rewards": { - "exp": 1000000, - "gold": 3000000, - "items": [ - {"vnum": BOUNTY_CHEST_VNUM, "name": "赏金猎人宝箱", "count": 1}, - {"vnum": 71003, "name": "祝福卷轴", "count": 1} - ] - } - } - - active_bounties[b1["id"]] = b1 - active_bounties[b2["id"]] = b2 - active_bounties[b3["id"]] = b3 - - var list = [b1, b2, b3] - bounties_refreshed.emit(list) - return list - -# 怪物击杀侦测挂钩 -func on_mob_killed(mob_vnum: int, count: int = 1) -> void: - for bid in active_bounties.keys(): - var b = active_bounties[bid] - if b["is_completed"]: - continue - - if (b["type"] == BOUNTY_TYPE_HUNT or b["type"] == BOUNTY_TYPE_BOSS) and b["target_vnum"] == mob_vnum: - b["current_count"] = min(b["target_count"], b["current_count"] + count) - bounty_progress_updated.emit(bid, b["current_count"], b["target_count"]) - - if b["current_count"] >= b["target_count"]: - b["is_completed"] = true - total_completed_count += 1 - bounty_completed.emit(bid, b["title"]) - -# 魔石击碎侦测挂钩 -func on_metin_destroyed(metin_vnum: int) -> void: - for bid in active_bounties.keys(): - var b = active_bounties[bid] - if b["is_completed"]: - continue - - if b["type"] == BOUNTY_TYPE_METIN and b["target_vnum"] == metin_vnum: - b["current_count"] = min(b["target_count"], b["current_count"] + 1) - bounty_progress_updated.emit(bid, b["current_count"], b["target_count"]) - - if b["current_count"] >= b["target_count"]: - b["is_completed"] = true - total_completed_count += 1 - bounty_completed.emit(bid, b["title"]) - -# 领取赏金通缉令奖励 -func claim_bounty_reward(bounty_id: String, player_data: Dictionary, inventory: Array) -> Dictionary: - if not active_bounties.has(bounty_id): - return {"ok": false, "reason": "BOUNTY_NOT_FOUND", "msg": "未找到该通缉令!"} - - var b = active_bounties[bounty_id] - if not b["is_completed"]: - return {"ok": false, "reason": "NOT_COMPLETED", "msg": "该通缉令目标尚未达成!"} - - if b["is_claimed"]: - return {"ok": false, "reason": "ALREADY_CLAIMED", "msg": "该通缉令赏金已被领取!"} - - var rew = b["rewards"] - var exp_gain: int = rew.get("exp", 0) - var gold_gain: int = rew.get("gold", 0) - var items: Array = rew.get("items", []) - - # 发放经验与金币 - player_data["exp"] = int(player_data.get("exp", 0)) + exp_gain - player_data["gold"] = int(player_data.get("gold", 0)) + gold_gain - - # 物品入包 - var granted_items := [] - for it in items: - for i in range(inventory.size()): - if inventory[i] == null: - inventory[i] = it.duplicate(true) - granted_items.append(it["name"]) - break - - b["is_claimed"] = true - bounty_reward_claimed.emit(bounty_id, exp_gain, gold_gain) - - return { - "ok": true, - "bounty_id": bounty_id, - "exp_gain": exp_gain, - "gold_gain": gold_gain, - "items_granted": granted_items, - "msg": "通缉赏金核销!获得 %d 经验、%d 金币以及丰厚物资!" % [exp_gain, gold_gain] - } - -# 获取所有活跃悬赏 -func get_active_bounties() -> Array: - return active_bounties.values() diff --git a/project/dynamic_bounty_event_system.gd.uid b/project/dynamic_bounty_event_system.gd.uid deleted file mode 100644 index 3b434ccc..00000000 --- a/project/dynamic_bounty_event_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dmh3ypb8w0pwt diff --git a/project/emotion_action_system.gd b/project/emotion_action_system.gd deleted file mode 100644 index 9102eb41..00000000 --- a/project/emotion_action_system.gd +++ /dev/null @@ -1,99 +0,0 @@ -# emotion_action_system.gd —— 角色情绪动作与表情快捷键(40250 官方 1:1 对齐) -# 对齐源码: -# - 40250 root/emotion.py (EMOTION_DICT, EMOTION_CLAP..DANCE_6) -# - 40250 server/game/src/cmd_emotion.cpp -# - 40250 root/game.py: __PressNumKey (Ctrl + 1..9) -class_name EmotionActionSystem -extends RefCounted - -# 40250 官方情绪动作常量 -const EMOTION_CLAP := 1 -const EMOTION_CONGRATULATION := 2 -const EMOTION_FORGIVE := 3 -const EMOTION_ANGRY := 4 -const EMOTION_ATTRACTIVE := 5 -const EMOTION_SAD := 6 -const EMOTION_SHY := 7 -const EMOTION_CHEERUP := 8 -const EMOTION_BANTER := 9 -const EMOTION_JOY := 10 -const EMOTION_CHEERS_1 := 11 -const EMOTION_CHEERS_2 := 12 -const EMOTION_DANCE_1 := 13 -const EMOTION_DANCE_2 := 14 -const EMOTION_DANCE_3 := 15 -const EMOTION_DANCE_4 := 16 -const EMOTION_DANCE_5 := 17 -const EMOTION_DANCE_6 := 18 - -# 情绪字典表 (对齐 emotion.py) -const EMOTION_TABLE := { - EMOTION_CLAP: {"name": "鼓掌", "command": "/clap", "motion": "clap"}, - EMOTION_CONGRATULATION: {"name": "祝贺", "command": "/congratulation", "motion": "congratulation"}, - EMOTION_FORGIVE: {"name": "原谅", "command": "/forgive", "motion": "forgive"}, - EMOTION_ANGRY: {"name": "愤怒", "command": "/angry", "motion": "angry"}, - EMOTION_ATTRACTIVE: {"name": "飞吻", "command": "/attractive", "motion": "attractive"}, - EMOTION_SAD: {"name": "悲伤", "command": "/sad", "motion": "sad"}, - EMOTION_SHY: {"name": "害羞", "command": "/shy", "motion": "shy"}, - EMOTION_CHEERUP: {"name": "加油", "command": "/cheerup", "motion": "cheerup"}, - EMOTION_BANTER: {"name": "嘲讽", "command": "/banter", "motion": "banter"}, - EMOTION_JOY: {"name": "喜悦", "command": "/joy", "motion": "joy"}, - EMOTION_CHEERS_1: {"name": "欢呼1", "command": "/cheer1", "motion": "cheers_1"}, - EMOTION_CHEERS_2: {"name": "欢呼2", "command": "/cheer2", "motion": "cheers_2"}, - EMOTION_DANCE_1: {"name": "跳舞1", "command": "/dance1", "motion": "dance_1"}, - EMOTION_DANCE_2: {"name": "跳舞2", "command": "/dance2", "motion": "dance_2"}, - EMOTION_DANCE_3: {"name": "跳舞3", "command": "/dance3", "motion": "dance_3"}, - EMOTION_DANCE_4: {"name": "跳舞4", "command": "/dance4", "motion": "dance_4"}, - EMOTION_DANCE_5: {"name": "跳舞5", "command": "/dance5", "motion": "dance_5"}, - EMOTION_DANCE_6: {"name": "跳舞6", "command": "/dance6", "motion": "dance_6"}, -} - -## 检查是否为有效的情绪动作 ID -static func is_valid_emotion(emotion_id: int) -> bool: - return EMOTION_TABLE.has(emotion_id) - -## 通过聊天命令解析情绪 (例如 "/clap" -> 1) -static func get_emotion_from_command(cmd_text: String) -> int: - var clean := cmd_text.strip_edges().to_lower() - for id in EMOTION_TABLE.keys(): - if EMOTION_TABLE[id]["command"] == clean: - return id - return 0 - -## 通过快捷键序号获取情绪 (0..8 对应 Ctrl + 1..9) -static func get_hotkey_emotion(hotkey_idx: int) -> int: - # 40250 官方快捷栏默认前 9 个动作 - var list := [ - EMOTION_CLAP, - EMOTION_CONGRATULATION, - EMOTION_FORGIVE, - EMOTION_ANGRY, - EMOTION_ATTRACTIVE, - EMOTION_SAD, - EMOTION_SHY, - EMOTION_CHEERUP, - EMOTION_BANTER, - ] - if hotkey_idx >= 0 and hotkey_idx < list.size(): - return list[hotkey_idx] - return 0 - -## 执行情绪动作 -static func perform_emotion(emotion_id: int, is_fighting := false, is_mounted := false) -> Dictionary: - if not is_valid_emotion(emotion_id): - return {"ok": false, "code": "INVALID_EMOTION", "msg": "未知的动作!"} - if is_fighting: - return {"ok": false, "code": "CANNOT_IN_BATTLE", "msg": "战斗中无法做出情绪动作!"} - if is_mounted: - return {"ok": false, "code": "CANNOT_ON_MOUNT", "msg": "骑乘状态下无法做出该动作!"} - - var em: Dictionary = EMOTION_TABLE[emotion_id] - return { - "ok": true, - "code": "SUCCESS", - "emotion_id": emotion_id, - "name": em["name"], - "motion": em["motion"], - "command": em["command"], - "msg": "执行了【%s】动作。" % em["name"] - } diff --git a/project/emotion_action_system.gd.uid b/project/emotion_action_system.gd.uid deleted file mode 100644 index e6031ec8..00000000 --- a/project/emotion_action_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c7ffckxgf2fyf diff --git a/project/empire_kingdom_system.gd b/project/empire_kingdom_system.gd deleted file mode 100644 index b8e3216b..00000000 --- a/project/empire_kingdom_system.gd +++ /dev/null @@ -1,213 +0,0 @@ -# empire_kingdom_system.gd —— Metin2 40250 三圣帝国国家归属与领地国运系统 1:1 -# 对照 40250 服务端 empire.cpp, questlua_empire.cpp, root/introempire.py -class_name EmpireKingdomSystem -extends RefCounted - -signal empire_selected(empire_id: int, empire_name: String) -signal empire_blessing_activated(empire_id: int, blessings: Dictionary) -signal empire_changed(old_empire: int, new_empire: int) -signal territory_entered(map_name: String, is_home: bool, is_hostile: bool) - -# 40250 官方三大帝国 ID -const EMPIRE_NONE := 0 -const EMPIRE_SHINSOO := 1 # 慎修帝国 (Shinsoo / 红国) -const EMPIRE_CHUNJO := 2 # 准照帝国 (Chunjo / 黄国) -const EMPIRE_JINNO := 3 # 晋诺帝国 (Jinno / 蓝国) - -const VNUM_TINCTURE_OF_EMPIRES := 71054 # 转国卷轴 (Tincture of Empires) - -# 三大帝国官方档案与专属祈福特性表 -const EMPIRES_INFO: Dictionary = { - EMPIRE_SHINSOO: { - "id": EMPIRE_SHINSOO, - "name": "慎修帝国", - "color": "红国", - "capital": "永安省 (Yongan)", - "desc": "南方沿海商业贸易帝国,商贾云集,财富充盈。", - "home_maps": ["Yongan", "Jayang"], - "blessings": { - "gold_bonus_pct": 10, # 金币获取 +10% - "shop_discount_pct": 5 # 商店折扣 5% - } - }, - EMPIRE_CHUNJO: { - "id": EMPIRE_CHUNJO, - "name": "准照帝国", - "color": "黄国", - "capital": "准安省 (Joan)", - "desc": "西方高原神圣宗教帝国,受大祭司与神官庇护,信仰虔诚。", - "home_maps": ["Joan", "Bokjung"], - "blessings": { - "cast_speed": 10, # 施法速度 +10 - "cooldown_reduction_pct": 5 # 技能冷却缩减 5% - } - }, - EMPIRE_JINNO: { - "id": EMPIRE_JINNO, - "name": "晋诺帝国", - "color": "蓝国", - "capital": "平武省 (Pyungmoo)", - "desc": "东方雄关铁血军事帝国,崇尚武道,兵强马壮。", - "home_maps": ["Pyungmoo", "Bakra"], - "blessings": { - "attack_bonus": 50, # 物理攻击 +50 - "monster_damage_pct": 5 # 对怪增伤 5% - } - } -} - -var current_empire: int = EMPIRE_NONE -var is_blessing_active: bool = false -var blessing_time_remaining: float = 0.0 # 祈福持续时间 (秒) -const BLESSING_DURATION: float = 86400.0 # 24 小时 (86400 秒) - -# 初始选择国家 -func select_empire(empire_id: int) -> Dictionary: - if not EMPIRES_INFO.has(empire_id): - return {"ok": false, "reason": "INVALID_EMPIRE", "msg": "无效的帝国代码!"} - - if current_empire != EMPIRE_NONE: - return {"ok": false, "reason": "ALREADY_HAS_EMPIRE", "msg": "已有国家归属,需使用转国卷轴!"} - - current_empire = empire_id - var info: Dictionary = EMPIRES_INFO[current_empire] - empire_selected.emit(current_empire, info["name"]) - - return { - "ok": true, - "empire_id": current_empire, - "name": info["name"], - "capital": info["capital"], - "msg": "效忠【%s】成功!开启帝国护国征途。" % info["name"] - } - -# 激活每日帝国国运祈福 (Daily Empire Blessing) -func activate_empire_prayer(player_stats: Dictionary) -> Dictionary: - if current_empire == EMPIRE_NONE: - return {"ok": false, "reason": "NO_EMPIRE", "msg": "尚未加入任何帝国!"} - - if is_blessing_active: - # 刷新时长 - blessing_time_remaining = BLESSING_DURATION - return {"ok": true, "msg": "国运祈福时长已刷新为 24 小时!"} - - var blessings: Dictionary = EMPIRES_INFO[current_empire]["blessings"] - _apply_blessings_to_player(blessings, player_stats, 1) - - is_blessing_active = true - blessing_time_remaining = BLESSING_DURATION - - empire_blessing_activated.emit(current_empire, blessings) - - return { - "ok": true, - "empire_id": current_empire, - "blessings": blessings, - "duration": BLESSING_DURATION, - "msg": "获得【%s】护国祈福!全境增益持续 24 小时。" % EMPIRES_INFO[current_empire]["name"] - } - -# 倒计时推进 -func update(delta: float, player_stats: Dictionary) -> void: - if is_blessing_active: - blessing_time_remaining -= delta - if blessing_time_remaining <= 0.0: - # 祈福过期 - var blessings: Dictionary = EMPIRES_INFO[current_empire]["blessings"] - _apply_blessings_to_player(blessings, player_stats, -1) - is_blessing_active = false - blessing_time_remaining = 0.0 - -# 领地巡检:判断玩家在某地图是否处于敌国入侵状态 -func check_territory(map_name: String) -> Dictionary: - if current_empire == EMPIRE_NONE: - return {"is_home": false, "is_hostile": false, "relation": "neutral"} - - var my_home_maps: Array = EMPIRES_INFO[current_empire]["home_maps"] - if my_home_maps.has(map_name): - territory_entered.emit(map_name, true, false) - return {"is_home": true, "is_hostile": false, "relation": "homeland"} - - # 检查是否为其他两个帝国的本土领地 - for other_id in [EMPIRE_SHINSOO, EMPIRE_CHUNJO, EMPIRE_JINNO]: - if other_id == current_empire: - continue - var foreign_maps: Array = EMPIRES_INFO[other_id]["home_maps"] - if foreign_maps.has(map_name): - territory_entered.emit(map_name, false, true) - return { - "is_home": false, - "is_hostile": true, - "hostile_empire": EMPIRES_INFO[other_id]["name"], - "relation": "hostile_invasion" - } - - # 中立公共地图 (如沙漠、雪山、烈焰、神殿) - territory_entered.emit(map_name, false, false) - return {"is_home": false, "is_hostile": false, "relation": "neutral"} - -# 使用转国卷轴更换帝国 (40250 Tincture of Empires 71054) -func change_empire(target_empire: int, item_slot: int, inventory: Array, player_stats: Dictionary, is_in_guild: bool = false) -> Dictionary: - if not EMPIRES_INFO.has(target_empire): - return {"ok": false, "reason": "INVALID_TARGET_EMPIRE", "msg": "无效的目标帝国!"} - - if target_empire == current_empire: - return {"ok": false, "reason": "CANNOT_CHANGE_TO_SAME_EMPIRE", "msg": "无法转入当前已效忠的国家!"} - - if is_in_guild: - return {"ok": false, "reason": "MUST_LEAVE_GUILD", "msg": "已有公会归属,必须先退出公会方可转国!"} - - if item_slot < 0 or item_slot >= inventory.size() or inventory[item_slot] == null: - return {"ok": false, "reason": "INVALID_ITEM_SLOT"} - - var scroll: Dictionary = inventory[item_slot] - if int(scroll.get("vnum", 0)) != VNUM_TINCTURE_OF_EMPIRES: - return {"ok": false, "reason": "NOT_A_TINCTURE_ITEM", "msg": "需要消耗【转国卷轴】(71054)!"} - - # 若当前已有祈福加成,先解除旧帝国祈福 - if is_blessing_active: - var old_blessings: Dictionary = EMPIRES_INFO[current_empire]["blessings"] - _apply_blessings_to_player(old_blessings, player_stats, -1) - is_blessing_active = false - blessing_time_remaining = 0.0 - - # 消耗转国卷轴 - var cnt := int(scroll.get("count", 1)) - if cnt > 1: - scroll["count"] = cnt - 1 - else: - inventory[item_slot] = null - - var old_emp := current_empire - current_empire = target_empire - - empire_changed.emit(old_emp, current_empire) - - var new_info: Dictionary = EMPIRES_INFO[current_empire] - return { - "ok": true, - "old_empire": old_emp, - "new_empire": current_empire, - "new_empire_name": new_info["name"], - "msg": "成功转入【%s】!请前往母国【%s】领取新的帝国嘉奖。" % [new_info["name"], new_info["capital"]] - } - -func _apply_blessings_to_player(blessings: Dictionary, player_stats: Dictionary, factor: int) -> void: - for k in blessings.keys(): - var val = int(blessings[k]) * factor - var cur = int(player_stats.get(k, 0)) - player_stats[k] = cur + val - -# 序列化 -func serialize() -> Dictionary: - return { - "current_empire": current_empire, - "is_blessing_active": is_blessing_active, - "blessing_time_remaining": blessing_time_remaining - } - -# 反序列化 -func deserialize(data: Dictionary) -> void: - current_empire = int(data.get("current_empire", EMPIRE_NONE)) - is_blessing_active = bool(data.get("is_blessing_active", false)) - blessing_time_remaining = float(data.get("blessing_time_remaining", 0.0)) diff --git a/project/empire_kingdom_system.gd.uid b/project/empire_kingdom_system.gd.uid deleted file mode 100644 index 0c4bd2d9..00000000 --- a/project/empire_kingdom_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cw0e8mkuuaupg diff --git a/project/endless_spire_dungeon_system.gd b/project/endless_spire_dungeon_system.gd deleted file mode 100644 index dc70db86..00000000 --- a/project/endless_spire_dungeon_system.gd +++ /dev/null @@ -1,194 +0,0 @@ -# endless_spire_dungeon_system.gd —— Metin2 40250 单人单机迷宫地牢【无尽天关】爬塔挑战 1:1 -# 对照 40250 服务端 dungeon.cpp, dungeon_info.cpp, questmanager.cpp -class_name EndlessSpireDungeonSystem -extends RefCounted - -signal floor_started(floor_num: int, is_boss_floor: bool, mob_count: int) -signal floor_cleared(floor_num: int, points_earned: int, time_used: float) -signal checkpoint_saved(checkpoint_floor: int) -signal spire_trial_completed(highest_floor: int, total_points: int) -signal spire_trial_failed(floor_num: int, reason: String) - -const MAX_FLOORS := 50 -const FLOOR_TIME_LIMIT := 120.0 # 每层 120 秒限时 - -const MILESTONE_BOSSES: Dictionary = { - 5: {"name": "半兽人首领", "vnum": 691, "hp": 50000}, - 10: {"name": "蜘蛛女王", "vnum": 2091, "hp": 80000}, - 15: {"name": "沙漠巨怪", "vnum": 2191, "hp": 120000}, - 20: {"name": "恶魔死神", "vnum": 1093, "hp": 180000}, - 25: {"name": "烈焰领主", "vnum": 2206, "hp": 220000}, - 30: {"name": "九尾妖狐", "vnum": 1901, "hp": 280000}, - 35: {"name": "黄虎之魂", "vnum": 1304, "hp": 320000}, - 40: {"name": "冥王阿兹瑞尔", "vnum": 2598, "hp": 400000}, - 45: {"name": "冰龙贝兰-塞陶", "vnum": 2493, "hp": 500000}, - 50: {"name": "混沌魔神·阿萨泽尔", "vnum": 8027, "hp": 800000} -} - -var is_in_trial: bool = false -var current_floor: int = 1 -var highest_checkpoint: int = 1 -var total_points: int = 0 -var floor_time_remaining: float = 0.0 - -var active_mobs: Dictionary = {} # mob_id -> { "vnum", "alive" } -var next_mob_id: int = 1 -var personal_best_floor: int = 0 - -# 开启爬塔挑战 -func start_trial(from_checkpoint: bool = false) -> Dictionary: - if is_in_trial: - return {"ok": false, "reason": "ALREADY_IN_TRIAL"} - - is_in_trial = true - current_floor = highest_checkpoint if from_checkpoint else 1 - total_points = 0 - next_mob_id = 1 - - _start_floor(current_floor) - - return { - "ok": true, - "starting_floor": current_floor, - "checkpoint": highest_checkpoint, - "msg": "踏入【无尽天关】第 %d 层!" % current_floor - } - -# 开启指定层数 -func _start_floor(floor_num: int) -> void: - current_floor = floor_num - floor_time_remaining = FLOOR_TIME_LIMIT - active_mobs.clear() - - var is_boss = MILESTONE_BOSSES.has(floor_num) - if is_boss: - var b_cfg = MILESTONE_BOSSES[floor_num] - var mid = next_mob_id - next_mob_id += 1 - active_mobs[mid] = { - "id": mid, - "vnum": b_cfg["vnum"], - "name": b_cfg["name"], - "is_boss": true, - "alive": true - } - floor_started.emit(floor_num, true, 1) - else: - # 普通层小怪 - var mob_count = 4 + (floor_num % 5) * 2 - for i in range(mob_count): - var mid = next_mob_id - next_mob_id += 1 - active_mobs[mid] = { - "id": mid, - "vnum": 101 + (floor_num % 10), - "name": "天关守卫", - "is_boss": false, - "alive": true - } - floor_started.emit(floor_num, false, mob_count) - -# 击杀当前层怪物 -func kill_mob(mob_id: int) -> Dictionary: - if not is_in_trial: - return {"ok": false, "reason": "NOT_IN_TRIAL"} - - if not active_mobs.has(mob_id) or not active_mobs[mob_id]["alive"]: - return {"ok": false, "reason": "MOB_NOT_FOUND"} - - active_mobs[mob_id]["alive"] = false - active_mobs.erase(mob_id) - - # 检查本层是否清空 - if active_mobs.is_empty(): - return _on_floor_cleared() - - return { - "ok": true, - "remaining_mobs": active_mobs.size() - } - -# 本层通关结算 -func _on_floor_cleared() -> Dictionary: - var time_used = FLOOR_TIME_LIMIT - floor_time_remaining - var points_awarded = current_floor * 10 - total_points += points_awarded - - if current_floor > personal_best_floor: - personal_best_floor = current_floor - - floor_cleared.emit(current_floor, points_awarded, time_used) - - # 检查点保存判定 (逢 5 层通关) - if current_floor % 5 == 0: - var next_cp = current_floor + 1 - if next_cp > highest_checkpoint and next_cp <= MAX_FLOORS: - highest_checkpoint = next_cp - checkpoint_saved.emit(highest_checkpoint) - - # 终极 50 层通关判定 - if current_floor >= MAX_FLOORS: - is_in_trial = false - spire_trial_completed.emit(MAX_FLOORS, total_points) - return { - "ok": true, - "trial_completed": true, - "highest_floor": MAX_FLOORS, - "total_points": total_points, - "msg": "登峰造极!你已彻底通关【无尽天关】第 50 层巅峰!" - } - - # 进阶下一层 - var next_f = current_floor + 1 - _start_floor(next_f) - - return { - "ok": true, - "floor_cleared": current_floor - 1, - "next_floor": next_f, - "points": total_points, - "msg": "第 %d 层顺利通关!进入第 %d 层!" % [current_floor - 1, next_f] - } - -# 帧心跳更新限时 -func update(delta: float) -> Dictionary: - if not is_in_trial: - return {"status": "inactive"} - - floor_time_remaining -= delta - if floor_time_remaining <= 0.0: - floor_time_remaining = 0.0 - is_in_trial = false - spire_trial_failed.emit(current_floor, "TIME_EXPIRED") - return { - "status": "failed", - "reason": "TIME_EXPIRED", - "floor": current_floor, - "msg": "时间耗尽!被传送出天关。" - } - - return { - "status": "active", - "floor": current_floor, - "remaining_time": floor_time_remaining, - "remaining_mobs": active_mobs.size() - } - -# 中途离开挑战 -func exit_trial() -> Dictionary: - if not is_in_trial: - return {"ok": false, "reason": "NOT_IN_TRIAL"} - is_in_trial = false - active_mobs.clear() - return {"ok": true, "msg": "已主动退出天关,检查点进度已保存。"} - -# 获取挑战概览 -func get_spire_status() -> Dictionary: - return { - "is_in_trial": is_in_trial, - "current_floor": current_floor, - "highest_checkpoint": highest_checkpoint, - "personal_best_floor": personal_best_floor, - "total_points": total_points, - "remaining_time": floor_time_remaining - } diff --git a/project/endless_spire_dungeon_system.gd.uid b/project/endless_spire_dungeon_system.gd.uid deleted file mode 100644 index 87d27fe8..00000000 --- a/project/endless_spire_dungeon_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c6sc72uf1btdv diff --git a/project/energy_system.gd b/project/energy_system.gd deleted file mode 100644 index 9767e2b8..00000000 --- a/project/energy_system.gd +++ /dev/null @@ -1,237 +0,0 @@ -# energy_system.gd —— Metin2 40250 官方能量碎片提取与能量水晶熔炼系统 1:1 -# 对照 40250 服务端 energy_system.quest, char_item.cpp:6319 (CBuffOnAttributes), char.h (POINT_ENERGY=128) -class_name EnergySystem -extends RefCounted - -signal fragments_extracted(count: int, item_name: String) -signal crystal_fused(crystal_item: Dictionary) -signal energy_activated(duration: float, bonus_pct: int) -signal energy_expired() - -const NPC_ALCHEMIST := 20001 # 炼金术士 -const MIN_PLAYER_LEVEL := 35 # 角色开启门槛 35 级 -const MIN_EQUIP_LEVEL := 35 # 装备分解门槛 35 级 - -const VNUM_ENERGY_STONE := 51001 # 能量碎片 (Energy Stone / Fragment) -const VNUM_CHARGING_STONE := 51002 # 能量水晶 (Charging Stone / Energy Crystal) - -const FUSION_NEED_STONES := 30 # 熔炼需要 30 颗碎片 -const FUSION_COST_GOLD := 1000 # 熔炼手续费 1000 金币 -const ENERGY_DURATION := 7200.0 # 2 小时 (7200 秒) -const ATTR_BONUS_PCT := 10 # 全身穿戴装备属性增幅 +10% - -var is_energy_active: bool = false -var energy_time_remaining: float = 0.0 -var applied_bonuses: Dictionary = {} - -# 计算装备分解产出碎片数量 (40250 energy_system.quest 概率阶梯) -static func calculate_fragment_yield(equip_level_limit: int, rand_val: int = -1) -> int: - var r: int = rand_val if rand_val >= 0 else (randi() % 100) - if equip_level_limit >= 35 and equip_level_limit <= 50: - # 35~50 级装备产出 1~5 颗 - return clampi(1 + (r % 5), 1, 5) - elif equip_level_limit > 50 and equip_level_limit <= 70: - # 51~70 级装备产出 3~8 颗 - return clampi(3 + (r % 6), 3, 8) - else: - # 70 级以上顶级装备产出 5~15 颗 - return clampi(5 + (r % 11), 5, 15) - -# 在炼金术士处分解淘汰装备提取能量碎片 -func extract_energy_fragments(inventory: Array, slot_index: int, player_level: int) -> Dictionary: - if player_level < MIN_PLAYER_LEVEL: - return {"ok": false, "reason": "PLAYER_LEVEL_TOO_LOW", "msg": "炼金术士只为 35 级以上的强者提取能量!"} - - if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null: - return {"ok": false, "reason": "INVALID_SLOT"} - - var item: Dictionary = inventory[slot_index] - var limit_lv := int(item.get("level_limit", item.get("limit_level", 0))) - if limit_lv < MIN_EQUIP_LEVEL: - return {"ok": false, "reason": "EQUIP_LEVEL_TOO_LOW", "msg": "只能分解 35 级以上的武器、防具或首饰!"} - - # 检查箭矢等消耗品 - if item.get("type", 0) == 1 and item.get("subtype", 0) == 5: # WEAPON_ARROW - return {"ok": false, "reason": "ARROW_NOT_ALLOWED", "msg": "箭矢无法提取能量!"} - - var item_name: String = str(item.get("name", "淘汰装备")) - var count_yield := calculate_fragment_yield(limit_lv) - - # 移除装备 - inventory[slot_index] = null - - # 发放能量碎片 (查找已有堆叠或找空位) - var placed := false - for i in range(inventory.size()): - if inventory[i] != null and int(inventory[i].get("vnum", 0)) == VNUM_ENERGY_STONE: - var cur_c := int(inventory[i].get("count", 1)) - inventory[i]["count"] = cur_c + count_yield - placed = true - break - - if not placed: - for i in range(inventory.size()): - if inventory[i] == null: - inventory[i] = { - "vnum": VNUM_ENERGY_STONE, - "name": "能量碎片", - "count": count_yield - } - placed = true - break - - fragments_extracted.emit(count_yield, item_name) - - return { - "ok": true, - "yield": count_yield, - "item_name": item_name, - "msg": "成功分解【%s】,提取出 %d 颗【能量碎片】!" % [item_name, count_yield] - } - -# 熔炼能量水晶 (30 碎片 + 1000 金币) -func fuse_energy_crystal(inventory: Array, player_gold: Dictionary) -> Dictionary: - var cur_gold := int(player_gold.get("gold", 0)) - if cur_gold < FUSION_COST_GOLD: - return {"ok": false, "reason": "NOT_ENOUGH_GOLD", "msg": "金币不足!熔炼需要 1,000 金币。"} - - # 统计能量碎片总数 - var total_stones := 0 - var stone_slots: Array = [] - for i in range(inventory.size()): - if inventory[i] != null and int(inventory[i].get("vnum", 0)) == VNUM_ENERGY_STONE: - total_stones += int(inventory[i].get("count", 1)) - stone_slots.append(i) - - if total_stones < FUSION_NEED_STONES: - return { - "ok": false, - "reason": "NOT_ENOUGH_STONES", - "msg": "能量碎片不足!需要 %d 颗,当前拥有 %d 颗。" % [FUSION_NEED_STONES, total_stones] - } - - # 寻找放置能量水晶的槽位 - var crystal_slot := -1 - for i in range(inventory.size()): - if inventory[i] == null: - crystal_slot = i - break - - # 扣除 30 颗碎片 - var need_to_deduct := FUSION_NEED_STONES - for slot_idx in stone_slots: - var it: Dictionary = inventory[slot_idx] - var c := int(it.get("count", 1)) - if c <= need_to_deduct: - need_to_deduct -= c - inventory[slot_idx] = null - if crystal_slot == -1: - crystal_slot = slot_idx # 复用清空的格子 - else: - it["count"] = c - need_to_deduct - need_to_deduct = 0 - break - - player_gold["gold"] = cur_gold - FUSION_COST_GOLD - - var crystal := { - "vnum": VNUM_CHARGING_STONE, - "name": "能量水晶", - "count": 1 - } - inventory[crystal_slot] = crystal - - crystal_fused.emit(crystal) - - return { - "ok": true, - "crystal": crystal, - "slot": crystal_slot, - "msg": "熔炼成功!纯净的【能量水晶】已凝聚成型。" - } - -# 使用能量水晶激活 2 小时全身穿戴装备 10% 增幅 (40250 CBuffOnAttributes) -func use_energy_crystal(inventory: Array, slot_index: int, equipped_items: Array, player_stats: Dictionary) -> Dictionary: - if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null: - return {"ok": false, "reason": "INVALID_SLOT"} - - var item: Dictionary = inventory[slot_index] - if int(item.get("vnum", 0)) != VNUM_CHARGING_STONE: - return {"ok": false, "reason": "NOT_A_CHARGING_STONE", "msg": "此物品不是能量水晶!"} - - # 消耗 1 颗能量水晶 - var c := int(item.get("count", 1)) - if c > 1: - item["count"] = c - 1 - else: - inventory[slot_index] = null - - # 如果已激活,先清除旧加成 - if is_energy_active: - _clear_energy_buffs(player_stats) - - # 计算穿戴装备基础属性总和的 10% - applied_bonuses.clear() - for eq in equipped_items: - if eq == null: - continue - var atk = int(eq.get("attack_power", 0)) - var def = int(eq.get("defense", 0)) - var hp = int(eq.get("max_hp", 0)) - if atk > 0: - applied_bonuses["attack_power"] = int(applied_bonuses.get("attack_power", 0)) + int(ceil(atk * 0.10)) - if def > 0: - applied_bonuses["defense"] = int(applied_bonuses.get("defense", 0)) + int(ceil(def * 0.10)) - if hp > 0: - applied_bonuses["max_hp"] = int(applied_bonuses.get("max_hp", 0)) + int(ceil(hp * 0.10)) - - # 保底全属性增益 (即使没有极品装备,也提供保底加成) - applied_bonuses["attack_power"] = maxi(10, int(applied_bonuses.get("attack_power", 0))) - applied_bonuses["defense"] = maxi(10, int(applied_bonuses.get("defense", 0))) - - # 注入玩家属性 - for k in applied_bonuses.keys(): - player_stats[k] = int(player_stats.get(k, 0)) + applied_bonuses[k] - - is_energy_active = true - energy_time_remaining = ENERGY_DURATION - - energy_activated.emit(ENERGY_DURATION, ATTR_BONUS_PCT) - - return { - "ok": true, - "duration": ENERGY_DURATION, - "bonuses": applied_bonuses, - "msg": "能量水晶激活!全身装备属性增幅 +10%%,持续 2 小时。" - } - -# 清除能量属性 -func _clear_energy_buffs(player_stats: Dictionary) -> void: - for k in applied_bonuses.keys(): - player_stats[k] = int(player_stats.get(k, 0)) - applied_bonuses[k] - applied_bonuses.clear() - is_energy_active = false - energy_time_remaining = 0.0 - -# 时钟心跳推进 -func update(delta: float, player_stats: Dictionary) -> void: - if not is_energy_active: - return - energy_time_remaining -= delta - if energy_time_remaining <= 0.0: - _clear_energy_buffs(player_stats) - energy_expired.emit() - -# 序列化 -func serialize() -> Dictionary: - return { - "is_energy_active": is_energy_active, - "energy_time_remaining": energy_time_remaining, - "applied_bonuses": applied_bonuses.duplicate(true) - } - -# 反序列化 -func deserialize(data: Dictionary) -> void: - is_energy_active = bool(data.get("is_energy_active", false)) - energy_time_remaining = float(data.get("energy_time_remaining", 0.0)) - applied_bonuses = data.get("applied_bonuses", {}).duplicate(true) diff --git a/project/energy_system.gd.uid b/project/energy_system.gd.uid deleted file mode 100644 index 4ec81ed3..00000000 --- a/project/energy_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ckdibi7t8851x diff --git a/project/epic_refine_system.gd b/project/epic_refine_system.gd deleted file mode 100644 index 71230f69..00000000 --- a/project/epic_refine_system.gd +++ /dev/null @@ -1,210 +0,0 @@ -# epic_refine_system.gd —— Metin2 40250 修元平史诗级神装升阶转换锻造 1:1 -# 对照 40250 服务端 refine.cpp, char_item.cpp, NPC 20091 (修元平 Seon-Pyeong) -class_name EpicRefineSystem -extends RefCounted - -signal epic_refine_succeeded(old_vnum: int, new_vnum: int, item_data: Dictionary) -signal epic_refine_failed(reason: String) - -const NPC_SEON_PYEONG := 20091 - -const ITEM_DRAGON_SCALE := 71123 # 龙鳞 -const ITEM_DRAGON_CLAW := 71129 # 龙爪 -const REQUIRED_SCALE_COUNT := 3 -const REQUIRED_CLAW_COUNT := 4 -const REQUIRED_YANG := 200000 - -# 官方 40250 经典史诗升阶配方表 -const EPIC_RECIPES: Dictionary = { - # 武器篇:65 级 +9 武器 -> 80~90 级 +0 史诗神兵 - 169: { - "target_vnum": 279, - "name": "崔顿之剑+0", - "desc": "战神之剑+9 升阶为 崔顿之剑+0" - }, - 3159: { - "target_vnum": 3209, - "name": "神圣之剑+0", - "desc": "鬼牙刃+9 升阶为 神圣之剑+0" - }, - 1109: { - "target_vnum": 1139, - "name": "太阳之刃+0", - "desc": "党人斧+9 升阶为 太阳之刃+0" - }, - 1119: { - "target_vnum": 1129, - "name": "龙神刀+0", - "desc": "龙刀+9 升阶为 龙神刀+0" - }, - 2149: { - "target_vnum": 2159, - "name": "巨鸦弓+0", - "desc": "黄龙弓+9 升阶为 巨鸦弓+0" - }, - 5109: { - "target_vnum": 5119, - "name": "恶魔之杖+0", - "desc": "救赎之杖+9 升阶为 恶魔之杖+0" - }, - # 铠甲篇:66 级 +9 黑钢铠甲 -> 90 级 +0 蓝钢铠甲 - 11299: { - "target_vnum": 12019, - "name": "蓝钢甲+0", - "desc": "黑钢甲+9 升阶为 蓝钢甲+0" - }, - 11499: { - "target_vnum": 12029, - "name": "蓝风甲+0", - "desc": "黑风甲+9 升阶为 蓝风甲+0" - }, - 11699: { - "target_vnum": 12039, - "name": "蓝仙袍+0", - "desc": "黑仙袍+9 升阶为 蓝仙袍+0" - }, - 11899: { - "target_vnum": 12049, - "name": "蓝龙袍+0", - "desc": "黑龙袍+9 升阶为 蓝龙袍+0" - } -} - -# 判定物品是否可进行史诗升阶 -func can_epic_refine(item: Dictionary) -> bool: - if item.is_empty(): - return false - var vnum: int = int(item.get("vnum", 0)) - return EPIC_RECIPES.has(vnum) - -# 获取升阶配方详情 -func get_recipe(vnum: int) -> Dictionary: - return EPIC_RECIPES.get(vnum, {}) - -# 执行史诗装备升阶转换锻造 -func do_epic_refine( - target_slot: int, - inventory: Array, - player_data: Dictionary -) -> Dictionary: - if target_slot < 0 or target_slot >= inventory.size(): - return {"ok": false, "reason": "INVALID_SLOT", "msg": "无效的装备栏位!"} - - var base_item = inventory[target_slot] - if base_item == null: - return {"ok": false, "reason": "EMPTY_SLOT", "msg": "该格子上没有装备!"} - - # 安全锁拦截 - if base_item.get("is_locked", false): - return {"ok": false, "reason": "ITEM_LOCKED", "msg": "已加锁的装备无法进行史诗锻造!"} - - var base_vnum: int = int(base_item.get("vnum", 0)) - if not EPIC_RECIPES.has(base_vnum): - return {"ok": false, "reason": "NOT_AN_EPIC_BASE_ITEM", "msg": "该物品无法进行修元平史诗升阶!"} - - var recipe: Dictionary = EPIC_RECIPES[base_vnum] - - # 1. 检查金币 (Yang) - var current_yang: int = int(player_data.get("gold", 0)) - if current_yang < REQUIRED_YANG: - return { - "ok": false, - "reason": "INSUFFICIENT_YANG", - "required_yang": REQUIRED_YANG, - "current_yang": current_yang, - "msg": "史诗锻造需要 200,000 金币!你的金币不足。" - } - - # 2. 统计并收集龙鳞与龙爪 - var scale_slots: Array = [] - var claw_slots: Array = [] - var total_scales: int = 0 - var total_claws: int = 0 - - for i in range(inventory.size()): - var item = inventory[i] - if item == null: - continue - var v: int = int(item.get("vnum", 0)) - var cnt: int = int(item.get("count", 1)) - if v == ITEM_DRAGON_SCALE: - total_scales += cnt - scale_slots.append({"slot": i, "count": cnt}) - elif v == ITEM_DRAGON_CLAW: - total_claws += cnt - claw_slots.append({"slot": i, "count": cnt}) - - if total_scales < REQUIRED_SCALE_COUNT: - return { - "ok": false, - "reason": "MISSING_DRAGON_SCALES", - "required": REQUIRED_SCALE_COUNT, - "current": total_scales, - "msg": "史诗锻造需要 3 个龙鳞!当前只有 %d 个。" % total_scales - } - - if total_claws < REQUIRED_CLAW_COUNT: - return { - "ok": false, - "reason": "MISSING_DRAGON_CLAWS", - "required": REQUIRED_CLAW_COUNT, - "current": total_claws, - "msg": "史诗锻造需要 4 个龙爪!当前只有 %d 个。" % total_claws - } - - # 3. 扣除金币 - player_data["gold"] = current_yang - REQUIRED_YANG - - # 4. 扣除材料 (龙鳞 3, 龙爪 4) - _consume_materials(inventory, scale_slots, REQUIRED_SCALE_COUNT) - _consume_materials(inventory, claw_slots, REQUIRED_CLAW_COUNT) - - # 5. 生成新史诗装备,1:1 完整继承 5 条附加属性与已镶嵌灵石! - var new_vnum: int = recipe["target_vnum"] - var inherited_attrs: Array = [] - if base_item.has("attributes"): - for attr in base_item["attributes"]: - inherited_attrs.append(attr.duplicate()) - - var inherited_sockets: Array = [] - if base_item.has("sockets"): - for sock in base_item["sockets"]: - inherited_sockets.append(sock) - - var epic_item: Dictionary = { - "vnum": new_vnum, - "name": recipe["name"], - "count": 1, - "attributes": inherited_attrs, - "sockets": inherited_sockets, - "socket_count": base_item.get("socket_count", 3), - "is_locked": false - } - - # 替换背包中原位置的装备 - inventory[target_slot] = epic_item - - epic_refine_succeeded.emit(base_vnum, new_vnum, epic_item) - - return { - "ok": true, - "old_vnum": base_vnum, - "new_vnum": new_vnum, - "item": epic_item, - "msg": "恭喜!修元平大师成功将装备升阶锻造为【%s】!" % recipe["name"] - } - -# 辅助函数:按需从多个格子中扣除材料数量 -func _consume_materials(inventory: Array, slots_info: Array, count_needed: int) -> void: - var needed := count_needed - for info in slots_info: - var s: int = info["slot"] - var have: int = info["count"] - if have <= needed: - needed -= have - inventory[s] = null - else: - inventory[s]["count"] = have - needed - needed = 0 - if needed <= 0: - break diff --git a/project/epic_refine_system.gd.uid b/project/epic_refine_system.gd.uid deleted file mode 100644 index 18ed5a73..00000000 --- a/project/epic_refine_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cg7egxv7schkr diff --git a/project/equipment_inspect_system.gd b/project/equipment_inspect_system.gd deleted file mode 100644 index ce4b559c..00000000 --- a/project/equipment_inspect_system.gd +++ /dev/null @@ -1,134 +0,0 @@ -# equipment_inspect_system.gd —— 40250 装备透视检视与 GM 监察系统 1:1 -# 严格对照: -# Client/Eternexus/root/uiequipmentdialog.py (EquipmentDialog) -# ClientVS22/source/UserInterface/PythonPlayer.cpp -# metin2/src/server/game/src/cmd_gm.cpp (do_view_equip) -extends RefCounted - -# 官方装备槽位常量定义 (1:1 ClientVS22/source/UserInterface/GameType.h) -const WEAR_BODY := 0 # 铠甲 -const WEAR_HEAD := 1 # 头盔 -const WEAR_FOOTS := 2 # 鞋子 -const WEAR_WRIST := 3 # 手镯 -const WEAR_WEAPON := 4 # 武器 -const WEAR_NECK := 5 # 项链 -const WEAR_EAR := 6 # 耳环 -const WEAR_SHIELD := 10 # 盾牌 -const WEAR_COSTUME_BODY := 19 # 时装身体 -const WEAR_COSTUME_HAIR := 20 # 时装发型 -const WEAR_COSTUME_WEAPON := 21 # 时装武器 - -const SLOT_NAMES := { - WEAR_BODY: "铠甲", - WEAR_HEAD: "头盔", - WEAR_FOOTS: "鞋子", - WEAR_WRIST: "手镯", - WEAR_WEAPON: "武器", - WEAR_NECK: "项链", - WEAR_EAR: "耳环", - WEAR_SHIELD: "盾牌", - WEAR_COSTUME_BODY: "时装身体", - WEAR_COSTUME_HAIR: "时装发型", - WEAR_COSTUME_WEAPON: "时装武器", -} - -# 窗口状态 -var is_open: bool = false -var inspected_vid: int = 0 -var inspected_name: String = "" -var equipment_slots: Dictionary = {} # slot_idx -> item_data Dictionary - -func _init() -> void: - is_open = false - inspected_vid = 0 - inspected_name = "" - equipment_slots.clear() - -# 1. 打开检视窗口 (1:1 EquipmentDialog.Open) -# caller_is_gm: GM 可强制穿透隐私限制 -func open_dialog( - target_vid: int, - target_name: String, - target_equip_dict: Dictionary, - allow_inspect: bool = true, - caller_is_gm: bool = false -) -> Dictionary: - var res := { "success": false, "error_code": "OK", "message": "" } - - if not allow_inspect and not caller_is_gm: - res["error_code"] = "INSPECT_FORBIDDEN" - res["message"] = "目标玩家设置了隐私保护,禁止查看装备。" - return res - - inspected_vid = target_vid - inspected_name = target_name - equipment_slots.clear() - - # 载入目标玩家装备数据 - for slot_key in target_equip_dict: - var slot_idx: int = int(slot_key) - var it: Dictionary = target_equip_dict[slot_key] - equipment_slots[slot_idx] = _normalize_item_data(it) - - is_open = true - res["success"] = true - res["message"] = "成功载入 %s 的装备信息。" % target_name - return res - -# 2. 关闭检视窗口 (EquipmentDialog.Close) -func close_dialog() -> void: - is_open = false - inspected_vid = 0 - inspected_name = "" - equipment_slots.clear() - -# 3. 规范化装备项数据 (确保含有 3 宝石孔与 7 词条结构) -func _normalize_item_data(it: Dictionary) -> Dictionary: - var item_copy := it.duplicate(true) - var sockets: Array = item_copy.get("sockets", [0, 0, 0]) - while sockets.size() < 3: - sockets.append(0) - item_copy["sockets"] = sockets - - var attrs: Array = item_copy.get("attributes", []) - item_copy["attributes"] = attrs - return item_copy - -# 4. 获取指定槽位装备 -func get_slot_item(slot_idx: int) -> Dictionary: - if not is_open: - return {} - return equipment_slots.get(slot_idx, {}) - -# 5. 悬停生成 Tooltip 详情 (1:1 EquipmentDialog.OverInItem) -func build_slot_tooltip(slot_idx: int) -> String: - if not is_open: - return "" - - if not equipment_slots.has(slot_idx): - var slot_name: String = SLOT_NAMES.get(slot_idx, "未知槽位") - return "[%s] 未佩戴装备" % slot_name - - var it: Dictionary = equipment_slots[slot_idx] - var lines: Array = [] - var title_name: String = it.get("name", "Unknown Item") - var vnum: int = it.get("vnum", 0) - - lines.append("=== %s (Vnum: %d) ===" % [title_name, vnum]) - lines.append("部位: %s" % SLOT_NAMES.get(slot_idx, "特殊部位")) - - # 宝石孔位 - var sockets: Array = it.get("sockets", []) - for i in range(sockets.size()): - var s: int = sockets[i] - if s > 0: - lines.append("孔位 %d: 镶嵌宝石 [Vnum %d]" % [i + 1, s]) - elif s == 1: - lines.append("孔位 %d: 空孔" % [i + 1]) - - # 附加属性 (1~7 词条) - var attrs: Array = it.get("attributes", []) - for a in attrs: - lines.append("附加属性: 属性 %d +%d" % [a.get("type", 0), a.get("value", 0)]) - - return ("\n").join(lines) diff --git a/project/equipment_inspect_system.gd.uid b/project/equipment_inspect_system.gd.uid deleted file mode 100644 index 70be868f..00000000 --- a/project/equipment_inspect_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://gt0l8bv2agu3 diff --git a/project/event_manager_system.gd b/project/event_manager_system.gd deleted file mode 100644 index b5374790..00000000 --- a/project/event_manager_system.gd +++ /dev/null @@ -1,171 +0,0 @@ -# event_manager_system.gd —— Metin2 40250 单人模式全服掉落活动与倍率调度器 1:1 -# 对照 40250 服务端 cmd_gm.cpp (do_priv_empire), priv_manager.cpp, event.cpp, Items 50011, 50037 -class_name EventManagerSystem -extends RefCounted - -signal rate_changed(rate_type: String, multiplier: float) -signal event_state_changed(event_name: String, is_active: bool) -signal event_box_dropped(box_vnum: int, box_name: String) - -const ITEM_MOONLIGHT_BOX := 50011 # 月光宝盒 -const ITEM_HEXAGONAL_BOX := 50037 # 六角宝盒 - -# 宝盒奖励奖池 (40250 官方 special_item_group.txt 1:1) -const EVENT_BOX_LOOT: Dictionary = { - 50011: [ - {"vnum": 25040, "name": "祝福卷轴", "count": 1, "weight": 30}, - {"vnum": 71084, "name": "高级附魔石", "count": 1, "weight": 25}, - {"vnum": 71085, "name": "高级洗练石", "count": 1, "weight": 25}, - {"vnum": 70024, "name": "祝福大理石", "count": 1, "weight": 10}, - {"vnum": 72723, "name": "月之灵药(特)", "count": 1, "weight": 10} - ], - 50037: [ - {"vnum": 72001, "name": "经验之戒", "count": 1, "weight": 20}, - {"vnum": 72002, "name": "盗贼手套", "count": 1, "weight": 20}, - {"vnum": 27102, "name": "绿药水(大)", "count": 5, "weight": 30}, - {"vnum": 27105, "name": "紫药水(大)", "count": 5, "weight": 30} - ] -} - -var exp_rate: float = 1.0 -var drop_rate: float = 1.0 -var gold_rate: float = 1.0 - -var is_moonlight_active: bool = false -var is_hexagonal_active: bool = false - -# 设置倍率 (exp, drop, gold) -func set_rate(rate_type: String, multiplier: float) -> Dictionary: - if multiplier < 1.0 or multiplier > 5.0: - return {"ok": false, "reason": "INVALID_MULTIPLIER", "msg": "倍率必须在 1.0 到 5.0 之间!"} - - match rate_type: - "exp": - exp_rate = multiplier - "drop": - drop_rate = multiplier - "gold": - gold_rate = multiplier - _: - return {"ok": false, "reason": "INVALID_RATE_TYPE", "msg": "未知的倍率类型!"} - - rate_changed.emit(rate_type, multiplier) - - return { - "ok": true, - "rate_type": rate_type, - "multiplier": multiplier, - "msg": "全服【%s】倍率已设置为 %.1fx!" % [rate_type.to_upper(), multiplier] - } - -# 开关经典限时掉落活动 -func toggle_event(event_name: String, enable: bool) -> Dictionary: - match event_name: - "moonlight": - is_moonlight_active = enable - "hexagonal": - is_hexagonal_active = enable - _: - return {"ok": false, "reason": "INVALID_EVENT_NAME", "msg": "未知的活动名称!"} - - event_state_changed.emit(event_name, enable) - - var state_str := "开启" if enable else "关闭" - var ev_desc := "月光宝盒掉落狂欢" if event_name == "moonlight" else "六角宝盒冲级庆典" - - return { - "ok": true, - "event_name": event_name, - "is_active": enable, - "msg": "【%s】活动已%s!" % [ev_desc, state_str] - } - -# 怪物击杀时判定额外活动宝盒掉落 -func check_event_drops(mob_level: int, player_level: int, forced_roll: float = -1.0) -> Array: - var drops: Array = [] - - # 等级差在 15 级以内才掉落活动宝盒 - if absi(mob_level - player_level) > 15: - return drops - - # 1. 月光宝盒判定 (基础概率 10% * drop_rate) - if is_moonlight_active: - var chance := 0.10 * drop_rate - var roll := forced_roll if forced_roll >= 0.0 else randf() - if roll <= chance: - drops.append({"vnum": ITEM_MOONLIGHT_BOX, "name": "月光宝盒", "count": 1}) - event_box_dropped.emit(ITEM_MOONLIGHT_BOX, "月光宝盒") - - # 2. 六角宝盒判定 (基础概率 15% * drop_rate) - if is_hexagonal_active: - var chance := 0.15 * drop_rate - var roll := forced_roll if forced_roll >= 0.0 else randf() - if roll <= chance: - drops.append({"vnum": ITEM_HEXAGONAL_BOX, "name": "六角宝盒", "count": 1}) - event_box_dropped.emit(ITEM_HEXAGONAL_BOX, "六角宝盒") - - return drops - -# 计算经倍率加成后的经验与金币 -func calculate_exp(base_exp: int) -> int: - return int(round(base_exp * exp_rate)) - -func calculate_gold(base_gold: int) -> int: - return int(round(base_gold * gold_rate)) - -# 打开活动宝盒 -func open_event_box(slot_idx: int, inventory: Array) -> Dictionary: - if slot_idx < 0 or slot_idx >= inventory.size(): - return {"ok": false, "reason": "INVALID_SLOT", "msg": "无效的背包栏位!"} - - var box = inventory[slot_idx] - if box == null: - return {"ok": false, "reason": "EMPTY_SLOT", "msg": "该格子上没有物品!"} - - var box_vnum: int = int(box.get("vnum", 0)) - if not EVENT_BOX_LOOT.has(box_vnum): - return {"ok": false, "reason": "NOT_AN_EVENT_BOX", "msg": "这不是活动宝盒!"} - - var pool: Array = EVENT_BOX_LOOT[box_vnum] - - # 权重抽奖 - var total_weight := 0 - for item in pool: - total_weight += int(item.get("weight", 1)) - - var roll := randi_range(1, total_weight) - var accumulated := 0 - var reward: Dictionary = {} - for item in pool: - accumulated += int(item.get("weight", 1)) - if roll <= accumulated: - reward = item.duplicate() - break - - if reward.is_empty(): - reward = pool[0].duplicate() - - # 扣除宝盒,存入奖励 - var cnt: int = int(box.get("count", 1)) - if cnt <= 1: - inventory[slot_idx] = null - else: - box["count"] = cnt - 1 - - var stored_slot := -1 - for i in range(inventory.size()): - if inventory[i] == null: - inventory[i] = { - "vnum": reward["vnum"], - "name": reward["name"], - "count": reward.get("count", 1) - } - stored_slot = i - break - - return { - "ok": true, - "reward": reward, - "stored_slot": stored_slot, - "msg": "打开活动宝盒,获得了【%s x%d】!" % [reward["name"], reward.get("count", 1)] - } diff --git a/project/event_manager_system.gd.uid b/project/event_manager_system.gd.uid deleted file mode 100644 index e7d0b8b6..00000000 --- a/project/event_manager_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bvf0ul7tdn6o5 diff --git a/project/fish_clam_roast_system.gd b/project/fish_clam_roast_system.gd deleted file mode 100644 index 8b83eb8c..00000000 --- a/project/fish_clam_roast_system.gd +++ /dev/null @@ -1,432 +0,0 @@ -# fish_clam_roast_system.gd —— Metin2 40250 官方珍珠蚌开珠与活鱼宰杀/篝火炙烤盛宴系统 1:1 -# 100% 对照 40250 服务端 fishing.cpp, char_item.cpp (ITEM_FISH, 27987, USE_ABILITY_UP, USE_INVISIBILITY), item_proto.txt -class_name FishClamRoastSystem -extends RefCounted - -signal fish_gutted(result_type: String, item_vnum: int, item_name: String) -signal clam_opened(result_type: String, item_vnum: int, item_name: String) -signal fish_grilled(raw_vnum: int, cooked_vnum: int, count: int) -signal cooked_fish_consumed(fish_vnum: int, effect_type: String, value: int, duration: float) -signal campfire_spawned(pos: Vector2, duration: float) - -# 官方核心 Vnum 常量 -const VNUM_CLAM := 27987 # 珍珠蚌 (Clam / Istiridye) -const VNUM_STONE_PIECE := 27990 # 碎石 (Stone Piece) -const VNUM_WHITE_PEARL := 27992 # 白珍珠 (White Pearl) -const VNUM_BLUE_PEARL := 27993 # 蓝珍珠 (Blue Pearl) -const VNUM_BLOOD_PEARL := 27994 # 血红珍珠 (Blood Pearl) -const VNUM_FISH_BONE := 27995 # 鱼骨 (Fishbone) -const VNUM_EARTHWORM := 27801 # 蚯蚓/饵料 (Earthworm) -const VNUM_CAMPFIRE_ITEM := 27600 # 篝火道具 -const MOB_CAMPFIRE := 12000 # 篝火 NPC/Mob - -# 40250 官方鱼类资料表 (alive_vnum, dead_vnum, cooked_vnum, buff/effect) -const FISH_DATA: Dictionary = { - 27803: { - "name": "鲈鱼 (Sudak)", - "alive_vnum": 27803, - "dead_vnum": 27833, - "cooked_vnum": 27863, - "effect": "instant_hp", - "value": 180, - "duration": 0.0, - "desc": "立即恢复 180 点生命值" - }, - 27804: { - "name": "鲱鱼 (Ringa)", - "alive_vnum": 27804, - "dead_vnum": 27834, - "cooked_vnum": 27864, - "effect": "instant_sp", - "value": 150, - "duration": 0.0, - "desc": "立即恢复 150 点魔法值" - }, - 27806: { - "name": "鲤鱼 (Sazan)", - "alive_vnum": 27806, - "dead_vnum": 27836, - "cooked_vnum": 27866, - "effect": "move_speed", - "value": 20, - "duration": 600.0, - "desc": "移动速度 +20 持续 10 分钟" - }, - 27807: { - "name": "三文鱼/鲑鱼 (Som)", - "alive_vnum": 27807, - "dead_vnum": 27837, - "cooked_vnum": 27867, - "effect": "instant_sp", - "value": 300, - "duration": 0.0, - "desc": "立即恢复 300 点魔法值" - }, - 27808: { - "name": "草鱼 (Ot Sazanı)", - "alive_vnum": 27808, - "dead_vnum": 27838, - "cooked_vnum": 27868, - "effect": "attack_speed", - "value": 20, - "duration": 600.0, - "desc": "攻击速度 +20 持续 10 分钟" - }, - 27810: { - "name": "水针鱼 (Zargana)", - "alive_vnum": 27810, - "dead_vnum": 27840, - "cooked_vnum": 27870, - "effect": "strength", - "value": 10, - "duration": 600.0, - "desc": "力量 (STR) +10 持续 10 分钟" - }, - 27813: { - "name": "凤尾鱼 (Hamsi)", - "alive_vnum": 27813, - "dead_vnum": 27843, - "cooked_vnum": 27873, - "effect": "dexterity", - "value": 10, - "duration": 600.0, - "desc": "敏捷 (DEX) +10 持续 10 分钟" - }, - 27814: { - "name": "海鲈鱼 (Levrek)", - "alive_vnum": 27814, - "dead_vnum": 27844, - "cooked_vnum": 27874, - "effect": "cure_poison", - "value": 1, - "duration": 0.0, - "desc": "祛除体内的一切猛毒状态" - }, - 27817: { - "name": "泥鳅 (Çopra)", - "alive_vnum": 27817, - "dead_vnum": 27847, - "cooked_vnum": 27877, - "effect": "invisibility", - "value": 1, - "duration": 300.0, - "desc": "进入幽冥迷雾隐身状态 5 分钟" - } -} - -# 逆向索引 -var _dead_to_cooked: Dictionary = {} -var _alive_to_cooked: Dictionary = {} -var _cooked_to_data: Dictionary = {} - -# 当前激活的烤鱼增益状态列表 -var _active_buffs: Dictionary = {} # effect_type -> {"value": int, "duration": float} - -# 当前地图是否存在有效篝火 -var is_near_active_campfire: bool = false -var campfire_time_left: float = 0.0 - -func _init() -> void: - for k in FISH_DATA.keys(): - var data = FISH_DATA[k] - _alive_to_cooked[data["alive_vnum"]] = data - _dead_to_cooked[data["dead_vnum"]] = data - _cooked_to_data[data["cooked_vnum"]] = data - -# ========================================== -# 1. 活鱼宰杀/使用 (fishing.cpp: UseFish) -# ========================================== -func gut_live_fish(inventory: Array, slot_index: int, forced_roll: int = -1) -> Dictionary: - if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null: - return {"ok": false, "reason": "EMPTY_SLOT", "msg": "该格位没有活鱼!"} - - var item = inventory[slot_index] - var vnum = int(item.get("vnum", 0)) - if not _alive_to_cooked.has(vnum): - return {"ok": false, "reason": "NOT_ALIVE_FISH", "msg": "此物品不是可以剖杀的鲜活鱼类!"} - - var fish = _alive_to_cooked[vnum] - # 扣减活鱼数量 - _consume_item_one(inventory, slot_index) - - # 40250 原版随机概率 1~10000: - # r >= 4001 (60%): 产出死鱼 (dead_vnum) - # r >= 2001 (20%): 产出鱼骨 (FISH_BONE_VNUM 27995) - # r < 2001 (20%): 肚中有物 (蚌壳 27987、饵料 27801 或失手滑入水中) - var r = forced_roll if forced_roll > 0 else randi_range(1, 10000) - - if r >= 4001: - _give_or_stack_item(inventory, fish["dead_vnum"], 1) - fish_gutted.emit("dead_fish", fish["dead_vnum"], "死" + fish["name"]) - return { - "ok": true, - "result": "dead_fish", - "vnum": fish["dead_vnum"], - "name": "死" + fish["name"], - "msg": "你熟练地宰杀了活鱼,获得了可用于烤制的死鱼。" - } - elif r >= 2001: - _give_or_stack_item(inventory, VNUM_FISH_BONE, 1) - fish_gutted.emit("fish_bone", VNUM_FISH_BONE, "鱼骨") - return { - "ok": true, - "result": "fish_bone", - "vnum": VNUM_FISH_BONE, - "name": "鱼骨", - "msg": "活鱼体内只剩下一截晶莹剔透的坚硬鱼骨。" - } - else: - # r < 2001 - if r <= 1000: - # 珍稀产出:珍珠蚌 (Clam 27987) - _give_or_stack_item(inventory, VNUM_CLAM, 1) - fish_gutted.emit("clam", VNUM_CLAM, "珍珠蚌") - return { - "ok": true, - "result": "clam", - "vnum": VNUM_CLAM, - "name": "珍珠蚌", - "msg": "难以置信!你在鱼肚中发现了一枚紧闭的神秘【珍珠蚌】!" - } - elif r <= 1500: - _give_or_stack_item(inventory, VNUM_EARTHWORM, 1) - fish_gutted.emit("earthworm", VNUM_EARTHWORM, "蚯蚓") - return { - "ok": true, - "result": "earthworm", - "vnum": VNUM_EARTHWORM, - "name": "蚯蚓", - "msg": "鱼肚里还残留着一条未消化的蚯蚓饵料。" - } - else: - # 滑入水中/空 - fish_gutted.emit("escape", 0, "溜走") - return { - "ok": true, - "result": "escape", - "vnum": 0, - "name": "", - "msg": "活鱼猛烈扑腾,从你的指缝中滑落回了深水之中!" - } - -# ========================================== -# 2. 珍珠蚌开珠 (char_item.cpp:2965 Case 27987) -# ========================================== -func open_clam( - inventory: Array, - slot_index: int, - forced_branch_roll: int = -1, - forced_pearl_roll: int = -1 -) -> Dictionary: - if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null: - return {"ok": false, "reason": "EMPTY_SLOT", "msg": "该格位没有物品!"} - - var item = inventory[slot_index] - var vnum = int(item.get("vnum", 0)) - if vnum != VNUM_CLAM: - return {"ok": false, "reason": "NOT_A_CLAM", "msg": "该物品不是珍珠蚌!"} - - # 消耗 1 枚珍珠蚌 - _consume_item_one(inventory, slot_index) - - # 40250 官方第一轮分支 (1~100): - # r <= 50 (50%): 碎石 (27990) - # r > 50 (50%): 判定珍珠概率表 prob_table {80, 90, 97} - var r = forced_branch_roll if forced_branch_roll > 0 else randi_range(1, 100) - - if r <= 50: - _give_or_stack_item(inventory, VNUM_STONE_PIECE, 1) - clam_opened.emit("stone", VNUM_STONE_PIECE, "碎石") - return { - "ok": true, - "result": "stone", - "vnum": VNUM_STONE_PIECE, - "name": "碎石", - "msg": "珍珠蚌打开了,但里面只包裹着一块毫无价值的普通碎石。" - } - else: - # 珍珠判定表 (1~100) - # roll <= 80 (80%): 空蚌壳 (什么都没有) - # 81 <= roll <= 90 (10%): 白珍珠 (27992) - # 91 <= roll <= 97 (7%): 蓝珍珠 (27993) - # roll >= 98 (3%): 血红珍珠 (27994) - var p_roll = forced_pearl_roll if forced_pearl_roll > 0 else randi_range(1, 100) - if p_roll <= 80: - clam_opened.emit("empty", 0, "空蚌") - return { - "ok": true, - "result": "empty", - "vnum": 0, - "name": "", - "msg": "蚌壳碎裂开来,里面空空如也,什么也没有留下。" - } - elif p_roll <= 90: - _give_or_stack_item(inventory, VNUM_WHITE_PEARL, 1) - clam_opened.emit("white_pearl", VNUM_WHITE_PEARL, "白珍珠") - return { - "ok": true, - "result": "white_pearl", - "vnum": VNUM_WHITE_PEARL, - "name": "白珍珠", - "msg": "璀璨温润!你从蚌肉中剥出了一颗纯净的【白珍珠】!" - } - elif p_roll <= 97: - _give_or_stack_item(inventory, VNUM_BLUE_PEARL, 1) - clam_opened.emit("blue_pearl", VNUM_BLUE_PEARL, "蓝珍珠") - return { - "ok": true, - "result": "blue_pearl", - "vnum": VNUM_BLUE_PEARL, - "name": "蓝珍珠", - "msg": "幽蓝如海!你收获了一颗弥足珍贵的【蓝珍珠】!" - } - else: - _give_or_stack_item(inventory, VNUM_BLOOD_PEARL, 1) - clam_opened.emit("blood_pearl", VNUM_BLOOD_PEARL, "血红珍珠") - return { - "ok": true, - "result": "blood_pearl", - "vnum": VNUM_BLOOD_PEARL, - "name": "血红珍珠", - "msg": "旷世奇珍!蚌心处赫然闪烁着血煞夺目的极品【血红珍珠】!" - } - -# ========================================== -# 3. 篝火炙烤 (char_item.cpp:6880, fishing.cpp: Grill) -# ========================================== -func place_campfire(duration: float = 600.0) -> void: - is_near_active_campfire = true - campfire_time_left = duration - campfire_spawned.emit(Vector2.ZERO, duration) - -func grill_fish(inventory: Array, slot_index: int, near_campfire_override: bool = false) -> Dictionary: - var can_grill = is_near_active_campfire or near_campfire_override - if not can_grill: - return {"ok": false, "reason": "NO_CAMPFIRE", "msg": "烤鱼必须在点燃的营火 (篝火) 旁进行!"} - - if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null: - return {"ok": false, "reason": "EMPTY_SLOT", "msg": "该格位没有鱼类!"} - - var item = inventory[slot_index] - var vnum = int(item.get("vnum", 0)) - var count = int(item.get("count", 1)) - - var fish_data = null - if _dead_to_cooked.has(vnum): - fish_data = _dead_to_cooked[vnum] - elif _alive_to_cooked.has(vnum): - fish_data = _alive_to_cooked[vnum] - else: - return {"ok": false, "reason": "NOT_GRILLABLE", "msg": "此物品无法用于篝火炙烤!"} - - var cooked_vnum = int(fish_data["cooked_vnum"]) - # 40250 item->SetCount(0); AutoGiveItem(grill_vnum, count); - inventory[slot_index] = null - _give_or_stack_item(inventory, cooked_vnum, count) - - fish_grilled.emit(vnum, cooked_vnum, count) - return { - "ok": true, - "cooked_vnum": cooked_vnum, - "count": count, - "name": "烤" + fish_data["name"], - "msg": "营火噼啪作响,鱼肉散发出浓郁扑鼻的焦香!你获得了 %d 条【烤%s】。" % [count, fish_data["name"]] - } - -# ========================================== -# 4. 食用熟鱼获得增益 (char_item.cpp: USE_POTION / USE_ABILITY_UP) -# ========================================== -func eat_cooked_fish(inventory: Array, slot_index: int) -> Dictionary: - if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null: - return {"ok": false, "reason": "EMPTY_SLOT", "msg": "该格位没有熟鱼!"} - - var item = inventory[slot_index] - var vnum = int(item.get("vnum", 0)) - if not _cooked_to_data.has(vnum): - return {"ok": false, "reason": "NOT_COOKED_FISH", "msg": "此物品不是烤制完成的熟鱼!"} - - var data = _cooked_to_data[vnum] - _consume_item_one(inventory, slot_index) - - var effect = str(data["effect"]) - var val = int(data["value"]) - var dur = float(data["duration"]) - - if dur > 0.0: - _active_buffs[effect] = { - "value": val, - "time_remaining": dur, - "name": "烤" + data["name"] - } - - cooked_fish_consumed.emit(vnum, effect, val, dur) - return { - "ok": true, - "effect": effect, - "value": val, - "duration": dur, - "desc": data["desc"], - "msg": "你大快朵颐地吃下了香喷喷的烤鱼,%s!" % data["desc"] - } - -func update(delta: float) -> void: - if campfire_time_left > 0.0: - campfire_time_left -= delta - if campfire_time_left <= 0.0: - campfire_time_left = 0.0 - is_near_active_campfire = false - - # 更新 BUFF 持续倒计时 - var to_remove: Array = [] - for eff in _active_buffs.keys(): - var b = _active_buffs[eff] - b["time_remaining"] -= delta - if b["time_remaining"] <= 0.0: - to_remove.append(eff) - - for eff in to_remove: - _active_buffs.erase(eff) - -func get_active_buffs() -> Dictionary: - return _active_buffs - -func clear_buffs() -> void: - _active_buffs.clear() - -# ========================================== -# 辅助函数: 背包堆叠与消耗 -# ========================================== -func _consume_item_one(inventory: Array, slot: int) -> void: - var item = inventory[slot] - var count = int(item.get("count", 1)) - if count > 1: - item["count"] = count - 1 - else: - inventory[slot] = null - -func _give_or_stack_item(inventory: Array, vnum: int, count: int) -> void: - # 尝试堆叠 - for it in inventory: - if it != null and int(it.get("vnum", 0)) == vnum: - it["count"] = int(it.get("count", 1)) + count - return - # 寻找首个空槽 - for i in range(inventory.size()): - if inventory[i] == null: - inventory[i] = {"vnum": vnum, "count": count} - return - # 若无空槽则追加 - inventory.append({"vnum": vnum, "count": count}) - -# 序列化与反序列化 -func serialize() -> Dictionary: - return { - "active_buffs": _active_buffs.duplicate(true), - "campfire_time_left": campfire_time_left, - "is_near_active_campfire": is_near_active_campfire - } - -func deserialize(data: Dictionary) -> void: - _active_buffs = data.get("active_buffs", {}).duplicate(true) - campfire_time_left = float(data.get("campfire_time_left", 0.0)) - is_near_active_campfire = bool(data.get("is_near_active_campfire", false)) diff --git a/project/fish_clam_roast_system.gd.uid b/project/fish_clam_roast_system.gd.uid deleted file mode 100644 index 447cce91..00000000 --- a/project/fish_clam_roast_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d1lso6wx22xr2 diff --git a/project/fishing_cooking_camp_system.gd b/project/fishing_cooking_camp_system.gd deleted file mode 100644 index ddf95cb7..00000000 --- a/project/fishing_cooking_camp_system.gd +++ /dev/null @@ -1,214 +0,0 @@ -# fishing_cooking_camp_system.gd —— Metin2 40250 野外钓鱼烹饪与生火露营增益系统 1:1 -# 对照 40250 服务端 fishing.cpp, char_item.cpp, affect.h -class_name FishingCookingCampSystem -extends RefCounted - -signal campfire_lit(pos: Vector3, duration: float) -signal campfire_extinguished() -signal fish_cooked(raw_vnum: int, cooked_vnum: int, cooked_name: String) -signal food_buff_applied(buff_id: String, buff_name: String, duration: float) -signal food_buff_expired(buff_id: String) - -const CAMPFIRE_KIT_VNUM := 27600 # 营火工具 (Campfire) -const CAMPFIRE_DURATION := 300.0 # 营火持续 5 分钟 -const FOOD_BUFF_DURATION := 1800.0 # 食用增益 30 分钟 (1800 秒) - -# 鱼类与烹饪食谱 -const RECIPES: Dictionary = { - 27802: { # 生鲤鱼 -> 烤鲤鱼 - "cooked_vnum": 27832, - "cooked_name": "烤鲤鱼", - "buff_id": "buff_grilled_carp", - "stat_key": "max_hp", - "stat_val": 1000, - "desc": "最大生命上限 +1000,持续 30 分钟" - }, - 27803: { # 生草鱼 -> 烤草鱼 - "cooked_vnum": 27833, - "cooked_name": "烤草鱼", - "buff_id": "buff_grilled_grass_carp", - "stat_key": "move_speed", - "stat_val": 15, - "desc": "移动速度 +15,持续 30 分钟" - }, - 27815: { # 生红鳟鱼 -> 烤红鳟鱼 - "cooked_vnum": 27845, - "cooked_name": "烤红鳟鱼", - "buff_id": "buff_grilled_trout", - "stat_key": "atk", - "stat_val": 50, - "desc": "攻击力 +50,持续 30 分钟" - } -} - -# 烤鱼物品映射 -const COOKED_FOODS: Dictionary = { - 27832: {"buff_id": "buff_grilled_carp", "name": "烤鲤鱼", "stat_key": "max_hp", "stat_val": 1000}, - 27833: {"buff_id": "buff_grilled_grass_carp", "name": "烤草鱼", "stat_key": "move_speed", "stat_val": 15}, - 27845: {"buff_id": "buff_grilled_trout", "name": "烤红鳟鱼", "stat_key": "atk", "stat_val": 50} -} - -var is_campfire_active: bool = false -var campfire_timer: float = 0.0 -var campfire_pos: Vector3 = Vector3.ZERO - -# 活跃中的食物 Buff: buff_id -> { "remaining": float, "stat_key": String, "stat_val": int } -var active_buffs: Dictionary = {} - -# 点燃野外营火 -func light_campfire(pos: Vector3, inventory: Array) -> Dictionary: - var kit_slot = -1 - for i in range(inventory.size()): - if inventory[i] != null and int(inventory[i].get("vnum", 0)) == CAMPFIRE_KIT_VNUM: - kit_slot = i - break - - if kit_slot == -1: - return {"ok": false, "reason": "NO_CAMPFIRE_KIT", "msg": "背包中没有【营火工具】(27600)!"} - - # 消耗 1 个营火工具 - var item = inventory[kit_slot] - var cnt = int(item.get("count", 1)) - if cnt > 1: - item["count"] = cnt - 1 - else: - inventory[kit_slot] = null - - is_campfire_active = true - campfire_timer = CAMPFIRE_DURATION - campfire_pos = pos - - campfire_lit.emit(pos, CAMPFIRE_DURATION) - - return { - "ok": true, - "pos": pos, - "duration": CAMPFIRE_DURATION, - "msg": "营火已点燃!散发着温暖火光,可在此烧烤烹饪渔获!" - } - -# 在营火旁烹饪烤鱼 -func cook_fish(slot: int, inventory: Array) -> Dictionary: - if not is_campfire_active: - return {"ok": false, "reason": "NO_ACTIVE_CAMPFIRE", "msg": "周围没有生起的营火,无法烹饪!"} - - if slot < 0 or slot >= inventory.size() or inventory[slot] == null: - return {"ok": false, "reason": "INVALID_SLOT"} - - var raw_item = inventory[slot] - var raw_vnum = int(raw_item.get("vnum", 0)) - - if not RECIPES.has(raw_vnum): - return {"ok": false, "reason": "NOT_COOKABLE", "msg": "此物品无法在营火上烹饪!"} - - var r = RECIPES[raw_vnum] - # 消耗 1 条生鱼 - var cnt = int(raw_item.get("count", 1)) - if cnt > 1: - raw_item["count"] = cnt - 1 - else: - inventory[slot] = null - - # 入包烤鱼 - var cooked_vnum: int = r["cooked_vnum"] - var cooked_name: String = r["cooked_name"] - - # 寻找可叠加的空格或同类 - var placed := false - for i in range(inventory.size()): - if inventory[i] != null and int(inventory[i].get("vnum", 0)) == cooked_vnum: - inventory[i]["count"] = int(inventory[i].get("count", 1)) + 1 - placed = true - break - - if not placed: - for i in range(inventory.size()): - if inventory[i] == null: - inventory[i] = { - "vnum": cooked_vnum, - "name": cooked_name, - "count": 1 - } - placed = true - break - - fish_cooked.emit(raw_vnum, cooked_vnum, cooked_name) - - return { - "ok": true, - "cooked_name": cooked_name, - "desc": r["desc"], - "msg": "滋滋作响!成功烤制出色香味俱全的【%s】!" % cooked_name - } - -# 食用烤鱼获得 30 分钟强力 Buff -func eat_cooked_fish(slot: int, inventory: Array, player_data: Dictionary) -> Dictionary: - if slot < 0 or slot >= inventory.size() or inventory[slot] == null: - return {"ok": false, "reason": "INVALID_SLOT"} - - var item = inventory[slot] - var vnum = int(item.get("vnum", 0)) - - if not COOKED_FOODS.has(vnum): - return {"ok": false, "reason": "NOT_EDIBLE_COOKED_FISH"} - - var cfg = COOKED_FOODS[vnum] - var bid: String = cfg["buff_id"] - var stat_k: String = cfg["stat_key"] - var stat_v: int = cfg["stat_val"] - - # 消耗 1 条烤鱼 - var cnt = int(item.get("count", 1)) - if cnt > 1: - item["count"] = cnt - 1 - else: - inventory[slot] = null - - # 如果已有同类 Buff,先刷新时长并避免重复叠加属性 - if active_buffs.has(bid): - active_buffs[bid]["remaining"] = FOOD_BUFF_DURATION - else: - player_data[stat_k] = int(player_data.get(stat_k, 0)) + stat_v - active_buffs[bid] = { - "remaining": FOOD_BUFF_DURATION, - "stat_key": stat_k, - "stat_val": stat_v, - "name": cfg["name"] - } - - food_buff_applied.emit(bid, cfg["name"], FOOD_BUFF_DURATION) - - return { - "ok": true, - "buff_id": bid, - "buff_name": cfg["name"], - "stat_key": stat_k, - "stat_val": stat_v, - "current_stat": player_data[stat_k], - "msg": "品尝了美味的【%s】!精神焕发,获得了【%s +%d】持续 30 分钟!" % [cfg["name"], stat_k, stat_v] - } - -# 帧心跳更新营火与食物 Buff -func update(delta: float, player_data: Dictionary) -> void: - # 营火计时 - if is_campfire_active: - campfire_timer -= delta - if campfire_timer <= 0.0: - is_campfire_active = false - campfire_extinguished.emit() - - # 食物 Buff 计时与过期反扣 - var expired_ids := [] - for bid in active_buffs.keys(): - var b = active_buffs[bid] - b["remaining"] -= delta - if b["remaining"] <= 0.0: - expired_ids.append(bid) - - for bid in expired_ids: - var b = active_buffs[bid] - var stat_k = b["stat_key"] - var stat_v = b["stat_val"] - player_data[stat_k] = max(0, int(player_data.get(stat_k, 0)) - stat_v) - active_buffs.erase(bid) - food_buff_expired.emit(bid) diff --git a/project/fishing_cooking_camp_system.gd.uid b/project/fishing_cooking_camp_system.gd.uid deleted file mode 100644 index 529c8cac..00000000 --- a/project/fishing_cooking_camp_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bl0claww12l2 diff --git a/project/fishing_mining_system.gd b/project/fishing_mining_system.gd deleted file mode 100644 index 099f53c2..00000000 --- a/project/fishing_mining_system.gd +++ /dev/null @@ -1,213 +0,0 @@ -# fishing_mining_system.gd —— 钓鱼与采矿休闲生活系统(40250 官方 1:1 对齐) -# 对齐源码: -# - 40250 server/game/src/fishing.cpp:180-360 (钓竿、鱼饵、钓鱼状态机、开鱼与蚌壳珍珠) -# - 40250 server/game/src/mining.cpp:50-180 (矿镐、矿脉 NPC、采矿倒计时与原石产出) -class_name FishingMiningSystem -extends RefCounted - -# 核心道具 Vnum 常量 -const VNUM_ROD_BASE := 27400 # 钓竿 +0 -const VNUM_BAIT_EARTHWORM := 27800 # 鱼饵 (蚯蚓) -const VNUM_BAIT_PASTE := 27801 # 鱼饵 (面团) - -const VNUM_FISHBONE := 27990 # 鱼骨 -const VNUM_CLAM := 27987 # 蚌壳 -const VNUM_STONE_PIECE := 27991 # 碎石片 -const VNUM_WHITE_PEARL := 27992 # 白珍珠 -const VNUM_BLUE_PEARL := 27993 # 蓝珍珠 -const VNUM_BLOOD_PEARL := 27994 # 血红珍珠 - -const VNUM_PICKAXE_BASE := 29101 # 矿镐 +0 - -# 经典鱼类列表 -const FISH_TABLE := [ - {"vnum": 27802, "name": "金鲫鱼"}, - {"vnum": 27803, "name": "鲤鱼"}, - {"vnum": 27804, "name": "鲑鱼"}, - {"vnum": 27805, "name": "鳟鱼"}, - {"vnum": 27806, "name": "鲶鱼"}, -] - -# 矿脉 NPC 与对应原石产出表 (40250 mining.cpp) -const VEIN_TABLE := { - 20047: {"name": "钻石矿脉", "ore_vnum": 50601, "ore_name": "钻石原石"}, - 20048: {"name": "琥珀矿脉", "ore_vnum": 50602, "ore_name": "琥珀原石"}, - 20049: {"name": "化石木", "ore_vnum": 50603, "ore_name": "化石原石"}, - 20050: {"name": "铜矿脉", "ore_vnum": 50604, "ore_name": "铜原石"}, - 20051: {"name": "银矿脉", "ore_vnum": 50605, "ore_name": "银原石"}, - 20052: {"name": "金矿脉", "ore_vnum": 50606, "ore_name": "金原石"}, - 20053: {"name": "玉矿脉", "ore_vnum": 50607, "ore_name": "玉原石"}, - 20054: {"name": "黑檀木", "ore_vnum": 50608, "ore_name": "黑檀原石"}, - 20055: {"name": "珍珠矿脉", "ore_vnum": 50609, "ore_name": "珍珠原石"}, - 20056: {"name": "白金矿脉", "ore_vnum": 50610, "ore_name": "白金原石"}, - 20057: {"name": "水晶矿脉", "ore_vnum": 50611, "ore_name": "水晶原石"}, - 20058: {"name": "紫晶矿脉", "ore_vnum": 50612, "ore_name": "紫晶原石"}, - 20059: {"name": "天堂泪矿脉", "ore_vnum": 50613, "ore_name": "天堂泪原石"}, -} - -# --- 钓鱼系统 --- - -## 检查物品是否为钓竿 -static func is_fishing_rod(vnum: int) -> bool: - return vnum >= 27400 and vnum <= 27590 - -## 检查物品是否为鱼饵 -static func is_bait(vnum: int) -> bool: - return vnum == VNUM_BAIT_EARTHWORM or vnum == VNUM_BAIT_PASTE - -## 检查是否为鲜鱼 -static func is_live_fish(vnum: int) -> bool: - return vnum >= 27802 and vnum <= 27832 - -## 检查是否为蚌壳 -static func is_clam(vnum: int) -> bool: - return vnum == VNUM_CLAM - -## 钓鱼判定 (40250 fishing.cpp:210) -## has_bait: 是否已挂饵; rod_level: 钓竿等级 (0..9) -static func perform_fishing(has_bait: bool, rod_level := 0, force_success := false) -> Dictionary: - if not has_bait: - return {"ok": false, "code": "NO_BAIT", "msg": "钓钩上没有鱼饵,无法钓鱼!"} - - # 40250 基础钓鱼成功率:40% + 钓竿等级 * 3% - var base_rate := 40 + rod_level * 3 - var roll := 0 if force_success else (randi() % 100 + 1) - - if not force_success and roll > base_rate: - return { - "ok": false, - "code": "FISH_ESCAPED", - "consumed_bait": true, - "msg": "鱼饵被咬走了,但鱼溜掉了!" - } - - # 成功钓上鲜鱼 - var fish_idx := randi() % FISH_TABLE.size() - var fish: Dictionary = FISH_TABLE[fish_idx] - return { - "ok": true, - "code": "SUCCESS", - "consumed_bait": true, - "fish_vnum": fish["vnum"], - "fish_name": fish["name"], - "msg": "钓上了一条活蹦乱跳的【%s】!" % fish["name"] - } - -## 打开鲜鱼 (40250 fishing.cpp:330) -## 鲜鱼开出:死鱼 (50%)、鱼骨 27990 (30%)、蚌壳 27987 (10%)、滑落逃脱 (10%) -static func open_live_fish(fish_vnum: int) -> Dictionary: - var roll := randi() % 100 - if roll < 50: - # 获得死鱼(死鱼 Vnum = 活鱼 + 31) - var dead_vnum := fish_vnum + 31 - return { - "ok": true, - "code": "DEAD_FISH", - "result_vnum": dead_vnum, - "msg": "得到了一条死鱼,可以在篝火上烤熟食用。" - } - elif roll < 80: - # 获得鱼骨 (精炼核心材料) - return { - "ok": true, - "code": "FISHBONE", - "result_vnum": VNUM_FISHBONE, - "msg": "剖开鲜鱼,在鱼肚里找到了一根坚硬的【鱼骨】!" - } - elif roll < 90: - # 获得珍贵蚌壳 - return { - "ok": true, - "code": "CLAM", - "result_vnum": VNUM_CLAM, - "msg": "不可思议!在鱼肚里发现了一个紧闭的【蚌壳】!" - } - else: - return { - "ok": false, - "code": "ESCAPED", - "msg": "鲜鱼一摆尾巴跳进水里溜走了!" - } - -## 打开蚌壳 (40250 fishing.cpp 蚌壳产出珍珠算法) -## 碎石 (50%)、白珍珠 27992 (25%)、蓝珍珠 27993 (15%)、血红珍珠 27994 (10%) -static func open_clam(force_pearl_type := 0) -> Dictionary: - if force_pearl_type > 0: - var pname := "白珍珠" if force_pearl_type == VNUM_WHITE_PEARL else ("蓝珍珠" if force_pearl_type == VNUM_BLUE_PEARL else "血红珍珠") - return { - "ok": true, - "code": "PEARL", - "result_vnum": force_pearl_type, - "msg": "蚌壳里散发着璀璨光芒,开出了珍稀的【%s】!" % pname - } - - var roll := randi() % 100 - if roll < 50: - return { - "ok": true, - "code": "STONE", - "result_vnum": VNUM_STONE_PIECE, - "msg": "蚌壳里只有一块无用的碎石片。" - } - elif roll < 75: - return { - "ok": true, - "code": "PEARL", - "result_vnum": VNUM_WHITE_PEARL, - "msg": "蚌壳里开出了一颗温润光泽的【白珍珠】!" - } - elif roll < 90: - return { - "ok": true, - "code": "PEARL", - "result_vnum": VNUM_BLUE_PEARL, - "msg": "蚌壳里开出了一颗幽蓝剔透的【蓝珍珠】!" - } - else: - return { - "ok": true, - "code": "PEARL", - "result_vnum": VNUM_BLOOD_PEARL, - "msg": "天赐奇宝!蚌壳里开出了一颗极品【血红珍珠】!" - } - -# --- 采矿系统 --- - -## 检查是否为矿镐 -static func is_pickaxe(vnum: int) -> bool: - return vnum >= 29101 and vnum <= 29110 - -## 检查 NPC 是否为矿脉 -static func is_vein(npc_vnum: int) -> bool: - return VEIN_TABLE.has(npc_vnum) - -## 获取矿脉信息 -static func get_vein_info(npc_vnum: int) -> Dictionary: - return VEIN_TABLE.get(npc_vnum, {}) - -## 采矿判定 (40250 mining.cpp:85) -## pickaxe_level: 矿镐等级 (0..9) -static func perform_mining(vein_vnum: int, pickaxe_level := 0, force_success := false) -> Dictionary: - if not is_vein(vein_vnum): - return {"ok": false, "code": "NOT_A_VEIN", "msg": "目标不是有效的矿脉!"} - - var vein: Dictionary = VEIN_TABLE[vein_vnum] - var base_rate := 35 + pickaxe_level * 4 - var roll := 0 if force_success else (randi() % 100 + 1) - - if not force_success and roll > base_rate: - return { - "ok": false, - "code": "MINING_FAIL", - "msg": "用力敲击了矿脉,但没有采集到有效矿石。" - } - - var count := 1 + (randi() % 3) # 1..3 个原石 - return { - "ok": true, - "code": "SUCCESS", - "ore_vnum": vein["ore_vnum"], - "ore_name": vein["ore_name"], - "count": count, - "msg": "成功从【%s】采集到了 %d 个【%s】!" % [vein["name"], count, vein["ore_name"]] - } diff --git a/project/fishing_mining_system.gd.uid b/project/fishing_mining_system.gd.uid deleted file mode 100644 index 959a047f..00000000 --- a/project/fishing_mining_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ceequyq2hytjy diff --git a/project/fishing_rod_reel_system.gd b/project/fishing_rod_reel_system.gd deleted file mode 100644 index 531e5438..00000000 --- a/project/fishing_rod_reel_system.gd +++ /dev/null @@ -1,307 +0,0 @@ -# fishing_rod_reel_system.gd —— Metin2 40250 官方钓鱼抛竿拉杆与浮漂反应小游戏机制 1:1 -# 100% 对照 40250 服务端 fishing.cpp (CreateFishingEvent, Take, FishingPractice, RealRefineRod), fishing.h -class_name FishingRodReelSystem -extends RefCounted - -signal line_cast(rod_level: int, bait_name: String) -signal fish_bite(reaction_time_window: float) -signal fish_caught(fish_vnum: int, fish_name: String, fish_length_cm: float, practice_points: int) -signal fish_escaped(reason: String, practice_points: int) -signal rod_refined(new_vnum: int, success: bool, new_level: int) -signal fishing_error(reason: String) - -# 40250 官方钓竿 Vnum (27400 基础钓鱼竿+0 ~ 27500 +9) -const BASE_ROD_VNUM := 27400 # 钓鱼竿+0 -const MAX_ROD_LEVEL := 9 - -# 40250 官方饵料 Vnum -const VNUM_PASTE := 27800 # 鱼饵面团 (Paste) -const VNUM_EARTHWORM := 27801 # 蚯蚓 (Earthworm) - -# 40250 渔夫 NPC -const NPC_FISHERMAN := 9009 - -# 钓竿进阶最大熟练度点数表与成功率 (对照 item_proto Value(2) 与 Value(3)) -const ROD_LEVEL_DATA: Dictionary = { - 0: {"max_points": 10, "refine_chance": 100, "base_catch_rate": 0.50}, - 1: {"max_points": 20, "refine_chance": 90, "base_catch_rate": 0.55}, - 2: {"max_points": 40, "refine_chance": 80, "base_catch_rate": 0.60}, - 3: {"max_points": 80, "refine_chance": 70, "base_catch_rate": 0.65}, - 4: {"max_points": 120, "refine_chance": 60, "base_catch_rate": 0.70}, - 5: {"max_points": 160, "refine_chance": 50, "base_catch_rate": 0.75}, - 6: {"max_points": 200, "refine_chance": 40, "base_catch_rate": 0.80}, - 7: {"max_points": 250, "refine_chance": 30, "base_catch_rate": 0.85}, - 8: {"max_points": 300, "refine_chance": 20, "base_catch_rate": 0.90}, - 9: {"max_points": 350, "refine_chance": 10, "base_catch_rate": 0.95} -} - -# 常见垂钓渔获池 (对照 fishing.cpp) -const CATCH_POOL: Array[Dictionary] = [ - {"vnum": 27803, "name": "鲈鱼 (Sudak)", "min_len": 15.0, "max_len": 35.0, "weight": 35}, - {"vnum": 27806, "name": "鲤鱼 (Sazan)", "min_len": 25.0, "max_len": 65.0, "weight": 25}, - {"vnum": 27808, "name": "草鱼 (Ot Sazanı)", "min_len": 20.0, "max_len": 55.0, "weight": 20}, - {"vnum": 27810, "name": "水针鱼 (Zargana)", "min_len": 30.0, "max_len": 70.0, "weight": 10}, - {"vnum": 27817, "name": "泥鳅 (Çopra)", "min_len": 8.0, "max_len": 18.0, "weight": 5}, - {"vnum": 70201, "name": "漂白水 (Bleach)", "min_len": 0.0, "max_len": 0.0, "weight": 3}, - {"vnum": 27987, "name": "珍稀珍珠蚌 (Clam)", "min_len": 5.0, "max_len": 12.0, "weight": 2} -] - -# 当前垂钓状态 -var is_fishing: bool = false -var has_bait: bool = false -var current_bait_vnum: int = 0 - -var rod_level: int = 0 -var rod_practice_points: int = 0 - -var bite_timer: float = 0.0 -var is_bite_active: bool = false -var bite_reaction_time_left: float = 0.0 -const REACTION_WINDOW_SECONDS := 3.0 # 3 秒拉杆反应时间窗口 - -# ========================================== -# 1. 挂饵与抛竿 (fishing.cpp: CreateFishingEvent) -# ========================================== - -# 穿戴/设定当前鱼竿 -func equip_rod(level: int, current_points: int = 0) -> void: - rod_level = clamp(level, 0, MAX_ROD_LEVEL) - var max_p = ROD_LEVEL_DATA[rod_level]["max_points"] - rod_practice_points = clamp(current_points, 0, max_p) - -# 挂上鱼饵 (面团 27800 或 蚯蚓 27801) -func attach_bait(inventory: Array, bait_slot: int) -> Dictionary: - if is_fishing: - fishing_error.emit("ALREADY_FISHING") - return {"ok": false, "reason": "ALREADY_FISHING", "msg": "正在垂钓中,无法更换鱼饵!"} - - if bait_slot < 0 or bait_slot >= inventory.size() or inventory[bait_slot] == null: - fishing_error.emit("INVALID_SLOT") - return {"ok": false, "reason": "INVALID_SLOT", "msg": "未找到指定的鱼饵!"} - - var item = inventory[bait_slot] - var vnum = int(item.get("vnum", 0)) - if vnum != VNUM_PASTE and vnum != VNUM_EARTHWORM: - fishing_error.emit("NOT_BAIT") - return {"ok": false, "reason": "NOT_BAIT", "msg": "该物品无法作为鱼饵挂在鱼钩上!"} - - _consume_item_one(inventory, bait_slot) - has_bait = true - current_bait_vnum = vnum - var bait_name = "鱼饵面团" if vnum == VNUM_PASTE else "蚯蚓" - return {"ok": true, "bait_vnum": vnum, "bait_name": bait_name, "msg": "将【%s】小心地挂在了鱼钩上。" % bait_name} - -# 抛竿垂钓 -func cast_line(forced_wait_time: float = -1.0) -> Dictionary: - if not has_bait: - fishing_error.emit("NO_BAIT") - return {"ok": false, "reason": "NO_BAIT", "msg": "鱼钩上空无一物!请先挂上蚯蚓或面团饵料。"} - - if is_fishing: - fishing_error.emit("ALREADY_FISHING") - return {"ok": false, "reason": "ALREADY_FISHING", "msg": "钓线已经抛入水中,静候鱼儿上钩。"} - - is_fishing = true - is_bite_active = false - has_bait = false # 抛竿时消耗鱼钩上的鱼饵 - - # 40250 等待上钩时间: 2~7 秒 - bite_timer = forced_wait_time if forced_wait_time > 0.0 else randf_range(2.0, 7.0) - bite_reaction_time_left = 0.0 - - var bait_name = "鱼饵面团" if current_bait_vnum == VNUM_PASTE else "蚯蚓" - line_cast.emit(rod_level, bait_name) - return {"ok": true, "msg": "钓竿轻扬,鱼线带着微澜划破水面,沉入了平静的湖心……"} - -# ========================================== -# 2. 反应窗口拉杆提竿 (fishing.cpp: Take) -# ========================================== -func pull_rod(inventory: Array, forced_catch_idx: int = -1, forced_success: bool = false) -> Dictionary: - if not is_fishing: - fishing_error.emit("NOT_FISHING") - return {"ok": false, "reason": "NOT_FISHING", "msg": "你并未处于垂钓状态!"} - - var max_p = ROD_LEVEL_DATA[rod_level]["max_points"] - - # 提前起竿 (鱼儿未咬钩) - if not is_bite_active: - is_fishing = false - bite_timer = 0.0 - # 哪怕失败亦获得 1 点熟练度练习 (FishingPractice) - if rod_practice_points < max_p: - rod_practice_points += 1 - fish_escaped.emit("PULLED_TOO_EARLY", rod_practice_points) - return { - "ok": false, - "reason": "PULLED_TOO_EARLY", - "practice_points": rod_practice_points, - "msg": "起竿太早了!鱼钩划破空荡荡的水面,鱼饵不翼而飞。" - } - - # 鱼已咬钩:计算捕获概率 - is_fishing = false - is_bite_active = false - - # 熟练度 +1 - if rod_practice_points < max_p: - rod_practice_points += 1 - - var base_rate = float(ROD_LEVEL_DATA[rod_level]["base_catch_rate"]) - if current_bait_vnum == VNUM_EARTHWORM: - base_rate += 0.05 # 蚯蚓比面团成功率多 5% - - var roll = randf() - var success = (roll <= base_rate) or forced_success - - if success: - # 捕获成功:从鱼池选取 - var picked_fish: Dictionary = {} - if forced_catch_idx >= 0 and forced_catch_idx < CATCH_POOL.size(): - picked_fish = CATCH_POOL[forced_catch_idx] - else: - var total_w := 0 - for f in CATCH_POOL: - total_w += int(f["weight"]) - var r = randi_range(1, total_w) - var cur := 0 - for f in CATCH_POOL: - cur += int(f["weight"]) - if r <= cur: - picked_fish = f - break - - var fish_vnum = int(picked_fish["vnum"]) - var fish_name = str(picked_fish["name"]) - var length_cm = randf_range(float(picked_fish["min_len"]), float(picked_fish["max_len"])) - - _give_or_stack_item(inventory, fish_vnum, 1) - fish_caught.emit(fish_vnum, fish_name, length_cm, rod_practice_points) - return { - "ok": true, - "fish_vnum": fish_vnum, - "fish_name": fish_name, - "length_cm": length_cm, - "practice_points": rod_practice_points, - "msg": "水花四溅!你收紧鱼线,成功钓上了一条 %.1f cm 的【%s】!" % [length_cm, fish_name] - } - else: - # 挣脱脱钩 - fish_escaped.emit("FISH_GOT_AWAY", rod_practice_points) - return { - "ok": false, - "reason": "FISH_GOT_AWAY", - "practice_points": rod_practice_points, - "msg": "大鱼猛烈摆尾,挣脱鱼钩游回了深水,只留下一串水泡……" - } - -# ========================================== -# 3. 渔夫 NPC 鱼竿升星锻造 (fishing.cpp: RealRefineRod) -# ========================================== -func can_refine_rod() -> bool: - if rod_level >= MAX_ROD_LEVEL: - return false - var max_p = ROD_LEVEL_DATA[rod_level]["max_points"] - return rod_practice_points >= max_p - -func refine_rod_at_fisherman(forced_roll: int = -1) -> Dictionary: - if not can_refine_rod(): - fishing_error.emit("PRACTICE_NOT_MAXED") - return { - "ok": false, - "reason": "PRACTICE_NOT_MAXED", - "msg": "鱼竿熟练度尚未练满!需要 %d/%d 熟练度点数方可前往渔夫处升级。" % [ - rod_practice_points, ROD_LEVEL_DATA[rod_level]["max_points"] - ] - } - - var chance = int(ROD_LEVEL_DATA[rod_level]["refine_chance"]) - var r = forced_roll if forced_roll > 0 else randi_range(1, 100) - - if r <= chance: - # 锻造升级成功 - rod_level += 1 - rod_practice_points = 0 - var new_vnum = BASE_ROD_VNUM + rod_level - rod_refined.emit(new_vnum, true, rod_level) - return { - "ok": true, - "success": true, - "new_level": rod_level, - "new_vnum": new_vnum, - "msg": "渔夫巧手翻飞,金光闪烁!你的钓鱼竿成功进阶为【钓鱼竿+%d】!" % rod_level - } - else: - # 锻造失败:降 1 级 (40250 官方机制) - rod_level = max(0, rod_level - 1) - rod_practice_points = 0 - var new_vnum = BASE_ROD_VNUM + rod_level - rod_refined.emit(new_vnum, false, rod_level) - return { - "ok": true, - "success": false, - "new_level": rod_level, - "new_vnum": new_vnum, - "msg": "精炼失败了!鱼竿受损,品质降为了【钓鱼竿+%d】。" % rod_level - } - -# ========================================== -# 4. 心跳推进与超时判定 -# ========================================== -func update(delta: float) -> void: - if not is_fishing: - return - - if not is_bite_active: - # 等待鱼咬钩 - bite_timer -= delta - if bite_timer <= 0.0: - is_bite_active = true - bite_reaction_time_left = REACTION_WINDOW_SECONDS - fish_bite.emit(REACTION_WINDOW_SECONDS) - else: - # 鱼咬钩等待提竿窗口 - bite_reaction_time_left -= delta - if bite_reaction_time_left <= 0.0: - # 超时逃跑 - is_fishing = false - is_bite_active = false - var max_p = ROD_LEVEL_DATA[rod_level]["max_points"] - if rod_practice_points < max_p: - rod_practice_points += 1 - fish_escaped.emit("REACTION_TIMEOUT", rod_practice_points) - -func _consume_item_one(inventory: Array, slot: int) -> void: - if slot < 0 or slot >= inventory.size() or inventory[slot] == null: - return - var item = inventory[slot] - var count = int(item.get("count", 1)) - if count > 1: - item["count"] = count - 1 - else: - inventory[slot] = null - -func _give_or_stack_item(inventory: Array, vnum: int, count: int) -> void: - for it in inventory: - if it != null and int(it.get("vnum", 0)) == vnum: - it["count"] = int(it.get("count", 1)) + count - return - for i in range(inventory.size()): - if inventory[i] == null: - inventory[i] = {"vnum": vnum, "count": count} - return - inventory.append({"vnum": vnum, "count": count}) - -# 序列化与反序列化 -func serialize() -> Dictionary: - return { - "rod_level": rod_level, - "rod_practice_points": rod_practice_points, - "has_bait": has_bait, - "current_bait_vnum": current_bait_vnum - } - -func deserialize(data: Dictionary) -> void: - rod_level = int(data.get("rod_level", 0)) - rod_practice_points = int(data.get("rod_practice_points", 0)) - has_bait = bool(data.get("has_bait", false)) - current_bait_vnum = int(data.get("current_bait_vnum", 0)) diff --git a/project/fishing_rod_reel_system.gd.uid b/project/fishing_rod_reel_system.gd.uid deleted file mode 100644 index 72fad235..00000000 --- a/project/fishing_rod_reel_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b1ti6751mgdy2 diff --git a/project/flame_dungeon_razador_system.gd b/project/flame_dungeon_razador_system.gd deleted file mode 100644 index 5c95bbf5..00000000 --- a/project/flame_dungeon_razador_system.gd +++ /dev/null @@ -1,196 +0,0 @@ -# flame_dungeon_razador_system.gd —— Metin2 40250 经典红龙烈焰神殿与拉扎多尔炼狱试炼 1:1 -# 对照 40250 服务端 flame_dungeon.start.1.script, quest/flame_dungeon*, char_item.cpp -class_name FlameDungeonRazadorSystem -extends RefCounted - -signal dungeon_entered(time_limit: float) -signal stage_advanced(new_stage: int, stage_desc: String) -signal altar_lit(altars_lit: int, total_altars: int) -signal razador_spawned(mob_vnum: int) -signal razador_slain(drop_rewards: Array) -signal dungeon_completed() -signal dungeon_failed(reason: String) - -const NPC_AM_HEH_WATCHER := 20385 # 烈焰守望者 (Am-heh Watcher) -const MIN_ENTRY_LEVEL := 100 # 进入等级门槛 100 级 -const VNUM_TICKET_71095 := 71095 # 通行证 -const VNUM_TICKET_71130 := 71130 # 烈焰通行证 - -const MOB_RAZADOR := 6091 # 烈焰魔王拉扎多尔 (Razador) -const VNUM_RAZADOR_CHEST := 50186 # 拉扎多尔宝箱 - -const DUNGEON_TIME_LIMIT := 2700.0 # 45 分钟 (2700 秒) - -# 7 大炼狱阶段定义 -const STAGES_DESC: Array = [ - "阶段 1: 清剿外围烈焰魔群", - "阶段 2: 寻觅正确齿轮诛灭引火者", - "阶段 3: 破灭炼狱灭世魔石", - "阶段 4: 诛灭四极炼狱护法", - "阶段 5: 使用真·玛阿特之石点燃五方火盆", - "阶段 6: 击碎烈焰火轮魔石", - "阶段 7: 进军神殿核心,决战烈焰魔王拉扎多尔!" -] - -var is_active: bool = false -var current_stage: int = 1 -var time_remaining: float = 0.0 -var altars_lit_count: int = 0 -var razador_alive: bool = false - -# 检票进入烈焰神殿 (40250 flame_dungeon.start.1.script) -func enter_dungeon(player_level: int, inventory: Array) -> Dictionary: - if is_active: - return {"ok": false, "reason": "DUNGEON_ALREADY_ACTIVE", "msg": "神殿试炼已在进行中!"} - - if player_level < MIN_ENTRY_LEVEL: - return { - "ok": false, - "reason": "LEVEL_TOO_LOW", - "msg": "实力不足!开启拉扎多尔烈焰试炼需角色达到 %d 级。" % MIN_ENTRY_LEVEL - } - - # 检查并消耗 1 张通行证 (71095 或 71130) - var ticket_slot := -1 - for i in range(inventory.size()): - if inventory[i] != null: - var v := int(inventory[i].get("vnum", 0)) - if v == VNUM_TICKET_71095 or v == VNUM_TICKET_71130: - ticket_slot = i - break - - if ticket_slot == -1: - return {"ok": false, "reason": "NO_ENTRY_TICKET", "msg": "守望者拦住了去路!需要持有【烈焰通行证】(71095/71130)。"} - - var t_item: Dictionary = inventory[ticket_slot] - var cnt := int(t_item.get("count", 1)) - if cnt > 1: - t_item["count"] = cnt - 1 - else: - inventory[ticket_slot] = null - - is_active = true - current_stage = 1 - time_remaining = DUNGEON_TIME_LIMIT - altars_lit_count = 0 - razador_alive = false - - dungeon_entered.emit(DUNGEON_TIME_LIMIT) - stage_advanced.emit(1, STAGES_DESC[0]) - - return { - "ok": true, - "time_limit": DUNGEON_TIME_LIMIT, - "stage": 1, - "stage_desc": STAGES_DESC[0], - "msg": "通行证校验通过!烈焰之门轰然开启,限时 45 分钟挑战拉扎多尔!" - } - -# 推进阶段目标 -func advance_stage() -> Dictionary: - if not is_active: - return {"ok": false, "reason": "DUNGEON_NOT_ACTIVE"} - - if current_stage < 7: - current_stage += 1 - var desc: String = STAGES_DESC[current_stage - 1] - - if current_stage == 7: - # 召唤最终首领拉扎多尔 - razador_alive = true - razador_spawned.emit(MOB_RAZADOR) - - stage_advanced.emit(current_stage, desc) - return { - "ok": true, - "current_stage": current_stage, - "stage_desc": desc, - "msg": "试炼突破!开启【%s】。" % desc - } - - return {"ok": false, "reason": "ALREADY_AT_FINAL_STAGE"} - -# 阶段 5 点燃玛阿特火盆 -func light_fire_altar() -> Dictionary: - if not is_active or current_stage != 5: - return {"ok": false, "reason": "NOT_IN_ALTAR_STAGE"} - - altars_lit_count = mini(5, altars_lit_count + 1) - altar_lit.emit(altars_lit_count, 5) - - if altars_lit_count >= 5: - # 5 座火盆全部点燃,自动迈入第 6 阶段 - advance_stage() - return { - "ok": true, - "altars_lit": 5, - "stage_cleared": true, - "msg": "五方火盆全部燃起纯阳烈焰!封印解除,进入下一阶段!" - } - - return { - "ok": true, - "altars_lit": altars_lit_count, - "stage_cleared": false, - "msg": "成功点燃 1 座火盆!当前进度:(%d / 5)。" % [altars_lit_count] - } - -# 斩杀烈焰魔王拉扎多尔 -func slay_razador(inventory: Array) -> Dictionary: - if not is_active or not razador_alive: - return {"ok": false, "reason": "RAZADOR_NOT_ALIVE"} - - razador_alive = false - - var rewards: Array = [ - {"vnum": VNUM_RAZADOR_CHEST, "name": "拉扎多尔宝箱", "count": 1}, - {"vnum": 25040, "name": "祝福卷轴", "count": 3}, - {"vnum": 51501, "name": "龙石炼金原石", "count": 2} - ] - - for drop in rewards: - for i in range(inventory.size()): - if inventory[i] == null: - inventory[i] = drop.duplicate(true) - break - - razador_slain.emit(rewards) - - # 通关副本 - is_active = false - time_remaining = 0.0 - dungeon_completed.emit() - - return { - "ok": true, - "rewards": rewards, - "msg": "伟大的征服!烈焰魔王拉扎多尔陨落!斩获【拉扎多尔宝箱】!" - } - -# 逻辑心跳更新 -func update(delta: float) -> void: - if not is_active: - return - time_remaining -= delta - if time_remaining <= 0.0: - is_active = false - razador_alive = false - dungeon_failed.emit("TIMEOUT") - -# 序列化 -func serialize() -> Dictionary: - return { - "is_active": is_active, - "current_stage": current_stage, - "time_remaining": time_remaining, - "altars_lit_count": altars_lit_count, - "razador_alive": razador_alive - } - -# 反序列化 -func deserialize(data: Dictionary) -> void: - is_active = bool(data.get("is_active", false)) - current_stage = int(data.get("current_stage", 1)) - time_remaining = float(data.get("time_remaining", 0.0)) - altars_lit_count = int(data.get("altars_lit_count", 0)) - razador_alive = bool(data.get("razador_alive", false)) diff --git a/project/flame_dungeon_razador_system.gd.uid b/project/flame_dungeon_razador_system.gd.uid deleted file mode 100644 index df4180b5..00000000 --- a/project/flame_dungeon_razador_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bue5wpi4eyixl diff --git a/project/game_button_money_dialog_system.gd b/project/game_button_money_dialog_system.gd deleted file mode 100644 index 8b386ca1..00000000 --- a/project/game_button_money_dialog_system.gd +++ /dev/null @@ -1,127 +0,0 @@ -# game_button_money_dialog_system.gd -# 40250 官方 1:1 浮动任务栏状态红点通知与金币分堆丢弃输入窗 -# 对照: uigamebutton.py, gamewindow.py, uipickmoney.py, PickMoneyDialog.py -class_name GameButtonMoneyDialogSystem -extends RefCounted - -# ========================================================================= -# 1. 浮动任务栏状态红点与操作按钮组 (GameButtonWindow) -# ========================================================================= -class GameButtonManager extends RefCounted: - var button_visible: Dictionary = { - "STATUS": false, - "SKILL": false, - "QUEST": false, - "HELP": false, - "BUILD": false, - "EXIT_OBSERVER": false - } - - var is_observer_mode: bool = false - var last_chat_packet_sent: String = "" - - signal button_visibility_changed(btn_name: String, visible: bool) - signal observer_exit_triggered() - - func check_game_buttons(stat_points: int, skill_active_points: int, play_time: int) -> void: - # player.GetStatus(player.STAT) > 0 -> StatusPlusButton - var show_status = stat_points > 0 - _set_button("STATUS", show_status) - - # player.GetStatus(player.SKILL_ACTIVE) > 0 -> SkillPlusButton - var show_skill = skill_active_points > 0 - _set_button("SKILL", show_skill) - - # 0 == player.GetPlayTime() -> HelpButton - var show_help = (play_time == 0) - _set_button("HELP", show_help) - - func set_quest_notice(has_unread: bool) -> void: - _set_button("QUEST", has_unread) - - func set_build_button(can_build: bool) -> void: - _set_button("BUILD", can_build) - - func set_observer_mode(enabled: bool) -> void: - is_observer_mode = enabled - _set_button("EXIT_OBSERVER", enabled) - - func click_exit_observer() -> void: - if is_observer_mode: - last_chat_packet_sent = "/observer_exit" - observer_exit_triggered.emit() - - func _set_button(btn_name: String, is_show: bool) -> void: - if button_visible.get(btn_name, false) != is_show: - button_visible[btn_name] = is_show - button_visibility_changed.emit(btn_name, is_show) - -# ========================================================================= -# 2. 金币/物品分堆输入对话框 (PickMoneyDialog) -# ========================================================================= -class PickMoneyDialog extends RefCounted: - const DIALOG_WIDTH: int = 170 - const DIALOG_HEIGHT: int = 90 - - var is_open: bool = false - var max_value: int = 0 - var unit_value: int = 1 - var current_input_text: String = "1" - - var pos_x: int = 0 - var pos_y: int = 0 - - var last_accepted_value: int = 0 - - signal opened(max_val: int, unit_val: int, x: int, y: int) - signal accepted(amount: int) - signal closed() - - func open_dialog(p_max_value: int, p_unit_value: int = 1, mouse_x: int = 0, mouse_y: int = 0, screen_w: int = 1024, screen_h: int = 768) -> void: - max_value = p_max_value - unit_value = p_unit_value - current_input_text = str(unit_value) - last_accepted_value = 0 - - # 40250 官方吸附定位计算 - var half_w = DIALOG_WIDTH / 2 - if mouse_x + half_w > screen_w: - pos_x = screen_w - DIALOG_WIDTH - elif mouse_x - half_w < 0: - pos_x = 0 - else: - pos_x = mouse_x - half_w - - pos_y = mouse_y - DIALOG_HEIGHT - 20 - if pos_y < 0: - pos_y = 10 - - is_open = true - opened.emit(max_value, unit_value, pos_x, pos_y) - - func input_text(text: String) -> void: - current_input_text = text - - func on_accept() -> bool: - if not is_open: - return false - - var text = current_input_text.strip_edges() - if text.is_valid_int(): - var val = text.to_int() - if val > 0: - val = mini(val, max_value) - last_accepted_value = val - accepted.emit(val) - close_dialog() - return true - - close_dialog() - return false - - func close_dialog() -> void: - is_open = false - closed.emit() - -var game_buttons: GameButtonManager = GameButtonManager.new() -var money_dialog: PickMoneyDialog = PickMoneyDialog.new() diff --git a/project/game_button_money_dialog_system.gd.uid b/project/game_button_money_dialog_system.gd.uid deleted file mode 100644 index 18c2fac2..00000000 --- a/project/game_button_money_dialog_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dqah8cpuylspa diff --git a/project/gm_toolkit_system.gd b/project/gm_toolkit_system.gd deleted file mode 100644 index 214ab2af..00000000 --- a/project/gm_toolkit_system.gd +++ /dev/null @@ -1,207 +0,0 @@ -# gm_toolkit_system.gd —— Metin2 40250 单人单机内置 GM 控制台与命令解析器 1:1 -# 对照 40250 服务端 cmd_gm.cpp, cmd.cpp -class_name GmToolkitSystem -extends RefCounted - -signal command_executed(cmd: String, success: bool, result_msg: String) -signal player_warped(target_pos: Vector3, map_name: String) -signal mob_spawned(vnum: int, count: int, pos: Vector3) - -const MAP_PRESETS: Dictionary = { - "village": Vector3(0.0, 0.0, 0.0), - "sohan": Vector3(400.0, 0.0, 600.0), # 冰山 Mount Sohan - "desert": Vector3(200.0, 0.0, 300.0), # 永壁沙漠 Yongbi Desert - "temple": Vector3(100.0, 0.0, 500.0), # 黄龙神殿 Hwang Temple - "flame": Vector3(600.0, 0.0, 200.0), # 烈焰原野 Doyyumhwan - "catacomb": Vector3(500.0, 0.0, 500.0) # 地下墓穴 Devil's Catacomb -} - -# 解析并执行 GM 斜杠指令 -func execute_command(cmd_string: String, ctx: Dictionary) -> Dictionary: - var trimmed := cmd_string.strip_edges() - if not trimmed.begins_with("/"): - return {"ok": false, "reason": "NOT_A_COMMAND", "msg": "指令必须以 '/' 开头!"} - - var parts := trimmed.substr(1).split(" ", false) - if parts.is_empty(): - return {"ok": false, "reason": "EMPTY_COMMAND"} - - var action := parts[0].to_lower() - var res: Dictionary = {} - - match action: - "item": - res = _handle_item(parts, ctx) - "level": - res = _handle_level(parts, ctx) - "gold": - res = _handle_gold(parts, ctx) - "set_skill", "skill": - res = _handle_skill(parts, ctx) - "warp", "map": - res = _handle_warp(parts, ctx) - "spawn_mob", "mob", "m": - res = _handle_mob(parts, ctx) - "help": - res = _handle_help() - _: - res = {"ok": false, "reason": "UNKNOWN_COMMAND", "msg": "未知的 GM 指令【%s】!输入 /help 查看支持的指令列表。" % action} - - command_executed.emit(action, res.get("ok", false), res.get("msg", "")) - return res - -# /item [count] -func _handle_item(parts: PackedStringArray, ctx: Dictionary) -> Dictionary: - if parts.size() < 2 or not parts[1].is_valid_int(): - return {"ok": false, "reason": "SYNTAX_ERROR", "msg": "用法: /item <物品代码vnum> [数量count]"} - - var vnum: int = int(parts[1]) - var count: int = 1 - if parts.size() >= 3 and parts[2].is_valid_int(): - count = maxi(1, int(parts[2])) - - var inventory: Array = ctx.get("inventory", []) - var empty_slot := -1 - for i in range(inventory.size()): - if inventory[i] == null: - empty_slot = i - break - - if empty_slot == -1: - return {"ok": false, "reason": "INVENTORY_FULL", "msg": "背包已满,无法生成物品!"} - - inventory[empty_slot] = { - "vnum": vnum, - "name": "道具_%d" % vnum, - "count": count - } - - return { - "ok": true, - "vnum": vnum, - "count": count, - "slot": empty_slot, - "msg": "【GM】已成功生成物品 [vnum: %d, count: %d] 至背包格子 %d!" % [vnum, count, empty_slot] - } - -# /level <1-105> -func _handle_level(parts: PackedStringArray, ctx: Dictionary) -> Dictionary: - if parts.size() < 2 or not parts[1].is_valid_int(): - return {"ok": false, "reason": "SYNTAX_ERROR", "msg": "用法: /level <1-105>"} - - var target_lv: int = clampi(int(parts[1]), 1, 105) - var player_data: Dictionary = ctx.get("player_data", {}) - player_data["level"] = target_lv - - return { - "ok": true, - "level": target_lv, - "msg": "【GM】角色等级已修改为 Lv %d!" % target_lv - } - -# /gold -func _handle_gold(parts: PackedStringArray, ctx: Dictionary) -> Dictionary: - if parts.size() < 2 or not parts[1].is_valid_int(): - return {"ok": false, "reason": "SYNTAX_ERROR", "msg": "用法: /gold <金币数额>"} - - var amount: int = int(parts[1]) - var player_data: Dictionary = ctx.get("player_data", {}) - var current: int = int(player_data.get("gold", 0)) - var new_gold: int = maxi(0, current + amount) - player_data["gold"] = new_gold - - return { - "ok": true, - "added": amount, - "total_gold": new_gold, - "msg": "【GM】金币已注入!当前金币:%d Yang。" % new_gold - } - -# /set_skill <1-40> -func _handle_skill(parts: PackedStringArray, ctx: Dictionary) -> Dictionary: - if parts.size() < 3 or not parts[1].is_valid_int() or not parts[2].is_valid_int(): - return {"ok": false, "reason": "SYNTAX_ERROR", "msg": "用法: /set_skill <技能ID> <等级1-40> (40=P级)"} - - var skill_id: int = int(parts[1]) - var skill_lv: int = clampi(int(parts[2]), 1, 40) - var skills: Dictionary = ctx.get("skills", {}) - skills[skill_id] = skill_lv - - var grade_str := "普通" - if skill_lv >= 40: - grade_str = "P级 (Perfect Master)" - elif skill_lv >= 30: - grade_str = "G级 (Grand Master)" - elif skill_lv >= 20: - grade_str = "M级 (Master)" - - return { - "ok": true, - "skill_id": skill_id, - "skill_level": skill_lv, - "grade": grade_str, - "msg": "【GM】技能 [%d] 等级已调整为 %d (%s)!" % [skill_id, skill_lv, grade_str] - } - -# /warp 或 /map -func _handle_warp(parts: PackedStringArray, ctx: Dictionary) -> Dictionary: - var target_pos := Vector3.ZERO - var map_name := "custom" - - if parts.size() >= 3 and parts[1].is_valid_float() and parts[2].is_valid_float(): - target_pos = Vector3(float(parts[1]), 0.0, float(parts[2])) - elif parts.size() >= 2: - var preset_key: String = parts[1].to_lower() - if MAP_PRESETS.has(preset_key): - target_pos = MAP_PRESETS[preset_key] - map_name = preset_key - else: - return {"ok": false, "reason": "UNKNOWN_MAP", "msg": "未知的预设地图!可用地图: %s" % str(MAP_PRESETS.keys())} - else: - return {"ok": false, "reason": "SYNTAX_ERROR", "msg": "用法: /warp 或 /map <地图名>"} - - player_warped.emit(target_pos, map_name) - - return { - "ok": true, - "position": target_pos, - "map_name": map_name, - "msg": "【GM】已瞬移传送至目标坐标 (%.1f, %.1f) [%s]!" % [target_pos.x, target_pos.z, map_name] - } - -# /spawn_mob [count] -func _handle_mob(parts: PackedStringArray, ctx: Dictionary) -> Dictionary: - if parts.size() < 2 or not parts[1].is_valid_int(): - return {"ok": false, "reason": "SYNTAX_ERROR", "msg": "用法: /spawn_mob <怪代码vnum> [数量count]"} - - var mob_vnum: int = int(parts[1]) - var count: int = 1 - if parts.size() >= 3 and parts[2].is_valid_int(): - count = maxi(1, int(parts[2])) - - var player_pos: Vector3 = ctx.get("player_pos", Vector3.ZERO) - mob_spawned.emit(mob_vnum, count, player_pos) - - return { - "ok": true, - "mob_vnum": mob_vnum, - "count": count, - "spawn_pos": player_pos, - "msg": "【GM】已在身旁召唤怪物 [vnum: %d, count: %d]!" % [mob_vnum, count] - } - -# /help -func _handle_help() -> Dictionary: - return { - "ok": true, - "commands": [ - "/item [count] - 生成物品", - "/level <1-105> - 设定等级", - "/gold - 注入金币", - "/set_skill <1-40> - 设定技能等级(40=P级)", - "/map - 传送至指定地图", - "/warp - 坐标瞬移", - "/spawn_mob [count] - 刷怪/召唤首领" - ], - "msg": "【GM指令帮助】输入对应命令快速调试单机环境!" - } diff --git a/project/gm_toolkit_system.gd.uid b/project/gm_toolkit_system.gd.uid deleted file mode 100644 index 9a74474a..00000000 --- a/project/gm_toolkit_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c0t4mhtsnoxqc diff --git a/project/guild_mark_uploader_system.gd b/project/guild_mark_uploader_system.gd deleted file mode 100644 index 771c9865..00000000 --- a/project/guild_mark_uploader_system.gd +++ /dev/null @@ -1,156 +0,0 @@ -# guild_mark_uploader_system.gd —— 40250 行会 16x12 图标与战旗上传分发系统 1:1 -# 严格对照: -# Client/Eternexus/root/uiuploadmark.py (MarkItem, SymbolItem) -# ClientVS22/source/UserInterface/GuildMarkUploader.cpp, GuildMarkDownloader.cpp -# ClientVS22/source/UserInterface/MarkManager.cpp, MarkImage.cpp -extends RefCounted - -const MARK_WIDTH := 16 # 官方行会图标宽度 -const MARK_HEIGHT := 12 # 官方行会图标高度 - -const SYMBOL_WIDTH := 64 # 官方战旗宽度 (GuildMarkUploader.cpp:119) -const SYMBOL_HEIGHT := 128 # 官方战旗高度 (GuildMarkUploader.cpp:124) - -# 错误码 (GuildMarkUploader.cpp) -enum UploadError { - ERROR_NONE, - ERROR_NOT_GUILD_MASTER, - ERROR_LOAD, - ERROR_WIDTH, - ERROR_HEIGHT, - ERROR_UNSUPPORTED_FORMAT -} - -class GuildMarkData: - var guild_id: int - var mark_type: String # "MARK" 或 "SYMBOL" - var width: int - var height: int - var raw_pixels: PackedByteArray - var mark_hash: String - - func _init(p_gid: int, p_type: String, w: int, h: int, pixels: PackedByteArray) -> void: - guild_id = p_gid - mark_type = p_type - width = w - height = h - raw_pixels = pixels - mark_hash = str(p_gid) + "_" + p_type + "_" + str(Time.get_ticks_msec()) - -# 客户端本地缓存: guild_id -> { "mark": GuildMarkData, "symbol": GuildMarkData } -var cached_marks: Dictionary = {} - -func _init() -> void: - cached_marks.clear() - -# 1. 上传 16x12 行会图标 (1:1 CPythonGuild::UploadMark) -func upload_guild_mark( - guild_id: int, - is_guild_master: bool, - image_width: int, - image_height: int, - image_format: String = "bmp", - pixel_bytes: PackedByteArray = PackedByteArray() -) -> Dictionary: - var res := { - "success": false, - "error_code": UploadError.ERROR_NONE, - "mark_data": null, - "message": "" - } - - # 权限检查:只有会长或持有上传权者可操作 - if not is_guild_master: - res["error_code"] = UploadError.ERROR_NOT_GUILD_MASTER - res["message"] = "只有行会长有权上传或更改行会图标。" - return res - - # 格式校验 (官方仅支持 BMP 格式) - if image_format.to_lower() != "bmp": - res["error_code"] = UploadError.ERROR_UNSUPPORTED_FORMAT - res["message"] = "行会图标必须为 BMP 格式图像。" - return res - - # 尺寸精确校验 (16 x 12) - if image_width != MARK_WIDTH: - res["error_code"] = UploadError.ERROR_WIDTH - res["message"] = "图标宽度不符合规格 (必须为 16 像素)。" - return res - - if image_height != MARK_HEIGHT: - res["error_code"] = UploadError.ERROR_HEIGHT - res["message"] = "图标高度不符合规格 (必须为 12 像素)。" - return res - - # 生成并存入缓存 - var mark_obj = GuildMarkData.new(guild_id, "MARK", MARK_WIDTH, MARK_HEIGHT, pixel_bytes) - if not cached_marks.has(guild_id): - cached_marks[guild_id] = {} - cached_marks[guild_id]["mark"] = mark_obj - - res["success"] = true - res["mark_data"] = mark_obj - res["message"] = "行会 16x12 图标上传成功,已同步至头顶战标渲染!" - return res - -# 2. 上传 64x128 行会战旗 (1:1 CPythonGuild::UploadSymbol) -func upload_guild_symbol( - guild_id: int, - is_guild_master: bool, - image_width: int, - image_height: int, - image_format: String = "bmp", - pixel_bytes: PackedByteArray = PackedByteArray() -) -> Dictionary: - var res := { - "success": false, - "error_code": UploadError.ERROR_NONE, - "symbol_data": null, - "message": "" - } - - if not is_guild_master: - res["error_code"] = UploadError.ERROR_NOT_GUILD_MASTER - res["message"] = "只有行会长有权上传行会战旗。" - return res - - if image_format.to_lower() != "bmp": - res["error_code"] = UploadError.ERROR_UNSUPPORTED_FORMAT - res["message"] = "战旗必须为 BMP 格式图像。" - return res - - if image_width != SYMBOL_WIDTH: - res["error_code"] = UploadError.ERROR_WIDTH - res["message"] = "战旗宽度不符合规格 (必须为 64 像素)。" - return res - - if image_height != SYMBOL_HEIGHT: - res["error_code"] = UploadError.ERROR_HEIGHT - res["message"] = "战旗高度不符合规格 (必须为 128 像素)。" - return res - - var symbol_obj = GuildMarkData.new(guild_id, "SYMBOL", SYMBOL_WIDTH, SYMBOL_HEIGHT, pixel_bytes) - if not cached_marks.has(guild_id): - cached_marks[guild_id] = {} - cached_marks[guild_id]["symbol"] = symbol_obj - - res["success"] = true - res["symbol_data"] = symbol_obj - res["message"] = "行会 64x128 战旗上传成功,已部署至领地旗杆!" - return res - -# 3. 获取行会图标与战旗 (1:1 MarkManager::GetMarkImage) -func get_guild_mark(guild_id: int) -> GuildMarkData: - if cached_marks.has(guild_id) and cached_marks[guild_id].has("mark"): - return cached_marks[guild_id]["mark"] - return null - -func get_guild_symbol(guild_id: int) -> GuildMarkData: - if cached_marks.has(guild_id) and cached_marks[guild_id].has("symbol"): - return cached_marks[guild_id]["symbol"] - return null - -# 4. 清除行会图标 -func clear_guild_mark(guild_id: int) -> void: - if cached_marks.has(guild_id): - cached_marks[guild_id].erase("mark") diff --git a/project/guild_mark_uploader_system.gd.uid b/project/guild_mark_uploader_system.gd.uid deleted file mode 100644 index ac738af7..00000000 --- a/project/guild_mark_uploader_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dkhvtgc7u2vet diff --git a/project/guild_skill_system.gd b/project/guild_skill_system.gd deleted file mode 100644 index cb572548..00000000 --- a/project/guild_skill_system.gd +++ /dev/null @@ -1,264 +0,0 @@ -# guild_skill_system.gd —— Metin2 40250 公会等级与 7 大神龙技能树系统 1:1 -# 对照 40250 服务端 guild.cpp, guild_manager.cpp, PythonGuild.cpp, skilldesc.txt:23-29 -class_name GuildSkillSystem -extends RefCounted - -signal guild_level_up(new_level: int, skill_points: int) -signal guild_skill_cast(skill_id: int, duration: float) - -const MAX_GUILD_LEVEL := 20 -const MAX_SKILL_LEVEL := 7 - -# 7 官方公会技能 (skilldesc.txt:23-29) -const SKILL_DRAGON_EYES := 151 # 龙神之眼: 被动增加最大神龙精力 -const SKILL_DRAGON_BLOOD := 152 # 龙神之血: 最大 HP 提升 -const SKILL_DRAGON_BLESSING := 153# 龙神庇护: 最大 SP 提升 -const SKILL_HOLY_ARMOR := 154 # 圣灵之甲: 防御力百分比提升 -const SKILL_ACCELERATE := 155 # 疾风步: 攻击与移动速度提升 -const SKILL_DRAGON_RAGE := 156 # 龙神之怒: 致命一击几率提升 -const SKILL_CASTING_AID := 157 # 咏唱神助: 施法提速 / 技能冷却缩减 - -const GUILD_SKILLS: Dictionary = { - SKILL_DRAGON_EYES: { - "name": "龙神之眼", - "is_passive": true, - "spirit_cost": 0, - "cooldown": 0.0, - "duration": 0.0 - }, - SKILL_DRAGON_BLOOD: { - "name": "龙神之血", - "is_passive": false, - "spirit_cost": 20, - "cooldown": 60.0, - "duration": 300.0 # 5 分钟 - }, - SKILL_DRAGON_BLESSING: { - "name": "龙神庇护", - "is_passive": false, - "spirit_cost": 20, - "cooldown": 60.0, - "duration": 300.0 - }, - SKILL_HOLY_ARMOR: { - "name": "圣灵之甲", - "is_passive": false, - "spirit_cost": 25, - "cooldown": 60.0, - "duration": 300.0 - }, - SKILL_ACCELERATE: { - "name": "疾风步", - "is_passive": false, - "spirit_cost": 25, - "cooldown": 60.0, - "duration": 300.0 - }, - SKILL_DRAGON_RAGE: { - "name": "龙神之怒", - "is_passive": false, - "spirit_cost": 30, - "cooldown": 60.0, - "duration": 300.0 - }, - SKILL_CASTING_AID: { - "name": "咏唱神助", - "is_passive": false, - "spirit_cost": 30, - "cooldown": 60.0, - "duration": 300.0 - } -} - -# 公会基础属性 -var guild_level: int = 1 -var guild_exp: int = 0 -var skill_points: int = 0 -var max_spirit: int = 100 -var current_spirit: int = 100 - -# 技能加点等级: skill_id -> level (0~7) -var skill_levels: Dictionary = { - SKILL_DRAGON_EYES: 0, - SKILL_DRAGON_BLOOD: 0, - SKILL_DRAGON_BLESSING: 0, - SKILL_HOLY_ARMOR: 0, - SKILL_ACCELERATE: 0, - SKILL_DRAGON_RAGE: 0, - SKILL_CASTING_AID: 0 -} - -# 技能冷却与生效增益 -var skill_cooldowns: Dictionary = {} -var active_buffs: Dictionary = {} # skill_id -> { duration, value } - -# 计算升级所需经验 (guild.cpp) -static func get_exp_needed_for_level(lvl: int) -> int: - return lvl * 50000 - -# 捐献公会经验 -func donate_exp(amount: int) -> Dictionary: - if guild_level >= MAX_GUILD_LEVEL: - return {"ok": false, "reason": "MAX_LEVEL", "msg": "公会已达到最高 20 级!"} - - guild_exp += amount - var leveled_up := false - var levels_gained := 0 - - while guild_level < MAX_GUILD_LEVEL: - var needed = get_exp_needed_for_level(guild_level) - if guild_exp >= needed: - guild_exp -= needed - guild_level += 1 - skill_points += 1 - levels_gained += 1 - leveled_up = true - else: - break - - if leveled_up: - _recalculate_spirit() - guild_level_up.emit(guild_level, skill_points) - - return { - "ok": true, - "guild_level": guild_level, - "guild_exp": guild_exp, - "needed_exp": get_exp_needed_for_level(guild_level), - "skill_points": skill_points, - "leveled_up": leveled_up - } - -# 升级公会技能 -func level_up_skill(skill_id: int) -> Dictionary: - if not GUILD_SKILLS.has(skill_id): - return {"ok": false, "reason": "INVALID_SKILL"} - if skill_points <= 0: - return {"ok": false, "reason": "NO_SKILL_POINTS", "msg": "可用公会技能点不足!"} - - var cur_lvl: int = int(skill_levels.get(skill_id, 0)) - if cur_lvl >= MAX_SKILL_LEVEL: - return {"ok": false, "reason": "MAX_SKILL_LEVEL", "msg": "该公会技能已满级!"} - - skill_levels[skill_id] = cur_lvl + 1 - skill_points -= 1 - _recalculate_spirit() - - return { - "ok": true, - "skill_id": skill_id, - "new_level": skill_levels[skill_id], - "remaining_points": skill_points - } - -func _recalculate_spirit() -> void: - # 151 龙神之眼扩充神龙精力 - var eyes_lvl: int = int(skill_levels.get(SKILL_DRAGON_EYES, 0)) - var bonus_sp: int = int(round(float(eyes_lvl) / float(MAX_SKILL_LEVEL) * 100.0)) - max_spirit = 100 + bonus_sp - current_spirit = mini(current_spirit, max_spirit) - -# 施放公会技能 -func cast_guild_skill(skill_id: int) -> Dictionary: - if not GUILD_SKILLS.has(skill_id): - return {"ok": false, "reason": "INVALID_SKILL"} - - var cfg: Dictionary = GUILD_SKILLS[skill_id] - if bool(cfg["is_passive"]): - return {"ok": false, "reason": "PASSIVE_SKILL", "msg": "被动技能无需主动施放!"} - - var lvl: int = int(skill_levels.get(skill_id, 0)) - if lvl <= 0: - return {"ok": false, "reason": "NOT_LEARNED", "msg": "尚未学习该公会技能!"} - - # 检查冷却 - if float(skill_cooldowns.get(skill_id, 0.0)) > 0.0: - return {"ok": false, "reason": "COOLDOWN_ACTIVE", "remaining": skill_cooldowns[skill_id]} - - # 检查精力消耗 - var cost: int = int(cfg["spirit_cost"]) - if current_spirit < cost: - return {"ok": false, "reason": "NOT_ENOUGH_SPIRIT", "msg": "公会神龙精力不足!"} - - # 扣除精力并进入冷却 - current_spirit -= cost - skill_cooldowns[skill_id] = float(cfg["cooldown"]) - - # 计算技能数值 (对齐 skilldesc.txt: k = lvl / 7.0) - var k: float = float(lvl) / float(MAX_SKILL_LEVEL) - var dur: float = float(cfg["duration"]) - var val: float = 0.0 - - match skill_id: - SKILL_DRAGON_BLOOD: val = k * 20.0 # Max HP +20% - SKILL_DRAGON_BLESSING: val = k * 20.0 # Max SP +20% - SKILL_HOLY_ARMOR: val = k * 10.0 # DEF +10% - SKILL_ACCELERATE: val = k * 30.0 # Atk & Motion Speed +30 - SKILL_DRAGON_RAGE: val = k * 50.0 # Crit +50% - SKILL_CASTING_AID: val = k * 50.0 # Cooldown reduction +50% - - active_buffs[skill_id] = { - "name": cfg["name"], - "value": val, - "duration": dur, - "total_duration": dur - } - - guild_skill_cast.emit(skill_id, dur) - - return { - "ok": true, - "skill_id": skill_id, - "name": cfg["name"], - "value": val, - "duration": dur, - "current_spirit": current_spirit - } - -# 逐帧更新冷却与生效增益 -func update(dt: float) -> void: - # 更新技能冷却 - for sid in skill_cooldowns.keys(): - skill_cooldowns[sid] = maxf(0.0, float(skill_cooldowns[sid]) - dt) - if float(skill_cooldowns[sid]) <= 0.0: - skill_cooldowns.erase(sid) - - # 更新增益持续时间 - var expired: Array[int] = [] - for sid in active_buffs.keys(): - var b: Dictionary = active_buffs[sid] - b["duration"] = maxf(0.0, float(b["duration"]) - dt) - if float(b["duration"]) <= 0.0: - expired.append(sid) - - for sid in expired: - active_buffs.erase(sid) - - # 恢复精力 (每秒恢复 0.5) - current_spirit = mini(max_spirit, current_spirit + int(round(0.5 * dt))) - -# 查询当前激活的所有公会增益 -func get_active_bonuses() -> Dictionary: - var res: Dictionary = { - "max_hp_pct": 0.0, - "max_sp_pct": 0.0, - "def_pct": 0.0, - "speed_bonus": 0.0, - "critical_pct": 0.0, - "casting_aid_pct": 0.0 - } - - if active_buffs.has(SKILL_DRAGON_BLOOD): - res["max_hp_pct"] = float(active_buffs[SKILL_DRAGON_BLOOD]["value"]) - if active_buffs.has(SKILL_DRAGON_BLESSING): - res["max_sp_pct"] = float(active_buffs[SKILL_DRAGON_BLESSING]["value"]) - if active_buffs.has(SKILL_HOLY_ARMOR): - res["def_pct"] = float(active_buffs[SKILL_HOLY_ARMOR]["value"]) - if active_buffs.has(SKILL_ACCELERATE): - res["speed_bonus"] = float(active_buffs[SKILL_ACCELERATE]["value"]) - if active_buffs.has(SKILL_DRAGON_RAGE): - res["critical_pct"] = float(active_buffs[SKILL_DRAGON_RAGE]["value"]) - if active_buffs.has(SKILL_CASTING_AID): - res["casting_aid_pct"] = float(active_buffs[SKILL_CASTING_AID]["value"]) - - return res diff --git a/project/guild_skill_system.gd.uid b/project/guild_skill_system.gd.uid deleted file mode 100644 index 2e025445..00000000 --- a/project/guild_skill_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dmqf2n3hr5phn diff --git a/project/guild_war_observer_arena_system.gd b/project/guild_war_observer_arena_system.gd deleted file mode 100644 index 3283945f..00000000 --- a/project/guild_war_observer_arena_system.gd +++ /dev/null @@ -1,188 +0,0 @@ -# guild_war_observer_arena_system.gd —— Metin2 40250 官方公会战备观战与武斗擂台传送系统 1:1 -# 100% 对照 40250 服务端 war_map.cpp, questlua_pc.cpp (warp_to_guild_war_observer_position), guild_war.cpp -class_name GuildWarObserverArenaSystem -extends RefCounted - -signal observer_entered(player_id: int, war_id: int, observer_pos: Vector2) -signal observer_left(player_id: int, return_map: String, return_pos: Vector2) -signal war_ended_evacuated(war_id: int, observer_count: int) -signal observer_error(reason: String) - -# 40250 官方公会战竞技场地标与地图常数 -const MAP_GUILD_ARENA := 110 # 公会战擂台竞技场地图 -const MAP_GUILD_FLAG_WAR := 111 # 公会夺旗战场地图 -const OBSERVER_PLATFORM_POS := Vector2(2000.0, 2000.0) # 观战席高台坐标 (Start Location Index 2) - -# 主城公会守卫 NPC (11001, 11003, 11005) -const NPC_TOWN_GUARD_SHINSOO := 11001 -const NPC_TOWN_GUARD_CHUNJO := 11003 -const NPC_TOWN_GUARD_JINNO := 11005 - -# 正在进行的公会战清单: war_id -> { ... } -var _active_wars: Dictionary = {} -# 观战者档案: player_id -> { "war_id": int, "saved_map": String, "saved_pos": Vector2, "is_observer": bool } -var _observers: Dictionary = {} -var _next_war_id: int = 1 - -# ========================================== -# 1. 公会战状态登记与告示 (war_map.cpp) -# ========================================== -func register_active_war( - guild1_id: int, - guild1_name: String, - guild2_id: int, - guild2_name: String, - war_type: int = 1, # 1 为 Arena 擂台赛 - target_score: int = 50 -) -> int: - var wid = _next_war_id - _next_war_id += 1 - - _active_wars[wid] = { - "war_id": wid, - "guild1_id": guild1_id, - "guild1_name": guild1_name, - "guild2_id": guild2_id, - "guild2_name": guild2_name, - "war_type": war_type, - "target_score": target_score, - "score1": 0, - "score2": 0, - "map_index": MAP_GUILD_ARENA if war_type == 1 else MAP_GUILD_FLAG_WAR, - "is_ended": false, - "winner_id": 0 - } - return wid - -# 主城守卫公告栏:获取当前全部可观战的公会对决列表 -func get_active_wars_for_display() -> Array[Dictionary]: - var list: Array[Dictionary] = [] - for wid in _active_wars.keys(): - var w = _active_wars[wid] - if not bool(w.get("is_ended", false)): - list.append(w.duplicate(true)) - return list - -# 更新战局比分 -func update_war_score(war_id: int, score1: int, score2: int) -> bool: - if not _active_wars.has(war_id): - return false - var w = _active_wars[war_id] - w["score1"] = score1 - w["score2"] = score2 - if score1 >= int(w["target_score"]): - end_war(war_id, int(w["guild1_id"])) - elif score2 >= int(w["target_score"]): - end_war(war_id, int(w["guild2_id"])) - return true - -# ========================================== -# 2. 传送入场观战 (pc_warp_to_guild_war_observer_position) -# ========================================== -func enter_as_observer( - player_id: int, - war_id: int, - current_map: String, - current_pos: Vector2 -) -> Dictionary: - if not _active_wars.has(war_id) or bool(_active_wars[war_id].get("is_ended", false)): - observer_error.emit("WAR_NOT_ACTIVE") - return {"ok": false, "reason": "WAR_NOT_ACTIVE", "msg": "该场公会战尚未开启或已经落下帷幕!"} - - if is_player_observing(player_id): - observer_error.emit("ALREADY_OBSERVING") - return {"ok": false, "reason": "ALREADY_OBSERVING", "msg": "你已经在观战席中!"} - - var war = _active_wars[war_id] - # 保存玩家原始位置 (ch->SaveExitLocation) - _observers[player_id] = { - "war_id": war_id, - "saved_map": current_map, - "saved_pos": current_pos, - "is_observer": true, - "can_attack": false, # 幽灵模式禁止攻击 - "can_be_attacked": false # 幽灵模式免疫任何伤害 - } - - observer_entered.emit(player_id, war_id, OBSERVER_PLATFORM_POS) - return { - "ok": true, - "war_id": war_id, - "map_index": war["map_index"], - "observer_pos": OBSERVER_PLATFORM_POS, - "is_observer_mode": true, - "msg": "穿过虚空之门,你抵达了公会战竞技场高台观战席!已开启隐匿观摩形态。" - } - -# 离开观战席重返主城 -func leave_observer_mode(player_id: int) -> Dictionary: - if not is_player_observing(player_id): - observer_error.emit("NOT_OBSERVING") - return {"ok": false, "reason": "NOT_OBSERVING", "msg": "你当前并不在观战模式中。"} - - var obs = _observers[player_id] - var return_map = str(obs["saved_map"]) - var return_pos = Vector2(obs["saved_pos"].x, obs["saved_pos"].y) - - _observers.erase(player_id) - observer_left.emit(player_id, return_map, return_pos) - - return { - "ok": true, - "return_map": return_map, - "return_pos": return_pos, - "msg": "你退出了观战高台,平安返回了主城集结地。" - } - -func is_player_observing(player_id: int) -> bool: - return _observers.has(player_id) and bool(_observers[player_id].get("is_observer", false)) - -func can_player_attack_or_be_attacked(player_id: int) -> bool: - # 若处于观战模式,彻底屏蔽战斗互攻判定 - return not is_player_observing(player_id) - -# ========================================== -# 3. 终局结算与观战席全员疏散 (war_map.cpp) -# ========================================== -func end_war(war_id: int, winner_guild_id: int) -> void: - if not _active_wars.has(war_id): - return - - var w = _active_wars[war_id] - w["is_ended"] = true - w["winner_id"] = winner_guild_id - - # 遣返该场战斗的所有观战者 - var evacuated_count := 0 - for pid in _observers.keys().duplicate(): - if int(_observers[pid].get("war_id", 0)) == war_id: - var obs = _observers[pid] - observer_left.emit(pid, obs["saved_map"], obs["saved_pos"]) - _observers.erase(pid) - evacuated_count += 1 - - war_ended_evacuated.emit(war_id, evacuated_count) - -# 序列化与反序列化 -func serialize() -> Dictionary: - var obs_copy = {} - for pid in _observers: - var item = _observers[pid].duplicate() - item["saved_pos"] = {"x": item["saved_pos"].x, "y": item["saved_pos"].y} - obs_copy[pid] = item - return { - "active_wars": _active_wars.duplicate(true), - "observers": obs_copy, - "next_war_id": _next_war_id - } - -func deserialize(data: Dictionary) -> void: - _active_wars = data.get("active_wars", {}).duplicate(true) - _next_war_id = int(data.get("next_war_id", 1)) - _observers.clear() - var raw_obs = data.get("observers", {}) - for pid in raw_obs: - var item = raw_obs[pid].duplicate() - var p_dict = item.get("saved_pos", {"x": 0.0, "y": 0.0}) - item["saved_pos"] = Vector2(float(p_dict.get("x", 0.0)), float(p_dict.get("y", 0.0))) - _observers[int(pid)] = item diff --git a/project/guild_war_observer_arena_system.gd.uid b/project/guild_war_observer_arena_system.gd.uid deleted file mode 100644 index c6ff39e4..00000000 --- a/project/guild_war_observer_arena_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b17l4jfxfcmlw diff --git a/project/guild_war_system.gd b/project/guild_war_system.gd deleted file mode 100644 index 97ac9404..00000000 --- a/project/guild_war_system.gd +++ /dev/null @@ -1,186 +0,0 @@ -# guild_war_system.gd —— Metin2 40250 官方公会战备宣战与积分决斗裁定系统 1:1 -# 对照 40250 服务端 guild_war.cpp, guild_manager.cpp, guild.h -class_name GuildWarSystem -extends RefCounted - -signal war_declared(opp_guild_name: String, war_type: int, target_score: int) -signal war_started(war_type: int, duration: float) -signal score_updated(score_a: int, score_b: int) -signal war_ended(winner_side: String, score_a: int, score_b: int, reward: Dictionary) - -# 40250 官方公会战模式类型 -const WAR_TYPE_FIELD := 0 # 野外遭遇战 (Field War) -const WAR_TYPE_ARENA := 1 # 竞技场擂台战 (Arena War, Map 110/111) -const WAR_TYPE_FLAG := 2 # 夺旗战 (Flag War) - -# 公会战状态机 -const STATE_NONE := 0 -const STATE_DECLARED := 1 # 宣战中 -const STATE_PREPARING := 2 # 备战中 -const STATE_FIGHTING := 3 # 激战中 -const STATE_FINISHED := 4 # 战役结束 - -# 单机竞争公会预设库 -const RIVAL_GUILDS: Dictionary = { - 101: {"id": 101, "name": "暗影刺客团", "master": "夜刃", "level": 20, "ladder_points": 1200}, - 102: {"id": 102, "name": "赤血狂战盟", "master": "狂屠", "level": 20, "ladder_points": 1350}, - 103: {"id": 103, "name": "青云修罗殿", "master": "冥火", "level": 20, "ladder_points": 1400} -} - -var war_state: int = STATE_NONE -var active_war_type: int = WAR_TYPE_ARENA -var active_opp_guild: Dictionary = {} -var target_score: int = 50 # 目标获胜积分 (50分/100分) -var time_remaining: float = 1800.0 # 30 分钟 (1800 秒) -var total_duration: float = 1800.0 - -var score_a: int = 0 # 玩家公会得分 -var score_b: int = 0 # 敌方公会得分 - -var player_guild_ladder: int = 1000 # 玩家公会天梯积分 -var total_wars_won: int = 0 - -# 发起公会宣战 (40250 CGuild::GuildWarPacket / UnderDeclaration) -func declare_war(opp_guild_id: int, war_type: int = WAR_TYPE_ARENA, win_score: int = 50, duration: float = 1800.0) -> Dictionary: - if war_state == STATE_FIGHTING or war_state == STATE_PREPARING: - return {"ok": false, "reason": "ALREADY_IN_WAR", "msg": "当前公会已处于战役交锋中!"} - - if not RIVAL_GUILDS.has(opp_guild_id): - return {"ok": false, "reason": "INVALID_OPP_GUILD", "msg": "未知的敌对公会!"} - - active_opp_guild = RIVAL_GUILDS[opp_guild_id].duplicate(true) - active_war_type = war_type - target_score = win_score - time_remaining = duration - total_duration = duration - score_a = 0 - score_b = 0 - - war_state = STATE_DECLARED - - war_declared.emit(active_opp_guild["name"], active_war_type, target_score) - - return { - "ok": true, - "opp_name": active_opp_guild["name"], - "war_type": active_war_type, - "target_score": target_score, - "msg": "战书已下达!向【%s】发起公会宣战。" % active_opp_guild["name"] - } - -# 响应接受战书开启战场 -func start_war() -> Dictionary: - if war_state != STATE_DECLARED: - return {"ok": false, "reason": "NOT_IN_DECLARED_STATE"} - - war_state = STATE_FIGHTING - war_started.emit(active_war_type, total_duration) - - return { - "ok": true, - "war_type": active_war_type, - "duration": total_duration, - "msg": "战役正式打响!双方公会成员进入战场拼杀。" - } - -# 记录战场击杀或占旗得分 -func record_score(side: String, points: int = 1) -> Dictionary: - if war_state != STATE_FIGHTING: - return {"ok": false, "reason": "WAR_NOT_ACTIVE"} - - if side == "A": - score_a += points - elif side == "B": - score_b += points - - score_updated.emit(score_a, score_b) - - # 检查是否率先达到目标胜分 - if score_a >= target_score or score_b >= target_score: - var winner := "A" if score_a >= target_score else "B" - return end_war(winner) - - return { - "ok": true, - "score_a": score_a, - "score_b": score_b, - "target_score": target_score - } - -# 结束战役与结算奖励 -func end_war(winner: String) -> Dictionary: - war_state = STATE_FINISHED - - var reward: Dictionary = {} - if winner == "A": - total_wars_won += 1 - player_guild_ladder += 30 - reward = { - "guild_exp": 5000, - "guild_gold": 1000000, - "ladder_change": 30, - "new_ladder": player_guild_ladder - } - elif winner == "B": - player_guild_ladder = maxi(0, player_guild_ladder - 20) - reward = { - "guild_exp": 1000, - "guild_gold": 200000, - "ladder_change": -20, - "new_ladder": player_guild_ladder - } - else: - reward = { - "guild_exp": 2000, - "guild_gold": 500000, - "ladder_change": 0, - "new_ladder": player_guild_ladder - } - - war_ended.emit(winner, score_a, score_b, reward) - - var msg_title := "大获全胜" if winner == "A" else ("虽败犹荣" if winner == "B" else "战局握手言和") - return { - "ok": true, - "winner": winner, - "score_a": score_a, - "score_b": score_b, - "reward": reward, - "msg": "公会战结束【%s】!比分:%d - %d。" % [msg_title, score_a, score_b] - } - -# 战役时钟心跳 -func update(delta: float) -> void: - if war_state != STATE_FIGHTING: - return - time_remaining -= delta - if time_remaining <= 0.0: - # 时间耗尽,按现有比分决出 - var winner := "A" if score_a > score_b else ("B" if score_b > score_a else "DRAW") - end_war(winner) - -# 序列化 -func serialize() -> Dictionary: - return { - "war_state": war_state, - "active_war_type": active_war_type, - "active_opp_guild": active_opp_guild.duplicate(true), - "target_score": target_score, - "time_remaining": time_remaining, - "score_a": score_a, - "score_b": score_b, - "player_guild_ladder": player_guild_ladder, - "total_wars_won": total_wars_won - } - -# 反序列化 -func deserialize(data: Dictionary) -> void: - war_state = int(data.get("war_state", STATE_NONE)) - active_war_type = int(data.get("active_war_type", WAR_TYPE_ARENA)) - active_opp_guild = data.get("active_opp_guild", {}).duplicate(true) - target_score = int(data.get("target_score", 50)) - time_remaining = float(data.get("time_remaining", 1800.0)) - score_a = int(data.get("score_a", 0)) - score_b = int(data.get("score_b", 0)) - player_guild_ladder = int(data.get("player_guild_ladder", 1000)) - total_wars_won = int(data.get("total_wars_won", 0)) diff --git a/project/guild_war_system.gd.uid b/project/guild_war_system.gd.uid deleted file mode 100644 index bc8239f7..00000000 --- a/project/guild_war_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dypy1i2skjo2w diff --git a/project/hair_dye_system.gd b/project/hair_dye_system.gd deleted file mode 100644 index 94d81589..00000000 --- a/project/hair_dye_system.gd +++ /dev/null @@ -1,105 +0,0 @@ -# hair_dye_system.gd —— Metin2 40250 官方发型染色与漂白重塑系统 1:1 -# 对照 40250 服务端 char_item.cpp:3540-3580 -class_name HairDyeSystem -extends RefCounted - -signal hair_dyed(color_id: int, color_name: String) -signal hair_bleached() - -# 40250 官方染发道具 Vnum -const VNUM_BLEACH := 70201 # 漂白水 (重置发色为 0, 清空冷却) -const VNUM_WHITE_DYE := 70202 # 染发剂(白) -> color 1 -const VNUM_BLONDE_DYE := 70203 # 染发剂(金) -> color 2 -const VNUM_RED_DYE := 70204 # 染发剂(红) -> color 3 -const VNUM_BROWN_DYE := 70205 # 染发剂(棕) -> color 4 -const VNUM_BLACK_DYE := 70206 # 染发剂(黑) -> color 5 - -# 颜色映射字典 -const HAIR_COLORS: Dictionary = { - 0: "原色黑发(Default)", - 1: "雪原纯白(White)", - 2: "璀璨金黄(Blonde)", - 3: "烈火赤红(Red)", - 4: "深栗浅棕(Brown)", - 5: "曜石纯黑(Black)" -} - -var current_hair_color: int = 0 -var current_hair_style_id: int = 0 # 0 为基础原生发型,>= 1001 为时装发型 -var last_dye_level: int = 0 # 记录上次染发时的角色等级 - -# 获取颜色名称 -static func get_color_name(color_id: int) -> String: - return HAIR_COLORS.get(color_id, "未知发色") - -# 执行染发或漂白 (40250 char_item.cpp:3540-3580) -func dye_hair(inventory: Array, slot_index: int, player_level: int) -> Dictionary: - if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null: - return {"ok": false, "reason": "INVALID_SLOT"} - - var item: Dictionary = inventory[slot_index] - var item_vnum := int(item.get("vnum", 0)) - if item_vnum < VNUM_BLEACH or item_vnum > VNUM_BLACK_DYE: - return {"ok": false, "reason": "NOT_A_HAIR_DYE", "msg": "此物品不是染发剂或漂白水!"} - - # 40250 官方规则:独立时装发型 (PART_HAIR >= 1001) 无法进行普通染色 - if current_hair_style_id >= 1001: - return {"ok": false, "reason": "CANNOT_DYE_COSTUME_HAIR", "msg": "当前穿戴的特殊时装发型无法进行原生发色染色!"} - - # 40250 官方冷却规则: - # last_dye_level == 0 || last_dye_level + 3 <= GetLevel() || item->GetVnum() == 70201 - var is_bleach: bool = (item_vnum == VNUM_BLEACH) - if not is_bleach and last_dye_level > 0 and (last_dye_level + 3 > player_level): - var req_lv := last_dye_level + 3 - return { - "ok": false, - "reason": "LEVEL_COOLDOWN_ACTIVE", - "req_level": req_lv, - "msg": "染发药水药效尚未散去!角色需达到 %d 级方可再次染色(漂白水除外)。" % req_lv - } - - # 消耗 1 瓶染发剂 - var cnt := int(item.get("count", 1)) - if cnt > 1: - item["count"] = cnt - 1 - else: - inventory[slot_index] = null - - if is_bleach: - # 漂白水重置发色为 0,并清空染色等级限制 - current_hair_color = 0 - last_dye_level = 0 - hair_bleached.emit() - return { - "ok": true, - "is_bleach": true, - "hair_color": 0, - "color_name": get_color_name(0), - "msg": "使用漂白水褪色成功!发色已恢复经典天然原色。" - } - else: - current_hair_color = item_vnum - VNUM_BLEACH - last_dye_level = player_level - hair_dyed.emit(current_hair_color, get_color_name(current_hair_color)) - return { - "ok": true, - "is_bleach": false, - "hair_color": current_hair_color, - "color_name": get_color_name(current_hair_color), - "last_dye_level": last_dye_level, - "msg": "染发成功!角色秀发已焕新为【%s】。" % get_color_name(current_hair_color) - } - -# 序列化 -func serialize() -> Dictionary: - return { - "current_hair_color": current_hair_color, - "current_hair_style_id": current_hair_style_id, - "last_dye_level": last_dye_level - } - -# 反序列化 -func deserialize(data: Dictionary) -> void: - current_hair_color = int(data.get("current_hair_color", 0)) - current_hair_style_id = int(data.get("current_hair_style_id", 0)) - last_dye_level = int(data.get("last_dye_level", 0)) diff --git a/project/hair_dye_system.gd.uid b/project/hair_dye_system.gd.uid deleted file mode 100644 index 5318a0b0..00000000 --- a/project/hair_dye_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cugg84qsxxqmy diff --git a/project/herbal_dew_system.gd b/project/herbal_dew_system.gd deleted file mode 100644 index 87eb73a8..00000000 --- a/project/herbal_dew_system.gd +++ /dev/null @@ -1,228 +0,0 @@ -# herbal_dew_system.gd —— Metin2 40250 白高草药炼丹酿造与 6 色神圣露水系统 1:1 -# 对照 40250 服务端 char_item.cpp, blend.txt, potion.quest 及 NPC 20018 Baek-Go -class_name HerbalDewSystem -extends RefCounted - -const NPC_BAEK_GO := 20018 -const ITEM_EMPTY_BOTTLE := 70030 - -# 6 色神圣露水定义 (50821~50826) -const DEW_TABLE: Dictionary = { - 50821: { - "name": "绿露水", - "color": "green", - "stat": "magic_defense", - "stat_desc": "魔法防御力", - "min_val": 30, - "max_val": 70, - "duration": 600.0, # 10 分钟 - "water_vnum": 50721 - }, - 50822: { - "name": "红露水", - "color": "red", - "stat": "critical_pct", - "stat_desc": "致命一击几率", - "min_val": 8, - "max_val": 20, - "duration": 600.0, - "water_vnum": 50722 - }, - 50823: { - "name": "蓝露水", - "color": "blue", - "stat": "att_grade", - "stat_desc": "物理攻击力", - "min_val": 50, - "max_val": 120, - "duration": 600.0, - "water_vnum": 50723 - }, - 50824: { - "name": "白露水", - "color": "white", - "stat": "def_grade", - "stat_desc": "物理防御力", - "min_val": 40, - "max_val": 100, - "duration": 600.0, - "water_vnum": 50724 - }, - 50825: { - "name": "粉露水", - "color": "pink", - "stat": "penetrate_pct", - "stat_desc": "穿透一击几率", - "min_val": 8, - "max_val": 20, - "duration": 600.0, - "water_vnum": 50725 - }, - 50826: { - "name": "黄露水", - "color": "yellow", - "stat": "att_speed", - "stat_desc": "攻击速度", - "min_val": 2, - "max_val": 8, - "duration": 600.0, - "water_vnum": 50726 - } -} - -# 草药提炼对应表 (herb_vnum -> water_vnum) -const HERB_TO_WATER: Dictionary = { - 50701: 50721, # 桃花 -> Sim-Water - 50702: 50722, # 风铃草 -> Dok-Water - 50703: 50723, # 柿子花 -> Bo-Water - 50704: 50724, # 高山石斛 -> Young-Water - 50705: 50725, # 紫丁香 -> Zin-Water - 50708: 50726 # 桑椹 -> Sam-Water -} - -# 正在生效的露水增益: dew_vnum -> { name, stat, value, duration } -var active_dews: Dictionary = {} - -# 1. 在白高处提取草药药水 (原料草药 -> 中间提取液) -static func brew_water(inventory: Array, herb_vnum: int) -> Dictionary: - if not HERB_TO_WATER.has(herb_vnum): - return {"ok": false, "reason": "INVALID_HERB", "msg": "此草药无法用于提炼!"} - - var target_water: int = int(HERB_TO_WATER[herb_vnum]) - - # 消耗草药 - var herb_slot := -1 - for i in range(inventory.size()): - var it = inventory[i] - if it != null and int(it.get("vnum", 0)) == herb_vnum: - herb_slot = i - break - - if herb_slot == -1: - return {"ok": false, "reason": "MISSING_HERB", "msg": "背包中缺少所需草药!"} - - inventory[herb_slot] = null - - # 寻找背包空位生成药水提取液 - var empty_slot := -1 - for i in range(inventory.size()): - if inventory[i] == null: - empty_slot = i - break - - if empty_slot == -1: - return {"ok": false, "reason": "INVENTORY_FULL", "msg": "背包已满!"} - - inventory[empty_slot] = {"vnum": target_water, "count": 1} - return {"ok": true, "water_vnum": target_water, "slot": empty_slot} - -# 2. 在白高处合成露水 (药水提取液 + 空瓶 70030 -> 神圣露水) -static func brew_dew(inventory: Array, dew_vnum: int, force_max_roll: bool = false) -> Dictionary: - if not DEW_TABLE.has(dew_vnum): - return {"ok": false, "reason": "INVALID_DEW"} - - var cfg: Dictionary = DEW_TABLE[dew_vnum] - var req_water: int = int(cfg["water_vnum"]) - - var water_slot := -1 - var bottle_slot := -1 - - for i in range(inventory.size()): - var it = inventory[i] - if it != null: - var v: int = int(it.get("vnum", 0)) - if v == req_water and water_slot == -1: - water_slot = i - elif v == ITEM_EMPTY_BOTTLE and bottle_slot == -1: - bottle_slot = i - - if water_slot == -1: - return {"ok": false, "reason": "MISSING_WATER", "msg": "缺少对应的草药提取水剂!"} - if bottle_slot == -1: - return {"ok": false, "reason": "MISSING_BOTTLE", "msg": "缺少用于盛装露水的空药瓶 (70030)!"} - - # 扣除水剂与空药瓶 - inventory[water_slot] = null - inventory[bottle_slot] = null - - # 抽取露水品质数值 (min_val ~ max_val) - var min_v: int = int(cfg["min_val"]) - var max_v: int = int(cfg["max_val"]) - var roll_val: int = max_v if force_max_roll else randi_range(min_v, max_v) - - var dew_item: Dictionary = { - "vnum": dew_vnum, - "name": cfg["name"], - "value": roll_val, - "duration": float(cfg["duration"]) - } - - # 存入背包 - var stored_slot := -1 - for i in range(inventory.size()): - if inventory[i] == null: - inventory[i] = dew_item - stored_slot = i - break - - return { - "ok": true, - "dew_vnum": dew_vnum, - "name": cfg["name"], - "value": roll_val, - "slot": stored_slot, - "stat_desc": cfg["stat_desc"], - "msg": "成功酿造出【%s】!品质增幅:%s +%d!" % [cfg["name"], cfg["stat_desc"], roll_val] - } - -# 3. 饮用露水获得增益 -func drink_dew(item: Dictionary) -> Dictionary: - var vnum: int = int(item.get("vnum", 0)) - if not DEW_TABLE.has(vnum): - return {"ok": false, "reason": "NOT_A_DEW"} - - var cfg: Dictionary = DEW_TABLE[vnum] - var roll_val: int = int(item.get("value", cfg["min_val"])) - - active_dews[vnum] = { - "name": cfg["name"], - "stat": cfg["stat"], - "value": roll_val, - "duration": float(cfg["duration"]), - "total_duration": float(cfg["duration"]) - } - - return { - "ok": true, - "dew_vnum": vnum, - "name": cfg["name"], - "stat": cfg["stat"], - "value": roll_val, - "duration": float(cfg["duration"]) - } - -# 逐帧更新倒计时 -func update(dt: float) -> Array[int]: - var expired: Array[int] = [] - for vnum in active_dews.keys(): - var data: Dictionary = active_dews[vnum] - data["duration"] = maxf(0.0, float(data["duration"]) - dt) - if float(data["duration"]) <= 0.0: - expired.append(vnum) - - for vnum in expired: - active_dews.erase(vnum) - - return expired - -# 查询某项属性在当前露水中的总加成 -func get_stat_bonus(stat_name: String) -> int: - var total := 0 - for vnum in active_dews: - var data: Dictionary = active_dews[vnum] - if String(data.get("stat", "")) == stat_name: - total += int(data.get("value", 0)) - return total - -func has_dew(vnum: int) -> bool: - return active_dews.has(vnum) diff --git a/project/herbal_dew_system.gd.uid b/project/herbal_dew_system.gd.uid deleted file mode 100644 index c37b769b..00000000 --- a/project/herbal_dew_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d3w5osfswkpk diff --git a/project/horse_combat_system.gd b/project/horse_combat_system.gd deleted file mode 100644 index eae00cd1..00000000 --- a/project/horse_combat_system.gd +++ /dev/null @@ -1,207 +0,0 @@ -# horse_combat_system.gd —— Metin2 40250 战马骑乘战斗与 4 大战马专属技能 1:1 -# 对照 40250 服务端 char_skill.cpp, char_horse.cpp, skilldesc.txt:137-140 -class_name HorseCombatSystem -extends RefCounted - -signal horse_attack_performed(target_count: int, total_dmg: int) -signal horse_skill_used(skill_id: int, skill_name: String, cost_sp: int) -signal horse_stamina_changed(current: float, max_val: float) - -const HORSE_TIER_BEGINNER := 1 # 初级马 (Lv 1-10, 仅代步) -const HORSE_TIER_COMBAT := 2 # 战斗马 (Lv 11-20, 可马上挥砍) -const HORSE_TIER_MILITARY := 3 # 军马 (Lv 21-30, 可马上挥砍并施展 4 大马术技能) - -const MOUNTED_ATTACK_RANGE := 5.0 -const FOOT_ATTACK_RANGE := 3.0 - -const ITEM_FOOD_CARROT := 50054 # 胡萝卜 (战斗马饲料) -const ITEM_FOOD_GINSENG := 50058 # 人参 (军马饲料) - -const HORSE_SKILLS: Dictionary = { - 137: { - "name": "战马砍击", - "sp_cost": 30, - "cooldown": 15.0, - "dmg_mult": 2.5, - "effect": "KNOCKBACK", - "range": 5.0 - }, - 138: { - "name": "战马突刺", - "sp_cost": 35, - "cooldown": 20.0, - "dmg_mult": 2.0, - "effect": "STUMBLE", - "range": 8.0 - }, - 139: { - "name": "战马震击", - "sp_cost": 40, - "cooldown": 25.0, - "dmg_mult": 3.0, - "effect": "AOE_STUN", - "range": 6.0 - }, - 140: { - "name": "破浪击", - "sp_cost": 30, - "cooldown": 12.0, - "dmg_mult": 2.2, - "effect": "RANGED_WAVE", - "range": 12.0 - } -} - -var is_riding: bool = false -var horse_tier: int = HORSE_TIER_NONE -var horse_stamina: float = 100.0 -var max_stamina: float = 100.0 -var skill_cooldowns: Dictionary = { - 137: 0.0, - 138: 0.0, - 139: 0.0, - 140: 0.0 -} - -const HORSE_TIER_NONE := 0 - -# 设置骑乘状态 -func set_mounted_state(riding: bool, tier: int = HORSE_TIER_MILITARY) -> void: - is_riding = riding - horse_tier = tier if riding else HORSE_TIER_NONE - -# 马上普通挥砍攻击 (Mounted Basic Sweep Attack) -func perform_mounted_basic_attack( - base_att: int, - targets_in_range: Array -) -> Dictionary: - if not is_riding: - return {"ok": false, "reason": "NOT_RIDING", "msg": "未处于骑乘状态!"} - - if horse_tier < HORSE_TIER_COMBAT: - return {"ok": false, "reason": "HORSE_TIER_TOO_LOW", "msg": "当前马匹等级不足,无法在马上战斗!需要战斗马或军马。"} - - # 精力衰减 - horse_stamina = maxf(0.0, horse_stamina - 0.5) - horse_stamina_changed.emit(horse_stamina, max_stamina) - - # 伤害计算:马上横扫增伤 1.2x (精力枯竭时惩罚为 0.8x) - var mult: float = 1.2 if horse_stamina > 0.0 else 0.8 - var dmg_per_target := int(base_att * mult) - var total_dmg: int = 0 - var hit_targets: Array = [] - - for target in targets_in_range: - var dist: float = float(target.get("distance", 999.0)) - if dist <= MOUNTED_ATTACK_RANGE: - hit_targets.append(target) - total_dmg += dmg_per_target - - horse_attack_performed.emit(hit_targets.size(), total_dmg) - - return { - "ok": true, - "attack_range": MOUNTED_ATTACK_RANGE, - "hit_count": hit_targets.size(), - "damage_per_hit": dmg_per_target, - "total_damage": total_dmg, - "stamina": horse_stamina - } - -# 施展 4 大战马专属技能 (137, 138, 139, 140) -func use_horse_skill( - skill_id: int, - player_sp: int, - base_att: int -) -> Dictionary: - if not is_riding: - return {"ok": false, "reason": "NOT_RIDING", "msg": "未在马背上,无法施展马术技能!"} - - if horse_tier < HORSE_TIER_MILITARY: - return {"ok": false, "reason": "REQUIRES_MILITARY_HORSE", "msg": "马术专属技能需要军马 (Lv21+) 方可施展!"} - - if not HORSE_SKILLS.has(skill_id): - return {"ok": false, "reason": "INVALID_SKILL_ID", "msg": "未知的战马技能!"} - - var cfg: Dictionary = HORSE_SKILLS[skill_id] - - # 检查冷却 - if skill_cooldowns[skill_id] > 0.0: - return { - "ok": false, - "reason": "SKILL_ON_COOLDOWN", - "remaining_cd": skill_cooldowns[skill_id], - "msg": "技能【%s】冷却中,剩余 %.1f 秒。" % [cfg["name"], skill_cooldowns[skill_id]] - } - - # 检查玩家法力 (SP) - var sp_cost: int = cfg["sp_cost"] - if player_sp < sp_cost: - return { - "ok": false, - "reason": "INSUFFICIENT_SP", - "cost": sp_cost, - "current_sp": player_sp, - "msg": "法力值不足,无法施展【%s】!" % cfg["name"] - } - - # 检查马匹精力 - if horse_stamina < 5.0: - return { - "ok": false, - "reason": "HORSE_EXHAUSTED", - "msg": "战马体力已透支,请先喂食胡萝卜或人参!" - } - - # 施法成功:扣除精力、进入冷却 - horse_stamina = maxf(0.0, horse_stamina - 5.0) - skill_cooldowns[skill_id] = cfg["cooldown"] - horse_stamina_changed.emit(horse_stamina, max_stamina) - horse_skill_used.emit(skill_id, cfg["name"], sp_cost) - - var dealt_dmg := int(base_att * cfg["dmg_mult"]) - - return { - "ok": true, - "skill_id": skill_id, - "skill_name": cfg["name"], - "sp_cost": sp_cost, - "cooldown": cfg["cooldown"], - "damage": dealt_dmg, - "effect": cfg["effect"], - "range": cfg["range"], - "msg": "施展了战马技能【%s】!" % cfg["name"] - } - -# 喂食恢复马匹精力 (胡萝卜 +40, 人参 +80) -func feed_horse(food_vnum: int) -> Dictionary: - if horse_tier == HORSE_TIER_NONE: - return {"ok": false, "reason": "NO_HORSE", "msg": "当前没有马匹!"} - - var restore_amount := 0.0 - var food_name := "" - if food_vnum == ITEM_FOOD_CARROT: - restore_amount = 40.0 - food_name = "胡萝卜" - elif food_vnum == ITEM_FOOD_GINSENG: - restore_amount = 80.0 - food_name = "人参" - else: - return {"ok": false, "reason": "WRONG_FOOD", "msg": "这不是战马适合的食物!"} - - horse_stamina = minf(max_stamina, horse_stamina + restore_amount) - horse_stamina_changed.emit(horse_stamina, max_stamina) - - return { - "ok": true, - "food_name": food_name, - "restored": restore_amount, - "current_stamina": horse_stamina, - "msg": "战马食用了【%s】,恢复了体力!" % food_name - } - -# 冷却逐帧更新 -func update(dt: float) -> void: - for sk in skill_cooldowns.keys(): - if skill_cooldowns[sk] > 0.0: - skill_cooldowns[sk] = maxf(0.0, skill_cooldowns[sk] - dt) diff --git a/project/horse_combat_system.gd.uid b/project/horse_combat_system.gd.uid deleted file mode 100644 index 3f7f617a..00000000 --- a/project/horse_combat_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bq413tb15c7fk diff --git a/project/horse_level_growth_system.gd b/project/horse_level_growth_system.gd deleted file mode 100644 index d403a0d9..00000000 --- a/project/horse_level_growth_system.gd +++ /dev/null @@ -1,283 +0,0 @@ -# horse_level_growth_system.gd —— Metin2 40250 官方马匹等级进化与养殖草料喂食系统 1:1 -# 对照 40250 服务端 horse_rider.cpp, char_horse.cpp, constants.h -class_name HorseLevelGrowthSystem -extends RefCounted - -signal horse_level_up(new_level: int, tier_name: String) -signal horse_fed(new_health: int, max_health: int) -signal horse_revived(new_health: int) -signal horse_mounted(horse_level: int, stats_applied: Dictionary) -signal horse_dismounted() -signal stamina_exhausted() - -# 40250 官方马牌与召唤道具 -const VNUM_HORSE_PICTURE := 50051 # 马牌 (初级小马召唤道具) -const VNUM_ARMOR_HORSE_BOOK := 50052 # 战马书 (战斗装甲马召唤道具) -const VNUM_MILITARY_HORSE_BOOK:= 50053 # 军马令 (军阶战马召唤道具) - -# 40250 官方草料饲料 -const VNUM_HAY := 50054 # 干草 (初级小马食物) -const VNUM_CARROT := 50055 # 胡萝卜 (战斗装甲马食物) -const VNUM_RED_GINSENG := 50056 # 红参 (军阶战马食物) - -# 40250 官方复活猴草 -const VNUM_HERB_EASY := 50057 # 初级猴草 (复活初级小马) -const VNUM_HERB_NORMAL := 50058 # 中级猴草 (复活战斗装甲马) -const VNUM_HERB_EXPERT := 50059 # 高级猴草 (复活军阶战马) - -# 3 大战马阶层定义 -const TIER_NONE := 0 -const TIER_BEGINNER := 1 # 1~10 级 初级小马 (Race 20101, 人物需求 Lv 25) -const TIER_COMBAT := 2 # 11~20 级 战斗装甲马 (Race 20104, 人物需求 Lv 35) -const TIER_MILITARY := 3 # 21~30 级 军阶战马 (Race 20107, 人物需求 Lv 50) - -# 40250 官方经典等级属性表 (选样代表级与通用成长模型) -const HORSE_STAT_TABLE: Dictionary = { - 1: {"min_lv": 25, "race": 20101, "max_health": 3, "max_stamina": 4, "st": 26, "dx": 35, "ht": 18, "iq": 9, "atk": 54, "def": 32}, - 5: {"min_lv": 25, "race": 20101, "max_health": 8, "max_stamina": 6, "st": 30, "dx": 40, "ht": 20, "iq": 10, "atk": 58, "def": 34}, - 10: {"min_lv": 25, "race": 20101, "max_health": 15, "max_stamina": 10, "st": 35, "dx": 46, "ht": 23, "iq": 12, "atk": 63, "def": 37}, - 11: {"min_lv": 35, "race": 20104, "max_health": 18, "max_stamina": 30, "st": 40, "dx": 53, "ht": 27, "iq": 13, "atk": 69, "def": 41}, - 15: {"min_lv": 35, "race": 20104, "max_health": 24, "max_stamina": 55, "st": 44, "dx": 58, "ht": 29, "iq": 15, "atk": 73, "def": 43}, - 20: {"min_lv": 35, "race": 20104, "max_health": 32, "max_stamina": 100, "st": 48, "dx": 64, "ht": 32, "iq": 16, "atk": 78, "def": 46}, - 21: {"min_lv": 50, "race": 20107, "max_health": 35, "max_stamina": 120, "st": 53, "dx": 71, "ht": 36, "iq": 18, "atk": 84, "def": 50}, - 25: {"min_lv": 50, "race": 20107, "max_health": 40, "max_stamina": 140, "st": 60, "dx": 80, "ht": 40, "iq": 20, "atk": 91, "def": 54}, - 30: {"min_lv": 50, "race": 20107, "max_health": 50, "max_stamina": 200, "st": 67, "dx": 89, "ht": 45, "iq": 22, "atk": 99, "def": 59} -} - -var horse_level: int = 0 -var current_health: int = 0 -var current_stamina: int = 0 -var is_mounted: bool = false -var applied_stats: Dictionary = {} - -# 获取当前战马阶层 -func get_tier() -> int: - if horse_level <= 0: - return TIER_NONE - elif horse_level <= 10: - return TIER_BEGINNER - elif horse_level <= 20: - return TIER_COMBAT - else: - return TIER_MILITARY - -# 获取阶层名称 -func get_tier_name() -> String: - match get_tier(): - TIER_BEGINNER: - return "初级小马" - TIER_COMBAT: - return "战斗装甲马" - TIER_MILITARY: - return "军阶战马" - _: - return "无战马" - -# 获取对应等级的基础属性 -func get_horse_stat(lv: int) -> Dictionary: - if HORSE_STAT_TABLE.has(lv): - return HORSE_STAT_TABLE[lv].duplicate(true) - # 近似插值计算 - var base_lv := 1 - for k in HORSE_STAT_TABLE.keys(): - if int(k) <= lv and int(k) > base_lv: - base_lv = int(k) - return HORSE_STAT_TABLE[base_lv].duplicate(true) - -# 初始化/领养战马 -func initialize_horse(level: int = 1) -> void: - horse_level = clampi(level, 1, 30) - var stat = get_horse_stat(horse_level) - current_health = int(stat["max_health"]) - current_stamina = int(stat["max_stamina"]) - -# 马匹等级升级 -func upgrade_horse_level(char_level: int) -> Dictionary: - if horse_level >= 30: - return {"ok": false, "reason": "MAX_HORSE_LEVEL", "msg": "战马已达最高等级 30 级!"} - - var target_lv := horse_level + 1 - var target_stat := get_horse_stat(target_lv) - var req_char_lv := int(target_stat["min_lv"]) - - if char_level < req_char_lv: - return { - "ok": false, - "reason": "CHAR_LEVEL_TOO_LOW", - "msg": "角色等级不足!晋升至 %d 级战马需要角色达到 %d 级。" % [target_lv, req_char_lv] - } - - horse_level = target_lv - var new_stat := get_horse_stat(horse_level) - current_health = int(new_stat["max_health"]) - current_stamina = int(new_stat["max_stamina"]) - - horse_level_up.emit(horse_level, get_tier_name()) - - return { - "ok": true, - "horse_level": horse_level, - "tier": get_tier_name(), - "msg": "战马成功进阶至 %d 级【%s】!" % [horse_level, get_tier_name()] - } - -# 喂食草料恢复健康值 (40250 horse_rider.cpp:FeedHorse) -func feed_horse(inventory: Array, slot_index: int) -> Dictionary: - if horse_level <= 0: - return {"ok": false, "reason": "NO_HORSE"} - - if current_health <= 0: - return {"ok": false, "reason": "HORSE_IS_DEAD", "msg": "战马已重伤休克!需要喂食复活猴草唤醒。"} - - var stat := get_horse_stat(horse_level) - var max_hp := int(stat["max_health"]) - if current_health >= max_hp: - return {"ok": false, "reason": "HEALTH_ALREADY_FULL", "msg": "战马饱食度与健康值已满!"} - - if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null: - return {"ok": false, "reason": "INVALID_SLOT"} - - var food: Dictionary = inventory[slot_index] - var food_vnum := int(food.get("vnum", 0)) - var tier := get_tier() - - # 校验草料与阶层匹配 (40250 规则: 初级吃干草, 战斗吃胡萝卜, 军马吃红参) - var match_ok := false - if tier == TIER_BEGINNER and food_vnum == VNUM_HAY: - match_ok = true - elif tier == TIER_COMBAT and food_vnum == VNUM_CARROT: - match_ok = true - elif tier == TIER_MILITARY and food_vnum == VNUM_RED_GINSENG: - match_ok = true - - if not match_ok: - return {"ok": false, "reason": "FOOD_TIER_MISMATCH", "msg": "该饲料与当前战马阶层不匹配!"} - - # 消耗 1 份食物 - var cnt := int(food.get("count", 1)) - if cnt > 1: - food["count"] = cnt - 1 - else: - inventory[slot_index] = null - - current_health = mini(max_hp, current_health + 2) - horse_fed.emit(current_health, max_hp) - - return { - "ok": true, - "current_health": current_health, - "max_health": max_hp, - "msg": "战马享用了饲料,健康值恢复至 %d/%d!" % [current_health, max_hp] - } - -# 使用猴草唤醒复活战马 -func revive_horse(inventory: Array, slot_index: int) -> Dictionary: - if current_health > 0: - return {"ok": false, "reason": "HORSE_NOT_DEAD", "msg": "战马依然健在,无需使用复活猴草!"} - - if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null: - return {"ok": false, "reason": "INVALID_SLOT"} - - var herb: Dictionary = inventory[slot_index] - var herb_vnum := int(herb.get("vnum", 0)) - var tier := get_tier() - - var herb_ok := false - if tier == TIER_BEGINNER and herb_vnum == VNUM_HERB_EASY: - herb_ok = true - elif tier == TIER_COMBAT and herb_vnum == VNUM_HERB_NORMAL: - herb_ok = true - elif tier == TIER_MILITARY and herb_vnum == VNUM_HERB_EXPERT: - herb_ok = true - - if not herb_ok: - return {"ok": false, "reason": "HERB_TIER_MISMATCH", "msg": "复活猴草阶层与战马不匹配!"} - - # 消耗 1 株猴草 - var cnt := int(herb.get("count", 1)) - if cnt > 1: - herb["count"] = cnt - 1 - else: - inventory[slot_index] = null - - var stat := get_horse_stat(horse_level) - current_health = maxi(1, int(stat["max_health"]) / 2) - current_stamina = maxi(1, int(stat["max_stamina"]) / 2) - - horse_revived.emit(current_health) - - return { - "ok": true, - "health": current_health, - "msg": "灵草生效!战马重焕生机复活苏醒。" - } - -# 骑乘上马 -func mount_horse(player_stats: Dictionary) -> Dictionary: - if horse_level <= 0: - return {"ok": false, "reason": "NO_HORSE", "msg": "尚未获得战马!"} - - if current_health <= 0: - return {"ok": false, "reason": "HORSE_IS_DEAD", "msg": "战马已昏迷,需先喂食猴草复活!"} - - if is_mounted: - return {"ok": false, "reason": "ALREADY_MOUNTED"} - - var stat := get_horse_stat(horse_level) - # 注入战马加成 - applied_stats = { - "attack_power": int(stat["atk"]), - "defense": int(stat["def"]) - } - for k in applied_stats.keys(): - player_stats[k] = int(player_stats.get(k, 0)) + applied_stats[k] - - is_mounted = true - horse_mounted.emit(horse_level, applied_stats) - - return {"ok": true, "msg": "成功乘骑战马!人马合一属性生效。"} - -# 下马 -func dismount_horse(player_stats: Dictionary) -> Dictionary: - if not is_mounted: - return {"ok": false, "reason": "NOT_MOUNTED"} - - for k in applied_stats.keys(): - player_stats[k] = int(player_stats.get(k, 0)) - applied_stats[k] - applied_stats.clear() - - is_mounted = false - horse_dismounted.emit() - - return {"ok": true, "msg": "已安全下马。"} - -# 消耗耐力 -func consume_stamina(amount: int, player_stats: Dictionary) -> void: - if not is_mounted: - return - current_stamina = maxi(0, current_stamina - amount) - if current_stamina <= 0: - stamina_exhausted.emit() - dismount_horse(player_stats) # 耐力耗尽自动下马 - -# 恢复耐力 -func recover_stamina(amount: int) -> void: - var stat := get_horse_stat(horse_level) - var max_stamina := int(stat["max_stamina"]) - current_stamina = mini(max_stamina, current_stamina + amount) - -# 序列化 -func serialize() -> Dictionary: - return { - "horse_level": horse_level, - "current_health": current_health, - "current_stamina": current_stamina, - "is_mounted": is_mounted - } - -# 反序列化 -func deserialize(data: Dictionary) -> void: - horse_level = int(data.get("horse_level", 0)) - current_health = int(data.get("current_health", 0)) - current_stamina = int(data.get("current_stamina", 0)) - is_mounted = bool(data.get("is_mounted", false)) diff --git a/project/horse_level_growth_system.gd.uid b/project/horse_level_growth_system.gd.uid deleted file mode 100644 index 92b32606..00000000 --- a/project/horse_level_growth_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cwiwpfsupj5gr diff --git a/project/horse_system.gd b/project/horse_system.gd deleted file mode 100644 index 0f221392..00000000 --- a/project/horse_system.gd +++ /dev/null @@ -1,138 +0,0 @@ -# horse_system.gd —— 40250 战马与骑乘系统 1:1 规则引擎 -# 严格对照: -# metin2/src/server/game/src/cmd_general.cpp:45-100 (do_user_horse_ride / back / feed) -# metin2/src/server/game/src/cmd_general.cpp:2426-2465 (do_ride) -# root/game.py:344, 461-490 (Ctrl+G, Ctrl+H, Ctrl+B, Ctrl+F) -# ClientVS22/UserInterface/InstanceBase.cpp (IsMountingHorse, 5.0m attack range) -extends RefCounted - -const ITEM_HORSE_PICTURE := 50051 # 初级马画 (Lv 1 - 10) -const ITEM_HORSE_MEDAL := 50052 # 战斗马牌 (Lv 11 - 20) -const ITEM_HORSE_MILITARY := 50053 # 军马牌 (Lv 21 - 30) - -const ITEM_FOOD_HAY := 50057 # 干草 -const ITEM_FOOD_CARROT := 50054 # 胡萝卜 -const ITEM_FOOD_GINSENG := 50058 # 人参 - -const HORSE_TIER_NONE := 0 -const HORSE_TIER_BEGINNER := 1 # 普通马 -const HORSE_TIER_COMBAT := 2 # 战斗马(可马上砍怪) -const HORSE_TIER_MILITARY := 3 # 军马(可使用马术技能) - -# 召唤消耗 SP -const SUMMON_SP_COST := { - HORSE_TIER_BEGINNER: 100, - HORSE_TIER_COMBAT: 200, - HORSE_TIER_MILITARY: 300, -} - -# 基础召唤成功率 -const BASE_SUMMON_CHANCE := { - HORSE_TIER_BEGINNER: 10, - HORSE_TIER_COMBAT: 20, - HORSE_TIER_MILITARY: 30, -} - -# 骑乘时战斗与拾取距离加成(从 300cm 扩至 500cm,1:1 ClientVS22) -const WALKING_ATTACK_RANGE := 3.0 -const MOUNTED_ATTACK_RANGE := 5.0 - -# 判定马牌对应的级别与需求 -static func get_horse_tier_from_item(vnum: int) -> int: - match vnum: - ITEM_HORSE_PICTURE: return HORSE_TIER_BEGINNER - ITEM_HORSE_MEDAL: return HORSE_TIER_COMBAT - ITEM_HORSE_MILITARY: return HORSE_TIER_MILITARY - _: return HORSE_TIER_NONE - -# 所需饲料 -static func get_required_food(tier: int) -> int: - match tier: - HORSE_TIER_BEGINNER: return ITEM_FOOD_HAY - HORSE_TIER_COMBAT: return ITEM_FOOD_CARROT - HORSE_TIER_MILITARY: return ITEM_FOOD_GINSENG - _: return 0 - -# 召唤几率计算(1:1 char_horse.cpp: HorseSummon) -# chance = base_chance + horse_summon_skill_level * 10, max 100 -static func calculate_summon_chance(tier: int, summon_skill_level: int) -> int: - var base: int = BASE_SUMMON_CHANCE.get(tier, 10) - return mini(100, base + summon_skill_level * 10) - -# 执行召唤判定 -static func try_summon_horse( - tier: int, - current_sp: int, - summon_skill_level: int, - roll_val := -1 -) -> Dictionary: - var sp_cost: int = SUMMON_SP_COST.get(tier, 100) - if current_sp < sp_cost: - return {"success": false, "err": "insufficient_sp", "sp_cost": sp_cost} - - var chance := calculate_summon_chance(tier, summon_skill_level) - var dice := roll_val if roll_val > 0 else (randi() % 100 + 1) - var success := (dice <= chance) - - return { - "success": success, - "sp_cost": sp_cost, - "tier": tier, - "err": "" if success else "summon_failed", - } - -# 执行 /user_horse_ride (Ctrl+H) 状态机 (cmd_general.cpp:45) -static func handle_user_horse_ride( - is_riding: bool, - is_horse_summoned: bool, - is_other_mount: bool, - is_dead_or_stun: bool -) -> Dictionary: - if is_dead_or_stun: - return {"action": "none", "err": "dead_or_stun"} - - if not is_riding: - if is_other_mount: - return {"action": "none", "err": "already_mounting_other", "msg": "已经在使用其他坐骑。"} - if not is_horse_summoned: - return {"action": "none", "err": "horse_not_summoned", "msg": "请先召唤马匹。"} - return {"action": "start_riding", "err": "", "is_riding": true} - else: - return {"action": "stop_riding", "err": "", "is_riding": false} - -# 执行 /user_horse_back (Ctrl+B) 状态机 (cmd_general.cpp:75) -static func handle_user_horse_back( - is_riding: bool, - is_horse_summoned: bool -) -> Dictionary: - if is_riding: - return {"action": "none", "err": "must_dismount_first", "msg": "请先下马。"} - if not is_horse_summoned: - return {"action": "none", "err": "horse_not_summoned", "msg": "请先召唤马匹。"} - return {"action": "unsummon", "err": "", "is_horse_summoned": false, "msg": "已送回马匹。"} - -# 执行 /ride (Ctrl+G) 状态机 (cmd_general.cpp:2426) -static func handle_ride( - is_riding: bool, - is_horse_summoned: bool, - has_ride_item: bool -) -> Dictionary: - if is_riding: - return {"action": "stop_riding", "err": "", "is_riding": false} - if is_horse_summoned: - return {"action": "start_riding", "err": "", "is_riding": true} - if has_ride_item: - return {"action": "mount_item", "err": "", "is_riding": true} - return {"action": "none", "err": "cannot_ride", "msg": "没有可骑乘的马匹或坐骑。"} - -# 喂食逻辑 (Ctrl+F) -static func handle_user_horse_feed( - tier: int, - food_item_vnum: int -) -> Dictionary: - var req_food := get_required_food(tier) - if req_food == 0: - return {"success": false, "msg": "当前没有出战马匹。"} - if food_item_vnum != req_food: - return {"success": false, "msg": "这种食物不适合当前马匹。"} - return {"success": true, "consumed_vnum": req_food, "msg": "战马饱餐了一顿,恢复了活力!"} diff --git a/project/horse_system.gd.uid b/project/horse_system.gd.uid deleted file mode 100644 index cefe0d26..00000000 --- a/project/horse_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b0qu4yyhtooea diff --git a/project/inventory_expansion_system.gd b/project/inventory_expansion_system.gd deleted file mode 100644 index 98ff2a09..00000000 --- a/project/inventory_expansion_system.gd +++ /dev/null @@ -1,164 +0,0 @@ -# inventory_expansion_system.gd —— 40250 官方背包槽位解锁凭证与扩展系统 1:1 -# 严格对照: -# metin2/src/server/common/length.h (INVENTORY_MAX_NUM = 90, 45格/页) -# metin2/src/server/share/conf/item_proto.txt (72319 / 72320 储物锁匙) -# ClientVS22/source/UserInterface/GameType.h (INVENTORY_PAGE_SIZE = 45, 5x9 grid) -extends RefCounted - -const PAGE_SIZE := 45 # 每页 45 格 (5 列 x 9 行) -const BASE_UNLOCKED_PAGES := 2 # 默认前 2 页解锁 (0..89 共 90 格) -const MAX_PAGES := 4 # 最大 4 页 (共 180 格) -const TOTAL_SLOTS := 180 # 45 * 4 = 180 - -const SLOTS_PER_STAGE := 5 # 每个解锁阶段解锁 5 格 -const TOTAL_EXPANSION_STAGES := 18 # 后 2 页共 90 格,划分为 18 个阶段 (90 / 5 = 18) - -const EXPANSION_KEY_VNUM := 72319 # 储物锁匙 - -# 18 个阶段的锁匙消耗曲线 (1:1 官方阶梯消耗) -# 阶段 1~6 (90..119 格): 1 匙/段 -# 阶段 7~12 (120..149 格): 2 匙/段 -# 阶段 13~17 (150..174 格): 3 匙/段 -# 阶段 18 (175..179 格): 4 匙/段 -const STAGE_KEY_COSTS := [ - 1, 1, 1, 1, 1, 1, # Stage 1..6 - 2, 2, 2, 2, 2, 2, # Stage 7..12 - 3, 3, 3, 3, 3, # Stage 13..17 - 4 # Stage 18 -] - -# 当前已解锁的扩展阶段数 (0..18) -var unlocked_stages: int = 0 -# 背包槽位存储 (slot 0..179 -> item_data Dictionary or null) -var inventory_slots: Array = [] - -func _init(initial_unlocked_stages: int = 0) -> void: - unlocked_stages = clamp(initial_unlocked_stages, 0, TOTAL_EXPANSION_STAGES) - inventory_slots.clear() - for i in range(TOTAL_SLOTS): - inventory_slots.append(null) - -# 获取当前已解锁的最大格位数 (基础 90 格 + 解锁阶段数 * 5) -func get_unlocked_slot_count() -> int: - return (BASE_UNLOCKED_PAGES * PAGE_SIZE) + (unlocked_stages * SLOTS_PER_STAGE) - -# 检查特定格位是否已解锁 -func is_slot_unlocked(slot_idx: int) -> bool: - if slot_idx < 0 or slot_idx >= TOTAL_SLOTS: - return false - return slot_idx < get_unlocked_slot_count() - -# 获取下一阶段所需的储物锁匙数量 -func get_next_stage_key_cost() -> int: - if unlocked_stages >= TOTAL_EXPANSION_STAGES: - return 0 - return STAGE_KEY_COSTS[unlocked_stages] - -# 解锁下一阶段背包槽位 (5 格) -func unlock_next_stage(player_keys: int) -> Dictionary: - var res := { - "success": false, - "stage": unlocked_stages, - "keys_deducted": 0, - "remaining_keys": player_keys, - "unlocked_slots": get_unlocked_slot_count(), - "is_all_unlocked": false, - "error_code": "OK", - "message": "" - } - - if unlocked_stages >= TOTAL_EXPANSION_STAGES: - res["is_all_unlocked"] = true - res["error_code"] = "ALREADY_MAX_EXPANDED" - res["message"] = "背包所有 4 页 180 格位已全部解锁,达到上限!" - return res - - var needed_keys := get_next_stage_key_cost() - if player_keys < needed_keys: - res["error_code"] = "INSUFFICIENT_KEYS" - res["message"] = "储物锁匙不足!解锁阶段 %d 需要 %d 枚储物锁匙。" % [unlocked_stages + 1, needed_keys] - return res - - unlocked_stages += 1 - res["success"] = true - res["stage"] = unlocked_stages - res["keys_deducted"] = needed_keys - res["remaining_keys"] = player_keys - needed_keys - res["unlocked_slots"] = get_unlocked_slot_count() - res["is_all_unlocked"] = (unlocked_stages >= TOTAL_EXPANSION_STAGES) - res["message"] = "解锁成功!阶段 %d 已解除封印,新增 5 个可用格位 (当前可用格数: %d)。" % [ - unlocked_stages, res["unlocked_slots"] - ] - return res - -# 检查物品是否能放置在指定格位 (考虑物品高度 height 与跨页/封印限制) -# 在 Metin2 中,背包每行 5 格,高度占据 slot, slot+5, slot+10 -func can_place_item_at(slot_idx: int, item_height: int = 1) -> Dictionary: - var res := { "can_place": false, "error_code": "OK", "message": "" } - - if slot_idx < 0 or slot_idx >= TOTAL_SLOTS: - res["error_code"] = "OUT_OF_BOUNDS" - res["message"] = "超出背包格位边界。" - return res - - var start_page := slot_idx / PAGE_SIZE - var page_local_slot := slot_idx % PAGE_SIZE - var row := page_local_slot / 5 - - # 跨页检查 (单物品不能从第 1 页跨到底部溢出到第 2 页) - if row + item_height > 9: - res["error_code"] = "PAGE_OVERFLOW" - res["message"] = "物品长度超出单页底部边界。" - return res - - # 检查高度占用的每一个单元格 - for h in range(item_height): - var check_slot := slot_idx + (h * 5) - if not is_slot_unlocked(check_slot): - res["error_code"] = "SLOT_LOCKED" - res["message"] = "目标单元格 [%d] 处于锁链封印状态,需使用储物锁匙解锁。" % check_slot - return res - - if inventory_slots[check_slot] != null: - res["error_code"] = "SLOT_OCCUPIED" - res["message"] = "目标单元格已被占用。" - return res - - res["can_place"] = true - return res - -# 放置物品到背包格位 -func put_item(slot_idx: int, item_data: Dictionary, item_height: int = 1) -> Dictionary: - var check := can_place_item_at(slot_idx, item_height) - if not check["can_place"]: - return check - - var it_copy := item_data.duplicate(true) - it_copy["size"] = item_height - it_copy["head_slot"] = slot_idx - - for h in range(item_height): - var cell := slot_idx + (h * 5) - inventory_slots[cell] = it_copy - - return { "success": true, "error_code": "OK", "slot": slot_idx } - -# 从背包移除物品 -func remove_item(slot_idx: int) -> Dictionary: - var res := { "success": false, "item": null, "error_code": "OK" } - if slot_idx < 0 or slot_idx >= TOTAL_SLOTS or inventory_slots[slot_idx] == null: - res["error_code"] = "EMPTY_SLOT" - return res - - var it: Dictionary = inventory_slots[slot_idx] - var head_slot: int = it.get("head_slot", slot_idx) - var size: int = it.get("size", 1) - - for h in range(size): - var cell := head_slot + (h * 5) - if cell < TOTAL_SLOTS: - inventory_slots[cell] = null - - res["success"] = true - res["item"] = it - return res diff --git a/project/inventory_expansion_system.gd.uid b/project/inventory_expansion_system.gd.uid deleted file mode 100644 index ff1245f6..00000000 --- a/project/inventory_expansion_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://8vqdwqxsikw diff --git a/project/inventory_sort_system.gd b/project/inventory_sort_system.gd deleted file mode 100644 index dec1834c..00000000 --- a/project/inventory_sort_system.gd +++ /dev/null @@ -1,163 +0,0 @@ -# inventory_sort_system.gd —— Metin2 40250 多页背包智能自动整理与堆叠合并 1:1 -# 对照 40250 服务端 char_item.cpp, uiinventory.py, item.cpp -class_name InventorySortSystem -extends RefCounted - -signal inventory_sorted(total_items: int, merged_stacks: int) -signal page_switched(new_page: int) - -const PAGE_SIZE := 45 -const TOTAL_PAGES := 2 -const TOTAL_SLOTS := 90 # 45 x 2 -const MAX_STACK_LIMIT := 200 - -# 排序优先级分类 -enum ItemCategory { - WEAPON = 1, # 武器 - ARMOR = 2, # 铠甲防具 - JEWELRY = 3, # 饰品首饰 - CONSUMABLE = 4, # 药水与消耗品 - MATERIAL = 5, # 强化升阶材料 - CHEST_KEY = 6, # 宝箱与钥匙 - OTHER = 7 # 其他 -} - -var current_page: int = 0 - -# 切换当前显示背包页 (0 或 1) -func switch_page(page_idx: int) -> Dictionary: - if page_idx < 0 or page_idx >= TOTAL_PAGES: - return {"ok": false, "reason": "INVALID_PAGE", "msg": "无效的背包页码!"} - - current_page = page_idx - page_switched.emit(page_idx) - - return { - "ok": true, - "page": page_idx, - "slot_start": page_idx * PAGE_SIZE, - "slot_end": (page_idx + 1) * PAGE_SIZE - 1 - } - -# 获取物品所属排序大类 -static func get_item_category(item: Dictionary) -> int: - var it_type: String = item.get("item_type", "") - match it_type: - "weapon": - return ItemCategory.WEAPON - "armor", "shield", "helmet": - return ItemCategory.ARMOR - "jewelry", "necklace", "earrings", "bracelet", "shoes", "belt": - return ItemCategory.JEWELRY - "potion", "consumable", "dew", "elixir": - return ItemCategory.CONSUMABLE - "material", "fragment", "scroll": - return ItemCategory.MATERIAL - "chest", "key": - return ItemCategory.CHEST_KEY - _: - # 根据 vnum 范围与特征推断 - var vnum: int = int(item.get("vnum", 0)) - if vnum < 3000 and vnum >= 10: - return ItemCategory.WEAPON - elif vnum >= 11000 and vnum < 13200: - return ItemCategory.ARMOR - elif (vnum >= 27000 and vnum < 28000) or (vnum >= 50800 and vnum < 50900) or (vnum >= 72700 and vnum < 72800): - return ItemCategory.CONSUMABLE - elif (vnum >= 50000 and vnum < 50200) or (vnum >= 30000 and vnum < 30400): - return ItemCategory.CHEST_KEY - elif vnum >= 71000 and vnum < 72000: - return ItemCategory.MATERIAL - return ItemCategory.OTHER - -# 判定物品是否可堆叠 -static func is_item_stackable(item: Dictionary) -> bool: - if item.get("is_stackable", false): - return true - var it_type: String = item.get("item_type", "") - if ["potion", "consumable", "dew", "elixir", "material", "fragment", "scroll", "arrow"].has(it_type): - return true - var cnt: int = int(item.get("count", 1)) - return cnt > 1 - -# 自动合并碎片堆叠 (Auto-Stack Merge) -func merge_stacks(inventory: Array) -> int: - var merged_count := 0 - - for i in range(inventory.size()): - var base = inventory[i] - if base == null or not is_item_stackable(base): - continue - - var b_vnum: int = int(base.get("vnum", 0)) - var b_count: int = int(base.get("count", 1)) - if b_count >= MAX_STACK_LIMIT: - continue - - for j in range(i + 1, inventory.size()): - var other = inventory[j] - if other == null or int(other.get("vnum", 0)) != b_vnum: - continue - - var o_count: int = int(other.get("count", 1)) - var space := MAX_STACK_LIMIT - b_count - - if o_count <= space: - b_count += o_count - base["count"] = b_count - inventory[j] = null - merged_count += 1 - else: - b_count = MAX_STACK_LIMIT - base["count"] = MAX_STACK_LIMIT - other["count"] = o_count - space - merged_count += 1 - break - - return merged_count - -# 一键智能整理背包 (堆叠合并 + 分类前置紧凑排序) -func sort_inventory(inventory: Array) -> Dictionary: - # 1. 先进行碎片堆叠合并 - var merged := merge_stacks(inventory) - - # 2. 收集所有有效物品并打上排序权重标签 - var items_to_sort: Array = [] - for i in range(inventory.size()): - var it = inventory[i] - if it != null: - var cat := get_item_category(it) - var vnum: int = int(it.get("vnum", 0)) - var level: int = int(it.get("level", 0)) - items_to_sort.append({ - "item": it, - "category": cat, - "vnum": vnum, - "level": level - }) - - # 3. 排序比较器:类别升序 (1~7),同类内 vnum 升序/降序排列 - items_to_sort.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: - if a["category"] != b["category"]: - return a["category"] < b["category"] - if a["vnum"] != b["vnum"]: - return a["vnum"] < b["vnum"] - return int(a["item"].get("count", 1)) > int(b["item"].get("count", 1)) - ) - - # 4. 紧凑回填背包并清空末尾 - var total_items := items_to_sort.size() - for i in range(inventory.size()): - if i < total_items: - inventory[i] = items_to_sort[i]["item"] - else: - inventory[i] = null - - inventory_sorted.emit(total_items, merged) - - return { - "ok": true, - "total_items": total_items, - "merged_stacks": merged, - "msg": "背包整理完成!共归纳 %d 件物品,合并 %d 处堆叠。" % [total_items, merged] - } diff --git a/project/inventory_sort_system.gd.uid b/project/inventory_sort_system.gd.uid deleted file mode 100644 index 33421472..00000000 --- a/project/inventory_sort_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://75w86o6g8kif diff --git a/project/item_enchant_system.gd b/project/item_enchant_system.gd deleted file mode 100644 index 7a234810..00000000 --- a/project/item_enchant_system.gd +++ /dev/null @@ -1,276 +0,0 @@ -# item_enchant_system.gd —— Metin2 40250 装备词条重铸与精炼附魔洗炼系统 1:1 -# 对照 40250 服务端 char_item.cpp, item_attribute.cpp, constants.cpp -class_name ItemEnchantSystem -extends RefCounted - -const ItemAttrSystem = preload("res://item_attr_system.gd") - -signal enchant_applied(target_slot: int, scroll_vnum: int, success: bool, new_attrs: Array) -signal rare_enchant_applied(target_slot: int, success: bool, rare_attrs: Array) -signal enchant_failed(target_slot: int, reason: String) - -const VNUM_ATTR_ADD := 71085 # 添加属性秘籍 (添加第 1~4 条) -const VNUM_ATTR_CHANGE := 71084 # 属性改变秘籍 (洗炼现有 1~5 条) -const VNUM_BLESSING_MARBLE := 70024 # 祝福宝珠 (添加第 5 条属性) -const VNUM_RARE_ADD := 71151 # 稀有属性秘籍 (添加第 6~7 条稀有词条) -const VNUM_RARE_CHANGE := 71152 # 稀有属性改变 (单独洗炼第 6~7 条稀有词条) - -const ADD_PERCENT_TABLE := [100, 80, 60, 50, 30] # 官方各阶段成功率 -const RARE_ADD_PERCENT := 50 # 稀有词条附加成功率 - -# 稀有词条池 (6~7 槽位特有词条) -const RARE_ATTR_POOL := { - 101: {"name": "全技能伤害加成", "values": [1, 2, 3, 5, 8], "weight": 8}, - 102: {"name": "首领怪物伤害加成", "values": [2, 4, 6, 8, 12], "weight": 8}, - 103: {"name": "魔石怪物伤害加成", "values": [3, 5, 8, 10, 15], "weight": 10}, - 104: {"name": "受首领伤害减免", "values": [2, 4, 6, 8, 10], "weight": 8}, - 105: {"name": "物理防御无视概率", "values": [1, 2, 3, 5, 7], "weight": 6} -} - -# 检查是否为有效附魔卷轴 -static func is_enchant_scroll(vnum: int) -> bool: - return vnum in [VNUM_ATTR_ADD, VNUM_ATTR_CHANGE, VNUM_BLESSING_MARBLE, VNUM_RARE_ADD, VNUM_RARE_CHANGE] - -# 检查物品是否为可附魔装备 -static func is_enchantable_item(item: Dictionary) -> bool: - if item.is_empty(): - return false - # 检查是否上锁保护 - if item.get("is_locked", false): - return false - var vnum: int = int(item.get("vnum", 0)) - # 箭矢、消耗品不可附魔 (箭矢 8000~8009) - if vnum >= 8000 and vnum <= 8009: - return false - var itype: int = int(item.get("type", 1)) # 默认为 1 (Weapon) 或 2 (Armor) - return itype in [1, 2] - -# 对背包内的装备使用附魔卷轴 -func apply_scroll(scroll_slot: int, target_slot: int, inventory: Array, force_success: bool = false) -> Dictionary: - if scroll_slot < 0 or scroll_slot >= inventory.size() or inventory[scroll_slot] == null: - return {"ok": false, "reason": "INVALID_SCROLL_SLOT", "msg": "附魔卷轴格子无效!"} - - if target_slot < 0 or target_slot >= inventory.size() or inventory[target_slot] == null: - return {"ok": false, "reason": "INVALID_TARGET_SLOT", "msg": "目标装备格子无效!"} - - var scroll: Dictionary = inventory[scroll_slot] - var target: Dictionary = inventory[target_slot] - var scroll_vnum: int = int(scroll.get("vnum", 0)) - - if not is_enchant_scroll(scroll_vnum): - return {"ok": false, "reason": "NOT_AN_ENCHANT_SCROLL", "msg": "此物品不是附魔洗炼卷轴!"} - - if not is_enchantable_item(target): - enchant_failed.emit(target_slot, "ITEM_CANNOT_BE_ENCHANTED") - return {"ok": false, "reason": "ITEM_CANNOT_BE_ENCHANTED", "msg": "此装备无法进行词条附魔或已被安全上锁!"} - - if not target.has("attrs"): - target["attrs"] = [] - if not target.has("rare_attrs"): - target["rare_attrs"] = [] - - var res: Dictionary = {} - match scroll_vnum: - VNUM_ATTR_ADD: - res = _handle_add_attribute(target, force_success) - VNUM_ATTR_CHANGE: - res = _handle_change_attribute(target) - VNUM_BLESSING_MARBLE: - res = _handle_blessing_marble(target, force_success) - VNUM_RARE_ADD: - res = _handle_rare_add(target, force_success) - VNUM_RARE_CHANGE: - res = _handle_rare_change(target) - - if not res.get("ok", false): - enchant_failed.emit(target_slot, res.get("reason", "FAILED")) - return res - - # 扣除 1 个卷轴 - _consume_scroll(scroll_slot, inventory) - - if res.get("success", false): - if scroll_vnum in [VNUM_RARE_ADD, VNUM_RARE_CHANGE]: - rare_enchant_applied.emit(target_slot, true, target["rare_attrs"]) - else: - enchant_applied.emit(target_slot, scroll_vnum, true, target["attrs"]) - else: - enchant_applied.emit(target_slot, scroll_vnum, false, target["attrs"]) - - return res - -# 扣除背包中的卷轴 -func _consume_scroll(slot: int, inventory: Array) -> void: - var item: Dictionary = inventory[slot] - var count: int = int(item.get("count", 1)) - if count > 1: - item["count"] = count - 1 - else: - inventory[slot] = null - -# 71085: 添加第 1~4 条属性 -func _handle_add_attribute(target: Dictionary, force_success: bool) -> Dictionary: - var attrs: Array = target["attrs"] - if attrs.size() >= 4: - return {"ok": false, "reason": "MAX_BASE_ATTRS_REACHED", "msg": "该装备已有 4 条属性!请使用祝福宝珠开辟第 5 条属性。"} - - var rate: int = ADD_PERCENT_TABLE[attrs.size()] - var roll: int = randi_range(1, 100) - var is_succ: bool = force_success or (roll <= rate) - - if not is_succ: - return {"ok": true, "success": false, "msg": "附魔失败,卷轴化为灰烬,未能追加新属性!"} - - var new_attr = _roll_random_attribute(attrs, target.get("type", 1)) - attrs.append(new_attr) - - return { - "ok": true, - "success": true, - "new_attr": new_attr, - "total_attrs": attrs.size(), - "msg": "附魔成功!装备获得了新词条【%s +%d】!" % [new_attr["name"], new_attr["value"]] - } - -# 71084: 改变所有现有 1~5 条属性 -func _handle_change_attribute(target: Dictionary) -> Dictionary: - var attrs: Array = target["attrs"] - if attrs.is_empty(): - return {"ok": false, "reason": "NO_ATTRS_TO_CHANGE", "msg": "该装备没有任何属性词条,无法进行洗炼!"} - - var count: int = attrs.size() - attrs.clear() - - for i in range(count): - var attr = _roll_random_attribute(attrs, target.get("type", 1)) - attrs.append(attr) - - return { - "ok": true, - "success": true, - "attrs": attrs, - "msg": "洗炼成功!%d 条词条已全新重铸!" % count - } - -# 70024: 祝福宝珠 (添加第 5 条属性) -func _handle_blessing_marble(target: Dictionary, force_success: bool) -> Dictionary: - var attrs: Array = target["attrs"] - if attrs.size() < 4: - return {"ok": false, "reason": "NEED_4_ATTRS_FIRST", "msg": "装备必须先拥有 4 条属性,才能使用祝福宝珠!"} - if attrs.size() >= 5: - return {"ok": false, "reason": "ALREADY_HAS_5_ATTRS", "msg": "该装备已拥有 5 条属性,达到上限!"} - - var rate: int = ADD_PERCENT_TABLE[4] # 30% - var roll: int = randi_range(1, 100) - var is_succ: bool = force_success or (roll <= rate) - - if not is_succ: - return {"ok": true, "success": false, "msg": "祝福宝珠破碎,未能为装备附加第 5 条属性!"} - - var new_attr = _roll_random_attribute(attrs, target.get("type", 1)) - attrs.append(new_attr) - - return { - "ok": true, - "success": true, - "new_attr": new_attr, - "total_attrs": 5, - "msg": "神圣光芒降临!成功为装备铭刻第 5 条核心属性【%s +%d】!" % [new_attr["name"], new_attr["value"]] - } - -# 71151: 稀有属性附加 (第 6~7 条) -func _handle_rare_add(target: Dictionary, force_success: bool) -> Dictionary: - var rares: Array = target["rare_attrs"] - if rares.size() >= 2: - return {"ok": false, "reason": "MAX_RARE_ATTRS", "msg": "已达到 2 条稀有词条上限!"} - - var roll: int = randi_range(1, 100) - var is_succ: bool = force_success or (roll <= RARE_ADD_PERCENT) - - if not is_succ: - return {"ok": true, "success": false, "msg": "稀有附魔失败,未能附加稀有词条!"} - - var new_rare = _roll_random_rare_attribute(rares) - rares.append(new_rare) - - return { - "ok": true, - "success": true, - "rare_attr": new_rare, - "total_rares": rares.size(), - "msg": "稀有附魔成功!觉醒了金色稀有词条【%s +%d%%】!" % [new_rare["name"], new_rare["value"]] - } - -# 71152: 稀有属性改变 (单独洗炼第 6~7 条) -func _handle_rare_change(target: Dictionary) -> Dictionary: - var rares: Array = target["rare_attrs"] - if rares.is_empty(): - return {"ok": false, "reason": "NO_RARES_TO_CHANGE", "msg": "该装备尚无任何稀有词条!"} - - var count: int = rares.size() - rares.clear() - - for i in range(count): - var r = _roll_random_rare_attribute(rares) - rares.append(r) - - return { - "ok": true, - "success": true, - "rare_attrs": rares, - "msg": "稀有词条洗炼成功!%d 条稀有属性已重新激活!" % count - } - -# 生成不重复的普通属性 -func _roll_random_attribute(existing_attrs: Array, item_type: int) -> Dictionary: - var existing_ids: Array = [] - for a in existing_attrs: - existing_ids.append(a.get("type", 0)) - - # 选用武器或防具属性池 - var pool = ItemAttrSystem.WEAPON_ATTR_POOL if item_type == 1 else ItemAttrSystem.ARMOR_ATTR_POOL - var candidates: Array = [] - for type_id in pool.keys(): - if not (type_id in existing_ids): - candidates.append(type_id) - - if candidates.is_empty(): - candidates = pool.keys() - - var picked_id: int = candidates[randi() % candidates.size()] - var entry = pool[picked_id] - var tier_idx: int = randi() % entry["values"].size() - var val: int = entry["values"][tier_idx] - - return { - "type": picked_id, - "name": entry["name"], - "value": val, - "tier": tier_idx + 1 - } - -# 生成不重复的稀有属性 -func _roll_random_rare_attribute(existing_rares: Array) -> Dictionary: - var existing_ids: Array = [] - for a in existing_rares: - existing_ids.append(a.get("type", 0)) - - var candidates: Array = [] - for type_id in RARE_ATTR_POOL.keys(): - if not (type_id in existing_ids): - candidates.append(type_id) - - if candidates.is_empty(): - candidates = RARE_ATTR_POOL.keys() - - var picked_id: int = candidates[randi() % candidates.size()] - var entry = RARE_ATTR_POOL[picked_id] - var tier_idx: int = randi() % entry["values"].size() - var val: int = entry["values"][tier_idx] - - return { - "type": picked_id, - "name": entry["name"], - "value": val, - "tier": tier_idx + 1 - } diff --git a/project/item_enchant_system.gd.uid b/project/item_enchant_system.gd.uid deleted file mode 100644 index 1692341c..00000000 --- a/project/item_enchant_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b24gvmuspy3k7 diff --git a/project/item_split_system.gd b/project/item_split_system.gd deleted file mode 100644 index 7646e1cb..00000000 --- a/project/item_split_system.gd +++ /dev/null @@ -1,143 +0,0 @@ -# item_split_system.gd —— Metin2 40250 物品堆叠精准拆分与快捷批量操作 1:1 -# 对照 40250 服务端 char_item.cpp, input_main.cpp, uiinventory.py -class_name ItemSplitSystem -extends RefCounted - -signal item_split(source_slot: int, target_slot: int, split_count: int) -signal item_quick_sold(slot_idx: int, gold_received: int, item_name: String) -signal item_split_requested(source_slot: int, target_slot: int, split_count: int) -signal item_quick_sell_requested(slot_idx: int, count: int, item_name: String) - -# 堆叠精准拆分 (Shift + 左键) -func split_item( - source_slot: int, - split_count: int, - target_slot: int, - inventory: Array, - client: Node = null, - source_window: int = 1, - target_window: int = 1 -) -> Dictionary: - if source_slot < 0 or source_slot >= inventory.size(): - return {"ok": false, "reason": "INVALID_SOURCE_SLOT", "msg": "无效的源栏位!"} - - var source_item = inventory[source_slot] - if source_item == null: - return {"ok": false, "reason": "EMPTY_SOURCE_SLOT", "msg": "源栏位上没有物品!"} - - var current_count: int = int(source_item.get("count", 1)) - if current_count <= 1: - return {"ok": false, "reason": "NOT_SPLITTABLE", "msg": "单件物品无法拆分!"} - - if split_count < 1 or split_count >= current_count: - return { - "ok": false, - "reason": "INVALID_SPLIT_COUNT", - "max_allowed": current_count - 1, - "msg": "拆分数量必须在 1 到 %d 之间!" % (current_count - 1) - } - - # 确定目标格子 - var dest_slot := target_slot - if dest_slot < 0: - for i in range(inventory.size()): - if inventory[i] == null: - dest_slot = i - break - - if dest_slot < 0 or dest_slot >= inventory.size(): - return {"ok": false, "reason": "NO_DEST_SLOT", "msg": "背包没有可用空位放置拆分出的物品!"} - - if inventory[dest_slot] != null: - return {"ok": false, "reason": "DEST_SLOT_NOT_EMPTY", "msg": "目标栏位已被占用!"} - - # 40250 does not edit CPythonPlayer's item array optimistically. Splitting - # is an ordinary CG_ITEM_MOVE(source, destination, count); both resulting - # counts arrive later through GC_ITEM_UPDATE/SET. Keep this helper from - # becoming a second local authority when it is used by desktop or mobile UI. - if client == null or not client.has_method("move_item"): - return {"ok": false, "reason": "NETWORK_CLIENT_REQUIRED", "msg": "拆分需要连接服务器确认!"} - if not client.move_item(source_window, source_slot, target_window, dest_slot, split_count): - return {"ok": false, "reason": "SEND_FAILED", "msg": "拆分请求发送失败!"} - - item_split_requested.emit(source_slot, dest_slot, split_count) - - return { - "ok": true, - "pending": true, - "source_slot": source_slot, - "target_slot": dest_slot, - "remaining_source": current_count - split_count, - "split_count": split_count, - "msg": "已发送拆分请求,等待服务器确认!" - } - -# 快捷批量出售给商人 (Ctrl + 右键) -func quick_sell_item( - slot_idx: int, - inventory: Array, - player_data: Dictionary, - client: Node = null, - window: int = 1 -) -> Dictionary: - if slot_idx < 0 or slot_idx >= inventory.size(): - return {"ok": false, "reason": "INVALID_SLOT", "msg": "无效的栏位!"} - - var item = inventory[slot_idx] - if item == null: - return {"ok": false, "reason": "EMPTY_SLOT", "msg": "该格子上没有物品!"} - - # 加锁保护 - if item.get("is_locked", false): - return {"ok": false, "reason": "ITEM_LOCKED", "msg": "已加锁的物品严禁出售!"} - if window != 1: - return {"ok": false, "reason": "INVALID_SELL_WINDOW", "msg": "只能出售背包中的物品!"} - - var count: int = int(item.get("count", 1)) - # 40250's SELL/SELL2 response supplies the authoritative price and the - # subsequent item/gold updates. Do not use sale_price/player_data as a local - # substitute for the server result. - if client == null or not client.has_method("shop_sell"): - return {"ok": false, "reason": "NETWORK_CLIENT_REQUIRED", "msg": "出售需要连接服务器确认!"} - if not client.shop_sell(slot_idx, count): - return {"ok": false, "reason": "SEND_FAILED", "msg": "出售请求发送失败!"} - - var iname: String = item.get("name", "物品") - item_quick_sell_requested.emit(slot_idx, count, iname) - - return { - "ok": true, - "pending": true, - "slot_idx": slot_idx, - "count": count, - "item_name": iname, - "msg": "已发送出售请求,等待服务器确认!" - } - -# 高价值物品丢弃/销毁防误触检查 -func check_drop_safety(item: Dictionary) -> Dictionary: - if item.is_empty(): - return {"can_drop": false} - - if item.get("is_locked", false): - return { - "can_drop": false, - "require_confirm": false, - "reason": "ITEM_LOCKED", - "msg": "已加锁的贵重物品无法丢弃!" - } - - var refine_level: int = int(item.get("refine_level", 0)) - var has_attrs: bool = item.has("attributes") and not item["attributes"].is_empty() - - if refine_level >= 7 or has_attrs: - return { - "can_drop": true, - "require_confirm": true, - "msg": "此装备强化等级较高或拥有附加属性,属于贵重装备!确定要丢弃吗?" - } - - return { - "can_drop": true, - "require_confirm": false - } diff --git a/project/item_split_system.gd.uid b/project/item_split_system.gd.uid deleted file mode 100644 index 8ea500e4..00000000 --- a/project/item_split_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bwpb3evcv5t6r diff --git a/project/jewelry_socket_system.gd b/project/jewelry_socket_system.gd deleted file mode 100644 index 4a744495..00000000 --- a/project/jewelry_socket_system.gd +++ /dev/null @@ -1,175 +0,0 @@ -# jewelry_socket_system.gd —— Metin2 40250 首饰饰品金刚石打孔与精炼宝石镶嵌 1:1 -# 对照 40250 服务端 char_item.cpp (PutGemInAccessory), item_attribute.cpp -class_name JewelrySocketSystem -extends RefCounted - -const ITEM_DIAMOND := 50621 # 精炼金刚石 -const MAX_SOCKETS := 3 -const GEM_LIFETIME_SECS := 86400.0 # 24 小时战斗佩戴寿命 - -# 饰品支持打孔的分类 (14:手镯, 16:项链, 17:耳环) -const VALID_ACCESSORY_PREFIXES := [14, 16, 17] - -# 饰品材质与对应精炼宝石映射 (base_vnum_pattern -> gem_vnum) -const ACCESSORY_GEM_MAPPING: Dictionary = { - "copper": 50624, # 铜首饰 -> 铜 50624 - "silver": 50625, # 银首饰 -> 银 50625 - "gold": 50626, # 金首饰 -> 金 50626 - "jade": 50627, # 玉首饰 -> 玉 50627 - "ebony": 50628, # 黑檀首饰 -> 黑檀 50628 - "pearl": 50629, # 珍珠首饰 -> 珍珠 50629 - "white_gold": 50630, # 白金首饰 -> 白金 50630 - "crystal": 50631, # 水晶首饰 -> 水晶 50631 - "amethyst": 50632, # 紫晶首饰 -> 紫晶 50632 - "heavens_tear": 50633,# 天泪首饰 -> 天泪石 50633 - "soul_crystal": 50634,# 灵魂水晶首饰 -> 灵魂水晶 50634 - "ruby": 50635, # 红宝石首饰 -> 红宝石 50635 - "garnet": 50636, # 石榴石首饰 -> 石榴石 50636 - "emerald": 50637, # 绿宝石首饰 -> 绿宝石 50637 - "sapphire": 50638 # 蓝宝石首饰 -> 蓝宝石 50638 -} - -# 打孔成功率 (0孔打第1孔: 70%, 1孔打第2孔: 50%, 2孔打第3孔: 30%) -const DRILL_CHANCES: Array[float] = [0.70, 0.50, 0.30] - -# 宝石阶梯增益百分比 (第1孔: +10%, 第2孔: +10%, 第3孔: +20%) -const GEM_BONUS_TIERS: Array[float] = [0.10, 0.10, 0.20] - -static func is_accessory(item: Dictionary) -> bool: - var vnum: int = int(item.get("vnum", 0)) - var prefix: int = int(floor(float(vnum) / 1000.0)) - return prefix in VALID_ACCESSORY_PREFIXES - -# 检查是否可用金刚石打孔 -static func can_drill_socket(item: Dictionary) -> Dictionary: - if not is_accessory(item): - return {"ok": false, "reason": "NOT_ACCESSORY", "msg": "金刚石只能用于手镯、项链或耳环打孔!"} - - var socket_count: int = int(item.get("socket_count", 0)) - if socket_count >= MAX_SOCKETS: - return {"ok": false, "reason": "SOCKETS_FULL", "msg": "该首饰已拥有最大的 3 个宝石孔槽!"} - - return {"ok": true, "current_sockets": socket_count} - -# 使用金刚石 50621 打孔 -static func drill_socket(item: Dictionary, diamond_slot_item: Dictionary, force_success: bool = false) -> Dictionary: - var check = can_drill_socket(item) - if not check["ok"]: - return check - - var dvnum: int = int(diamond_slot_item.get("vnum", 0)) - if dvnum != ITEM_DIAMOND: - return {"ok": false, "reason": "NOT_DIAMOND", "msg": "打孔必须使用精炼金刚石!"} - - var current_sockets: int = int(item.get("socket_count", 0)) - var chance: float = DRILL_CHANCES[current_sockets] - var roll: float = 0.0 if force_success else randf() - var success: bool = roll < chance - - if not item.has("sockets"): - item["sockets"] = [0, 0, 0] - if not item.has("gem_times"): - item["gem_times"] = [0.0, 0.0, 0.0] - - if success: - item["socket_count"] = current_sockets + 1 - return { - "ok": true, - "success": true, - "socket_count": item["socket_count"], - "msg": "打孔成功!已开辟出第 %d 个宝石孔槽!" % item["socket_count"] - } - else: - return { - "ok": true, - "success": false, - "socket_count": current_sockets, - "msg": "打孔失败!金刚石破碎,未能开出孔槽。" - } - -# 镶嵌精炼矿石宝石 -static func insert_gem(item: Dictionary, gem_item: Dictionary, required_gem_vnum: int = 0) -> Dictionary: - if not is_accessory(item): - return {"ok": false, "reason": "NOT_ACCESSORY"} - - var socket_count: int = int(item.get("socket_count", 0)) - if socket_count <= 0: - return {"ok": false, "reason": "NO_SOCKETS_DRILLED", "msg": "该首饰尚未打孔,请先使用金刚石开孔!"} - - if not item.has("sockets"): - item["sockets"] = [0, 0, 0] - if not item.has("gem_times"): - item["gem_times"] = [0.0, 0.0, 0.0] - - # 查找第一个空孔 - var empty_slot := -1 - for i in range(socket_count): - if int(item["sockets"][i]) == 0: - empty_slot = i - break - - if empty_slot == -1: - return {"ok": false, "reason": "NO_EMPTY_SOCKET", "msg": "所有已开辟的孔槽都已镶嵌满宝石!"} - - var gem_vnum: int = int(gem_item.get("vnum", 0)) - if gem_vnum < 50622 or gem_vnum > 50638: - return {"ok": false, "reason": "INVALID_GEM", "msg": "这不是有效的精炼宝石!"} - - if required_gem_vnum > 0 and gem_vnum != required_gem_vnum: - return {"ok": false, "reason": "GEM_MISMATCH", "msg": "此宝石与该首饰材质不匹配!"} - - # 镶嵌成功 - item["sockets"][empty_slot] = gem_vnum - item["gem_times"][empty_slot] = GEM_LIFETIME_SECS - - var active_gems := count_active_gems(item) - var bonus_pct := calculate_bonus_percentage(active_gems) - - return { - "ok": true, - "slot": empty_slot, - "gem_vnum": gem_vnum, - "active_gems": active_gems, - "total_bonus_pct": bonus_pct, - "lifetime": GEM_LIFETIME_SECS, - "msg": "宝石成功镶入第 %d 个孔槽!固有属性提升 %d%%!" % [empty_slot + 1, int(round(bonus_pct * 100))] - } - -# 统计当前生效的宝石数量 -static func count_active_gems(item: Dictionary) -> int: - var count := 0 - var sockets: Array = item.get("sockets", []) - var times: Array = item.get("gem_times", []) - for i in range(sockets.size()): - if int(sockets[i]) > 0 and float(times[i]) > 0.0: - count += 1 - return count - -# 计算宝石提供的总属性加成百分比 (0~3 颗宝石) -static func calculate_bonus_percentage(gem_count: int) -> float: - var pct: float = 0.0 - for i in range(mini(gem_count, GEM_BONUS_TIERS.size())): - pct += GEM_BONUS_TIERS[i] - return pct - -# 佩戴战斗时间自然衰减 (dt: 秒) -static func update_gem_endurance(item: Dictionary, dt: float) -> Dictionary: - var expired_gems: Array[int] = [] - if not item.has("sockets") or not item.has("gem_times"): - return {"expired": expired_gems} - - var sockets: Array = item["sockets"] - var times: Array = item["gem_times"] - - for i in range(sockets.size()): - if int(sockets[i]) > 0 and float(times[i]) > 0.0: - times[i] = maxf(0.0, float(times[i]) - dt) - if float(times[i]) <= 0.0: - expired_gems.append(int(sockets[i])) - sockets[i] = 0 # 宝石破碎,孔槽重新清空 - - return { - "expired": expired_gems, - "active_gems": count_active_gems(item), - "total_bonus_pct": calculate_bonus_percentage(count_active_gems(item)) - } diff --git a/project/jewelry_socket_system.gd.uid b/project/jewelry_socket_system.gd.uid deleted file mode 100644 index 41afb957..00000000 --- a/project/jewelry_socket_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bbfkjq1coiykj diff --git a/project/lucky_lottery_fortune_system.gd b/project/lucky_lottery_fortune_system.gd deleted file mode 100644 index 481f440c..00000000 --- a/project/lucky_lottery_fortune_system.gd +++ /dev/null @@ -1,277 +0,0 @@ -# lucky_lottery_fortune_system.gd —— Metin2 40250 官方幸运乐透刮刮乐与老妪占卜转运系统 1:1 -# 100% 对照 40250 服务端 fortune_telling.quest, xxx_change_to_lotto.quest, questlua_pc.cpp (give_lotto) -class_name LuckyLotteryFortuneSystem -extends RefCounted - -signal fortune_told(tier: String, exp_bonus: float, gold_bonus: float, desc: String) -signal talisman_acquired(item_vnum: int, cost: int) -signal lottery_scratched(digits: Array, prize_tier: String, prize_gold: int, prize_item_vnum: int) -signal system_error(reason: String) - -# NPC 与道具 Vnum 常量 -const NPC_OLD_WOMAN := 9006 # 老妪 (Old Woman) -const FORTUNE_COST_GOLD := 7777 # 占卜消耗金币 (40250 原版 7777) -const TALISMAN_COST_GOLD := 15000 # 辟邪转运符费用 -const VNUM_LOTTO_TICKET := 50027 # 乐透彩票奖券 (Lotterielos) -const VNUM_LOTTO_BOOK := 50001 # 幸运奖券号 (Glücksbuch) -const VNUM_TALISMAN := 70052 # 转运符咒 (Karma Talisman) -const VNUM_BLESSING_SCROLL := 25040 # 祝福卷轴 (特等奖附赠) - -# 占卜卦象运势常量 -const TIER_GREAT_LUCK := "great_luck" # 大吉 (+10% 经验, +10% 金币掉率) -const TIER_GOOD_LUCK := "good_luck" # 中吉 (+5% 经验, +5% 金币掉率) -const TIER_NORMAL := "normal" # 平 (无额外加成) -const TIER_BAD_LUCK := "bad_luck" # 凶 (-10% 金币掉率) - -const FORTUNE_DESCRIPTIONS: Dictionary = { - TIER_GREAT_LUCK: { - "name": "【大吉·紫气东来】", - "text": "老妪眯起双眼惊叹道:“祥云笼罩,鸿运当头!你今天的所有修行与征战都将得到福星的高照庇护!”", - "exp_bonus": 0.10, - "gold_bonus": 0.10 - }, - TIER_GOOD_LUCK: { - "name": "【中吉·吉星照拂】", - "text": "老妪慈祥地微笑道:“运势平稳向上,行事多有小顺,今日野外历练收获定然不错。”", - "exp_bonus": 0.05, - "gold_bonus": 0.05 - }, - TIER_NORMAL: { - "name": "【平常·云淡风轻】", - "text": "老妪看了看签文道:“波澜不惊,顺其自然。一切还需依靠你自身的勤勉苦练。”", - "exp_bonus": 0.0, - "gold_bonus": 0.0 - }, - TIER_BAD_LUCK: { - "name": "【大凶·晦暗蒙尘】", - "text": "老妪脸色微微一变:“乌云蔽日,近来出行需万分小心,财运恐有所折损!老身这里有一枚开光转运符……”", - "exp_bonus": 0.0, - "gold_bonus": -0.10 - } -} - -# 当前玩家占卜运势状态 -var current_fortune_tier: String = "" -var fortune_time_remaining: float = 0.0 # 40250 原版持续 8 小时 (28800 秒) -var has_karma_talisman: bool = false -var lotto_history: Array[Dictionary] = [] - -# ========================================== -# 1. 老妪占卜祈福 (fortune_telling.quest) -# ========================================== -func tell_fortune( - player_level: int, - player_gold: int, - forced_tier: String = "" -) -> Dictionary: - # 等级需达到 20 级 - if player_level <= 20: - system_error.emit("LEVEL_TOO_LOW") - return { - "ok": false, - "reason": "LEVEL_TOO_LOW", - "msg": "老妪摇了摇头:“年轻的冒险者,你还不满 20 级,未来的命运尚在凝聚之中,现在还不是占卜的时候。”" - } - - # 检查金币 - if player_gold < FORTUNE_COST_GOLD: - system_error.emit("NOT_ENOUGH_GOLD") - return { - "ok": false, - "reason": "NOT_ENOUGH_GOLD", - "msg": "占卜诚意金不足!老妪需要 %d 枚金币作为供奉卦金。" % FORTUNE_COST_GOLD - } - - var remaining_gold = player_gold - FORTUNE_COST_GOLD - - # 判定运势 - var picked_tier = "" - if forced_tier != "" and FORTUNE_DESCRIPTIONS.has(forced_tier): - picked_tier = forced_tier - else: - var r = randi_range(1, 100) - if r <= 20: - picked_tier = TIER_GREAT_LUCK - elif r <= 50: - picked_tier = TIER_GOOD_LUCK - elif r <= 80: - picked_tier = TIER_NORMAL - else: - picked_tier = TIER_BAD_LUCK - - current_fortune_tier = picked_tier - fortune_time_remaining = 28800.0 # 8 小时 - has_karma_talisman = false - - var info = FORTUNE_DESCRIPTIONS[picked_tier] - var can_buy_talisman = (picked_tier == TIER_BAD_LUCK or picked_tier == TIER_NORMAL) - - fortune_told.emit(picked_tier, info["exp_bonus"], info["gold_bonus"], info["text"]) - - return { - "ok": true, - "tier": picked_tier, - "tier_name": info["name"], - "text": info["text"], - "exp_bonus": info["exp_bonus"], - "gold_bonus": info["gold_bonus"], - "remaining_gold": remaining_gold, - "can_buy_talisman": can_buy_talisman, - "talisman_cost": TALISMAN_COST_GOLD - } - -# 购买转运符咒化解凶煞 -func buy_karma_talisman(inventory: Array, player_gold: int) -> Dictionary: - if player_gold < TALISMAN_COST_GOLD: - system_error.emit("NOT_ENOUGH_GOLD_TALISMAN") - return {"ok": false, "reason": "NOT_ENOUGH_GOLD_TALISMAN", "msg": "购买辟邪转运符金币不足!需要 15,000 金币。"} - - var remaining_gold = player_gold - TALISMAN_COST_GOLD - _give_or_stack_item(inventory, VNUM_TALISMAN, 1) - has_karma_talisman = true - - # 若当前为大凶,直接洗去负面金币减成,转化为中吉 - if current_fortune_tier == TIER_BAD_LUCK or current_fortune_tier == TIER_NORMAL: - current_fortune_tier = TIER_GOOD_LUCK - - talisman_acquired.emit(VNUM_TALISMAN, TALISMAN_COST_GOLD) - return { - "ok": true, - "talisman_vnum": VNUM_TALISMAN, - "new_tier": current_fortune_tier, - "remaining_gold": remaining_gold, - "msg": "你佩戴上了老妪开光的辟邪转运符,周身晦暗之气顿时消散,运势转危为安!" - } - -# ========================================== -# 2. 乐透刮刮乐奖券开奖 (xxx_change_to_lotto.quest) -# ========================================== -func play_scratch_lottery( - inventory: Array, - ticket_slot: int, - forced_digits: Array = [] -) -> Dictionary: - if ticket_slot < 0 or ticket_slot >= inventory.size() or inventory[ticket_slot] == null: - system_error.emit("INVALID_TICKET_SLOT") - return {"ok": false, "reason": "INVALID_TICKET_SLOT", "msg": "未找到指定的乐透彩票!"} - - var item = inventory[ticket_slot] - var vnum = int(item.get("vnum", 0)) - if vnum != VNUM_LOTTO_TICKET and vnum != VNUM_LOTTO_BOOK: - system_error.emit("NOT_A_LOTTO_TICKET") - return {"ok": false, "reason": "NOT_A_LOTTO_TICKET", "msg": "该物品并不是乐透彩票奖券!"} - - # 消耗 1 张彩票 - _consume_item_one(inventory, ticket_slot) - - # 刮开 3 位幸运数字 (1~9) - var digits: Array = [] - if forced_digits.size() == 3: - digits = forced_digits.duplicate() - else: - digits = [randi_range(1, 9), randi_range(1, 9), randi_range(1, 9)] - - var d1 = int(digits[0]) - var d2 = int(digits[1]) - var d3 = int(digits[2]) - - var prize_tier := "" - var prize_gold := 0 - var prize_item := 0 - var prize_name := "" - - if d1 == d2 and d2 == d3: - # 三星连珠:特等大奖 (1,000,000 金币 + 祝福卷轴 25040) - prize_tier = "grand_jackpot" - prize_gold = 1000000 - prize_item = VNUM_BLESSING_SCROLL - prize_name = "【特等头奖】1,000,000 金币 + 祝福卷轴" - _give_or_stack_item(inventory, prize_item, 1) - elif d1 == d2 or d2 == d3 or d1 == d3: - # 双星闪耀:二等奖 (200,000 金币) - prize_tier = "second_prize" - prize_gold = 200000 - prize_item = 0 - prize_name = "【二等奖】200,000 金币" - else: - # 安慰奖:30,000 金币 - prize_tier = "consolation_prize" - prize_gold = 30000 - prize_item = 0 - prize_name = "【幸运鼓励奖】30,000 金币" - - var record = { - "digits": digits, - "prize_tier": prize_tier, - "prize_gold": prize_gold, - "prize_item": prize_item - } - lotto_history.append(record) - lottery_scratched.emit(digits, prize_tier, prize_gold, prize_item) - - return { - "ok": true, - "digits": digits, - "prize_tier": prize_tier, - "prize_name": prize_name, - "prize_gold": prize_gold, - "prize_item": prize_item, - "msg": "刮开刮刮乐涂层,赫然显现数字 [%d, %d, %d]!恭喜斩获 %s!" % [d1, d2, d3, prize_name] - } - -# 获取当前占卜增益属性 -func get_active_fortune_bonus() -> Dictionary: - if current_fortune_tier == "" or fortune_time_remaining <= 0.0: - return {"exp_bonus": 0.0, "gold_bonus": 0.0, "tier": ""} - var info = FORTUNE_DESCRIPTIONS[current_fortune_tier] - return { - "tier": current_fortune_tier, - "exp_bonus": info["exp_bonus"], - "gold_bonus": info["gold_bonus"], - "time_remaining": fortune_time_remaining - } - -# 时间推移 -func update(delta: float) -> void: - if fortune_time_remaining > 0.0: - fortune_time_remaining -= delta - if fortune_time_remaining <= 0.0: - fortune_time_remaining = 0.0 - current_fortune_tier = "" - -# 序列化与反序列化 -func serialize() -> Dictionary: - return { - "current_fortune_tier": current_fortune_tier, - "fortune_time_remaining": fortune_time_remaining, - "has_karma_talisman": has_karma_talisman, - "lotto_history": lotto_history.duplicate(true) - } - -func deserialize(data: Dictionary) -> void: - current_fortune_tier = str(data.get("current_fortune_tier", "")) - fortune_time_remaining = float(data.get("fortune_time_remaining", 0.0)) - has_karma_talisman = bool(data.get("has_karma_talisman", false)) - lotto_history = data.get("lotto_history", []).duplicate(true) - -# 内部消耗与堆叠工具 -func _consume_item_one(inventory: Array, slot: int) -> void: - if slot < 0 or slot >= inventory.size() or inventory[slot] == null: - return - var item = inventory[slot] - var count = int(item.get("count", 1)) - if count > 1: - item["count"] = count - 1 - else: - inventory[slot] = null - -func _give_or_stack_item(inventory: Array, vnum: int, count: int) -> void: - for it in inventory: - if it != null and int(it.get("vnum", 0)) == vnum: - it["count"] = int(it.get("count", 1)) + count - return - for i in range(inventory.size()): - if inventory[i] == null: - inventory[i] = {"vnum": vnum, "count": count} - return - inventory.append({"vnum": vnum, "count": count}) diff --git a/project/lucky_lottery_fortune_system.gd.uid b/project/lucky_lottery_fortune_system.gd.uid deleted file mode 100644 index 40dd2a4f..00000000 --- a/project/lucky_lottery_fortune_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://u4eytsf1mh0k diff --git a/project/mail_system.gd b/project/mail_system.gd deleted file mode 100644 index 68e8dc66..00000000 --- a/project/mail_system.gd +++ /dev/null @@ -1,213 +0,0 @@ -# mail_system.gd —— Metin2 40250 游戏内邮箱系统与附件寄送提取 1:1 -# 对照 40250 服务端 mailbox.cpp, input_db.cpp, uimailbox.py -class_name MailSystem -extends RefCounted - -signal mail_sent(mail_id: int, sender: String, recipient: String) -signal mail_received(mail_id: int, recipient: String, title: String) -signal unread_count_changed(recipient: String, count: int) -signal attachment_claimed(mail_id: int) - -const POSTAGE_FEE := 1000 # 寄件固定邮资 1000 金币 -const MAX_TITLE_LENGTH := 25 # 标题最长字符 -const MAX_BODY_LENGTH := 100 # 正文最长字符 -const MAIL_EXPIRATION_SECONDS := 2592000.0 # 30 天有效期 (30 * 86400) - -var mails: Array = [] -var next_mail_id: int = 1 - -# 寄送邮件 -func send_mail( - sender: String, - recipient: String, - title: String, - body: String, - player_data: Dictionary, - inventory: Array, - attach_slot: int = -1, - attach_gold: int = 0, - current_time: float = 0.0 -) -> Dictionary: - if recipient.strip_edges().is_empty(): - return {"ok": false, "reason": "EMPTY_RECIPIENT", "msg": "收件人不能为空!"} - - if title.length() > MAX_TITLE_LENGTH: - return {"ok": false, "reason": "TITLE_TOO_LONG", "msg": "邮件标题过长!"} - - if body.length() > MAX_BODY_LENGTH: - return {"ok": false, "reason": "BODY_TOO_LONG", "msg": "邮件正文过长!"} - - if attach_gold < 0: - return {"ok": false, "reason": "INVALID_GOLD", "msg": "附加金币数额无效!"} - - # 检查玩家金币 (邮资 1000 + 附加金币) - var total_gold_cost := POSTAGE_FEE + attach_gold - var player_gold: int = int(player_data.get("gold", 0)) - if player_gold < total_gold_cost: - return { - "ok": false, - "reason": "INSUFFICIENT_YANG", - "required": total_gold_cost, - "current": player_gold, - "msg": "金币不足以支付邮资与附件!需要 %d 金币。" % total_gold_cost - } - - # 检查附加物品 - var attached_item = null - if attach_slot >= 0: - if attach_slot >= inventory.size() or inventory[attach_slot] == null: - return {"ok": false, "reason": "INVALID_ATTACH_ITEM", "msg": "所选格子没有物品!"} - var item = inventory[attach_slot] - if item.get("is_locked", false): - return {"ok": false, "reason": "ITEM_LOCKED", "msg": "已加锁的物品无法邮寄!"} - attached_item = item.duplicate(true) - inventory[attach_slot] = null - - # 扣除金币 - player_data["gold"] = player_gold - total_gold_cost - - # 创建邮件 - var mail_id := next_mail_id - next_mail_id += 1 - - var mail_entry: Dictionary = { - "id": mail_id, - "sender": sender, - "recipient": recipient, - "title": title, - "body": body, - "send_time": current_time, - "expire_time": current_time + MAIL_EXPIRATION_SECONDS, - "is_read": false, - "attached_gold": attach_gold, - "attached_item": attached_item, - "is_gold_claimed": (attach_gold == 0), - "is_item_claimed": (attached_item == null) - } - - mails.append(mail_entry) - - mail_sent.emit(mail_id, sender, recipient) - mail_received.emit(mail_id, recipient, title) - unread_count_changed.emit(recipient, get_unread_count(recipient)) - - return { - "ok": true, - "mail_id": mail_id, - "msg": "邮件已成功寄出!" - } - -# 获取指定收件人的收件箱 -func get_inbox(recipient: String) -> Array: - var list: Array = [] - for m in mails: - if m["recipient"] == recipient: - list.append(m) - return list - -# 查阅邮件 -func read_mail(mail_id: int, current_player: String) -> Dictionary: - for m in mails: - if m["id"] == mail_id and m["recipient"] == current_player: - if not m["is_read"]: - m["is_read"] = true - unread_count_changed.emit(current_player, get_unread_count(current_player)) - return {"ok": true, "mail": m.duplicate(true)} - return {"ok": false, "reason": "MAIL_NOT_FOUND", "msg": "未找到该邮件!"} - -# 提取邮件附件 (金币与物品) -func claim_attachments( - mail_id: int, - current_player: String, - player_data: Dictionary, - inventory: Array -) -> Dictionary: - var target_mail = null - for m in mails: - if m["id"] == mail_id and m["recipient"] == current_player: - target_mail = m - break - - if target_mail == null: - return {"ok": false, "reason": "MAIL_NOT_FOUND", "msg": "未找到指定邮件!"} - - var claimed_gold: int = 0 - var claimed_item = null - - # 提取金币 - if not target_mail["is_gold_claimed"] and target_mail["attached_gold"] > 0: - claimed_gold = target_mail["attached_gold"] - player_data["gold"] = int(player_data.get("gold", 0)) + claimed_gold - target_mail["attached_gold"] = 0 - target_mail["is_gold_claimed"] = true - - # 提取物品 - if not target_mail["is_item_claimed"] and target_mail["attached_item"] != null: - # 寻找背包空位 - var empty_slot := -1 - for i in range(inventory.size()): - if inventory[i] == null: - empty_slot = i - break - - if empty_slot == -1: - return { - "ok": false, - "reason": "INVENTORY_FULL", - "claimed_gold": claimed_gold, - "msg": "背包已满,无法提取物品附件!" - } - - claimed_item = target_mail["attached_item"].duplicate(true) - inventory[empty_slot] = claimed_item - target_mail["attached_item"] = null - target_mail["is_item_claimed"] = true - - attachment_claimed.emit(mail_id) - - return { - "ok": true, - "claimed_gold": claimed_gold, - "claimed_item": claimed_item, - "msg": "附件提取成功!" - } - -# 删除邮件 -func delete_mail(mail_id: int, current_player: String) -> Dictionary: - for i in range(mails.size()): - var m = mails[i] - if m["id"] == mail_id and m["recipient"] == current_player: - # 检查是否有未提取的附件 - if not m["is_gold_claimed"] or not m["is_item_claimed"]: - return { - "ok": false, - "reason": "HAS_UNCLAIMED_ATTACHMENTS", - "msg": "邮件内包含尚未提取的附件,无法删除!" - } - - mails.remove_at(i) - unread_count_changed.emit(current_player, get_unread_count(current_player)) - return {"ok": true, "msg": "邮件已删除。"} - - return {"ok": false, "reason": "MAIL_NOT_FOUND", "msg": "未找到指定邮件!"} - -# 获取未读邮件数量 -func get_unread_count(recipient: String) -> int: - var cnt := 0 - for m in mails: - if m["recipient"] == recipient and not m["is_read"]: - cnt += 1 - return cnt - -# 过期邮件清理 (30天自动销毁) -func cleanup_expired_mails(current_time: float) -> int: - var removed := 0 - var i := mails.size() - 1 - while i >= 0: - if current_time >= mails[i]["expire_time"]: - var recipient: String = mails[i]["recipient"] - mails.remove_at(i) - removed += 1 - unread_count_changed.emit(recipient, get_unread_count(recipient)) - i -= 1 - return removed diff --git a/project/mail_system.gd.uid b/project/mail_system.gd.uid deleted file mode 100644 index a3ff100b..00000000 --- a/project/mail_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bm8yqmakmou63 diff --git a/project/mall_affect_system.gd b/project/mall_affect_system.gd deleted file mode 100644 index 842167f4..00000000 --- a/project/mall_affect_system.gd +++ /dev/null @@ -1,194 +0,0 @@ -# mall_affect_system.gd —— Metin2 40250 商城常驻 Affect 增益与双倍特权系统 1:1 -# 对照 40250 服务端 affect.h, char_affect.cpp 及客户端 uiaffectshower.py:647-670 -class_name MallAffectSystem -extends RefCounted - -signal affect_added(affect_type: int, data: Dictionary) -signal affect_removed(affect_type: int) - -# 40250 官方常驻增益 Affect 常量 (uiaffectshower.py) -const AFFECT_EXP_BONUS := 500 -const AFFECT_ITEM_BONUS := 501 -const AFFECT_SAFEBOX := 502 -const AFFECT_AUTOLOOT := 503 -const AFFECT_FISH_MIND := 504 -const AFFECT_MARRIAGE_FAST := 505 -const AFFECT_GOLD_BONUS := 506 -const AFFECT_NO_DEATH_PENALTY := 507 -const AFFECT_AUTO_HP_RECOVERY := 508 -const AFFECT_AUTO_SP_RECOVERY := 509 - -# 官方图标资源映射 (uiaffectshower.py:647-670) -const AFFECT_INFO: Dictionary = { - AFFECT_EXP_BONUS: { - "name": "经验值获取提升", - "icon_sub": "d:/ymir work/ui/skill/common/affect/exp_bonus.sub", - "desc": "打怪获得的经验值提升!" - }, - AFFECT_ITEM_BONUS: { - "name": "盗贼手套掉宝增益", - "icon_sub": "d:/ymir work/ui/skill/common/affect/item_bonus.sub", - "desc": "击杀怪物掉落物品的概率翻倍!" - }, - AFFECT_SAFEBOX: { - "name": "仓库空间扩展", - "icon_sub": "d:/ymir work/ui/skill/common/affect/safebox.sub", - "desc": "享有二号与三号高级扩展储物箱!" - }, - AFFECT_AUTOLOOT: { - "name": "自动拾取金币", - "icon_sub": "d:/ymir work/ui/skill/common/affect/autoloot.sub", - "desc": "击杀怪物掉落的金币自动拾取至钱包。" - }, - AFFECT_FISH_MIND: { - "name": "渔神专注", - "icon_sub": "d:/ymir work/ui/skill/common/affect/fishmind.sub", - "desc": "钓鱼成功率获得大幅提升。" - }, - AFFECT_MARRIAGE_FAST: { - "name": "爱神羽毛", - "icon_sub": "d:/ymir work/ui/skill/common/affect/marriage_fast.sub", - "desc": "同伴好感度积累速度倍增。" - }, - AFFECT_GOLD_BONUS: { - "name": "幸运金币", - "icon_sub": "d:/ymir work/ui/skill/common/affect/gold_bonus.sub", - "desc": "怪物掉落的金币数量翻倍!" - }, - AFFECT_AUTO_HP_RECOVERY: { - "name": "太阳仙丹", - "icon_sub": "d:/ymir work/ui/pattern/auto_hpgauge/05.dds", - "desc": "生命值低于阈值时自动回复。" - }, - AFFECT_AUTO_SP_RECOVERY: { - "name": "月亮仙丹", - "icon_sub": "d:/ymir work/ui/pattern/auto_spgauge/05.dds", - "desc": "法力值低于阈值时自动回复。" - } -} - -# 经典商城道具 Vnum 映射 -const MALL_ITEM_TABLE: Dictionary = { - 70005: {"affect": AFFECT_EXP_BONUS, "duration": 3600.0, "value": 50}, # 经验戒指 1小时 (+50%) - 72001: {"affect": AFFECT_EXP_BONUS, "duration": 604800.0, "value": 100}, # 经验戒指 7天 (+100%) - 70043: {"affect": AFFECT_ITEM_BONUS, "duration": 7200.0, "value": 100}, # 盗贼手套 2小时 (2x) - 72004: {"affect": AFFECT_ITEM_BONUS, "duration": 604800.0, "value": 100}, # 盗贼手套 7天 (2x) - 70044: {"affect": AFFECT_GOLD_BONUS, "duration": 10800.0, "value": 100}, # 幸运金币 3小时 (2x) - 72007: {"affect": AFFECT_GOLD_BONUS, "duration": 604800.0, "value": 100}, # 幸运金币 7天 (2x) - 72006: {"affect": AFFECT_SAFEBOX, "duration": 604800.0, "value": 1}, # 仓库扩展卷 7天 - 71069: {"affect": AFFECT_MARRIAGE_FAST, "duration": 604800.0, "value": 1} # 爱神羽毛 7天 -} - -# 当前生效的增益字典: affect_type -> {name, icon_sub, value, duration, total_duration, desc} -var active_affects: Dictionary = {} - -func use_mall_item(item: Dictionary) -> Dictionary: - var vnum: int = int(item.get("vnum", 0)) - if not MALL_ITEM_TABLE.has(vnum): - return {"ok": false, "reason": "NOT_MALL_ITEM", "msg": "这不是商城特权道具!"} - - var cfg: Dictionary = MALL_ITEM_TABLE[vnum] - var atype: int = int(cfg["affect"]) - var dur: float = float(cfg["duration"]) - var val: int = int(cfg["value"]) - - add_affect(atype, dur, val) - - var info: Dictionary = AFFECT_INFO.get(atype, {}) - return { - "ok": true, - "affect_type": atype, - "name": info.get("name", "特权增益"), - "duration": dur, - "value": val, - "msg": "已成功激活【%s】特权!" % info.get("name", "") - } - -func add_affect(atype: int, duration: float, value: int = 0) -> void: - var info: Dictionary = AFFECT_INFO.get(atype, { - "name": "未知特权", - "icon_sub": "", - "desc": "" - }) - - var data: Dictionary = { - "name": info["name"], - "icon_sub": info["icon_sub"], - "value": value, - "duration": duration, - "total_duration": duration, - "desc": info["desc"] - } - - active_affects[atype] = data - affect_added.emit(atype, data) - -func remove_affect(atype: int) -> void: - if active_affects.has(atype): - active_affects.erase(atype) - affect_removed.emit(atype) - -func has_affect(atype: int) -> bool: - return active_affects.has(atype) - -func get_affect(atype: int) -> Dictionary: - return active_affects.get(atype, {}) - -# 经验倍率加成 (char.cpp:PointChange) -func get_exp_multiplier() -> float: - var mult: float = 1.0 - if has_affect(AFFECT_EXP_BONUS): - var bonus_pct: float = float(active_affects[AFFECT_EXP_BONUS].get("value", 50)) - mult += bonus_pct / 100.0 - return mult - -# 物品掉率倍率加成 (item.cpp:Drop) -func get_drop_multiplier() -> float: - if has_affect(AFFECT_ITEM_BONUS): - return 2.0 - return 1.0 - -# 金币掉落倍率加成 (char_battle.cpp:GiveGold) -func get_gold_multiplier() -> float: - if has_affect(AFFECT_GOLD_BONUS): - return 2.0 - return 1.0 - -# 仓库扩充权限 (safebox.cpp) -func has_safebox_expansion() -> bool: - return has_affect(AFFECT_SAFEBOX) - -# 逐帧衰减更新 -func update(dt: float) -> Array[int]: - var expired: Array[int] = [] - for atype in active_affects.keys(): - var data: Dictionary = active_affects[atype] - data["duration"] = maxf(0.0, float(data["duration"]) - dt) - if float(data["duration"]) <= 0.0: - expired.append(atype) - - for atype in expired: - remove_affect(atype) - - return expired - -# 获取供 HUD 渲染的图标列表 -func get_hud_icons() -> Array[Dictionary]: - var icons: Array[Dictionary] = [] - for atype in active_affects: - var data: Dictionary = active_affects[atype] - var total_sec := int(ceil(float(data["duration"]))) - var hours := total_sec / 3600 - var mins := (total_sec % 3600) / 60 - var secs := total_sec % 60 - var time_str := "%02d:%02d:%02d" % [hours, mins, secs] if hours > 0 else "%02d:%02d" % [mins, secs] - - icons.append({ - "affect_type": atype, - "name": data["name"], - "icon_sub": data["icon_sub"], - "time_remaining_str": time_str, - "remaining_seconds": float(data["duration"]), - "desc": data["desc"] - }) - return icons diff --git a/project/mall_affect_system.gd.uid b/project/mall_affect_system.gd.uid deleted file mode 100644 index 667e55ee..00000000 --- a/project/mall_affect_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://5m0rml3w6scf diff --git a/project/map_name_shower_system.gd b/project/map_name_shower_system.gd deleted file mode 100644 index 533a2ada..00000000 --- a/project/map_name_shower_system.gd +++ /dev/null @@ -1,168 +0,0 @@ -# map_name_shower_system.gd -# 40250 官方 1:1 地图切换横幅渐显与魔塔/恶魔陵墓楼层识别系统 -# 对照: uimapnameshower.py, locale_game.txt -class_name MapNameShowerSystem -extends RefCounted - -enum State { - STATE_HIDE = 0, - STATE_FADE_IN = 1, - STATE_SHOW = 2, - STATE_FADE_OUT = 3 -} - -const MAP_NAME_IMAGES: Dictionary = { - "metin2_map_a1": "locale/ui/mapname/a1.tga", - "map_a2": "locale/ui/mapname/a2.tga", - "metin2_map_a3": "locale/ui/mapname/a3.tga", - "metin2_map_b1": "locale/ui/mapname/b1.tga", - "map_b2": "locale/ui/mapname/b2.tga", - "metin2_map_b3": "locale/ui/mapname/b3.tga", - "metin2_map_c1": "locale/ui/mapname/c1.tga", - "map_c2": "locale/ui/mapname/c2.tga", - "metin2_map_c3": "locale/ui/mapname/c3.tga", - "map_n_snowm_01": "locale/ui/mapname/snow1.tga", - "metin2_map_deviltower1": "locale/ui/mapname/devil1_title.tga", - "metin2_map_n_flame_01": "locale/ui/mapname/frame1.tga", - "metin2_map_n_desert_01": "locale/ui/mapname/desert1.tga", - "metin2_map_milgyo": "locale/ui/mapname/milgyo.tga", - "metin2_map_monkeydungeon": "locale/ui/mapname/monkey1.tga", - "metin2_map_monkeydungeon_02": "locale/ui/mapname/monkey2.tga", - "metin2_map_monkeydungeon_03": "locale/ui/mapname/monkey3.tga", - "metin2_map_guild_01": "locale/ui/mapname/guild1.tga", - "metin2_map_guild_02": "locale/ui/mapname/guild2.tga", - "metin2_map_guild_03": "locale/ui/mapname/guild3.tga", - "metin2_map_trent": "locale/ui/mapname/trent.tga", - "metin2_map_trent02": "locale/ui/mapname/trent02.tga", - "metin2_map_devilsCatacomb": "locale/ui/mapname/devil_basement.tga", - "metin2_map_spiderdungeon": "locale/ui/mapname/spider1.tga", - "metin2_map_spiderdungeon_02": "locale/ui/mapname/spider1.tga", - "metin2_map_spiderdungeon_03": "locale/ui/mapname/spider1.tga" -} - -var current_map_name: String = "" -var current_image_path: String = "" -var current_floor_image: String = "" -var current_floor_number: int = 0 - -var current_x: float = 0.0 -var current_y: float = 0.0 - -var state: State = State.STATE_HIDE -var cur_alpha: float = 0.0 -var fade_start_time: float = 0.0 - -var pos_x: int = 0 -var pos_y: int = 80 - -signal map_name_triggered(map_name: String, image_path: String, floor_num: int) -signal state_changed(new_state: State, alpha: float) -signal banner_hidden() - -# 40250 官方通天魔塔 9 层空间识别算法 -func get_devil_tower_floor(x: float, y: float) -> int: - if x > 10000 and y > 58000 and x < 25000 and y < 72000: - return 1 - elif x > 10000 and y > 35000 and x < 25000 and y < 50000: - return 2 - elif x > 10000 and y > 10000 and x < 25000 and y < 25000: - return 3 - elif x > 35000 and y > 61000 and x < 43500 and y < 70500: - return 4 - elif x > 35000 and y > 38000 and x < 43500 and y < 48000: - return 5 - elif x > 14000 and y > 14000 and x < 43500 and y < 24500: - return 6 - elif x > 56000 and y > 60000 and x < 68000 and y < 73000: - return 7 - elif x > 56000 and y > 38000 and x < 68000 and y < 49000: - return 8 - elif x > 56000 and y > 13000 and x < 68000 and y < 23000: - return 9 - return 0 - -# 40250 官方恶魔地下陵墓 7 层空间识别算法 -func get_devil_catacomb_base(x: float, y: float) -> int: - if x > 3000 and y > 4500 and x < 45000 and y < 45000: - return 1 - elif x > 54000 and y > 3900 and x < 100000 and y < 46200: - return 2 - elif x > 104800 and y > 3500 and x < 145500 and y < 45800: - return 3 - elif x > 3100 and y > 54100 and x < 56400 and y < 105800: - return 4 - elif x > 65000 and y > 54000 and x < 105000 and y < 95500: - return 5 - elif x > 117500 and y > 57600 and x < 142000 and y < 81000: - return 6 - elif x > 5000 and y > 104900 and x < 15000 and y < 122000: - return 7 - return 0 - -func show_map_name(map_name: String, x: float, y: float, now_time: float) -> bool: - if not MAP_NAME_IMAGES.has(map_name): - return false - - current_map_name = map_name - current_image_path = MAP_NAME_IMAGES[map_name] - current_x = x - current_y = y - current_floor_image = "" - current_floor_number = 0 - - pos_x = 0 - pos_y = 80 - - if map_name == "metin2_map_deviltower1": - pos_x = -60 - current_floor_number = get_devil_tower_floor(x, y) - if current_floor_number > 0: - current_floor_image = "locale/ui/mapname/devil1_%df.tga" % current_floor_number - elif map_name == "metin2_map_devilsCatacomb": - pos_x = -75 - current_floor_number = get_devil_catacomb_base(x, y) - if current_floor_number > 0: - current_floor_image = "locale/ui/mapname/devil1_%df.tga" % current_floor_number - - cur_alpha = 0.0 - state = State.STATE_FADE_IN - fade_start_time = now_time + 1.0 - - map_name_triggered.emit(map_name, current_image_path, current_floor_number) - state_changed.emit(state, cur_alpha) - return true - -func update_step(now_time: float) -> void: - if state == State.STATE_HIDE: - return - - if state == State.STATE_FADE_IN: - if now_time >= fade_start_time: - cur_alpha = minf(1.0, cur_alpha + 0.05) - if cur_alpha >= 0.9: - state = State.STATE_SHOW - fade_start_time = now_time + 5.0 - state_changed.emit(state, cur_alpha) - - elif state == State.STATE_SHOW: - if now_time >= fade_start_time: - state = State.STATE_FADE_OUT - state_changed.emit(state, cur_alpha) - - elif state == State.STATE_FADE_OUT: - cur_alpha = maxf(0.0, cur_alpha - 0.05) - if cur_alpha < 0.0001: - cur_alpha = 0.0 - state = State.STATE_HIDE - current_floor_image = "" - state_changed.emit(state, cur_alpha) - banner_hidden.emit() - else: - state_changed.emit(state, cur_alpha) - -func hide_banner() -> void: - state = State.STATE_HIDE - cur_alpha = 0.0 - current_floor_image = "" - state_changed.emit(state, cur_alpha) - banner_hidden.emit() diff --git a/project/map_name_shower_system.gd.uid b/project/map_name_shower_system.gd.uid deleted file mode 100644 index b8a772ab..00000000 --- a/project/map_name_shower_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dea0xs15tw2hx diff --git a/project/marriage_system.gd b/project/marriage_system.gd deleted file mode 100644 index aab0b469..00000000 --- a/project/marriage_system.gd +++ /dev/null @@ -1,111 +0,0 @@ -# marriage_system.gd —— Metin2 40250 结婚戒指与伴侣瞬移传送系统 1:1 -# 对照 40250 服务端 wedding.cpp, char_item.cpp (Wedding Ring 70301, NPC 20017 Musician) -class_name MarriageSystem -extends RefCounted - -signal couple_warped(target_map: String, target_pos: Vector3) - -const NPC_MUSICIAN := 20017 # 月下老人 / 乐师 -const ITEM_WEDDING_RING := 70301 # 结婚戒指 -const WARP_SP_COST := 50 # 每次传送消耗 50 SP -const WARP_COOLDOWN := 180.0 # 3 分钟传送冷却 - -# 禁止传送进入的特殊副本/区域 -const FORBIDDEN_MAPS := ["deviltower_f7", "deviltower_f8", "deviltower_f9", "oxevent"] - -var is_married: bool = false -var spouse_name: String = "" -var love_points: int = 0 # 0 ~ 100% -var warp_cooldown: float = 0.0 - -# 在月下老人处登记结婚 (wedding.cpp) -func marry(player_name: String, partner_name: String, ring_item: Dictionary, player_level: int) -> Dictionary: - if is_married: - return {"ok": false, "reason": "ALREADY_MARRIED", "msg": "你已经结过婚了!"} - if player_level < 25: - return {"ok": false, "reason": "LEVEL_TOO_LOW", "msg": "角色等级未满 25 级,无法缔结婚约!"} - - var vnum: int = int(ring_item.get("vnum", 0)) - if vnum != ITEM_WEDDING_RING: - return {"ok": false, "reason": "NO_WEDDING_RING", "msg": "缔结婚约必须携带结婚戒指!"} - - is_married = true - spouse_name = partner_name - love_points = 50 # 初始爱意值 50% - warp_cooldown = 0.0 - - # 戒指上刻录伴侣名字 - ring_item["inscribed_name"] = partner_name - ring_item["is_bound"] = true - - return { - "ok": true, - "spouse_name": spouse_name, - "love_points": love_points, - "msg": "恭喜!你与【%s】在月下老人的见证下正式结为夫妻!" % spouse_name - } - -# 离婚解除婚约 -func divorce() -> Dictionary: - if not is_married: - return {"ok": false, "reason": "NOT_MARRIED"} - - var old_spouse = spouse_name - is_married = false - spouse_name = "" - love_points = 0 - warp_cooldown = 0.0 - - return {"ok": true, "msg": "已与【%s】解除婚姻关系。" % old_spouse} - -# 增加爱意值 -func add_love_points(amount: int) -> int: - if is_married: - love_points = clampi(love_points + amount, 0, 100) - return love_points - -# 夫妻瞬移传送 (char_item.cpp:UseItemEx 70301) -func warp_to_spouse(current_sp: int, spouse_location: Dictionary, is_dead: bool) -> Dictionary: - if not is_married: - return {"ok": false, "reason": "NOT_MARRIED", "msg": "你尚未结婚,无法使用夫妻传送!"} - if is_dead: - return {"ok": false, "reason": "PLAYER_DEAD", "msg": "死亡状态下无法进行传送!"} - if warp_cooldown > 0.0: - return {"ok": false, "reason": "COOLDOWN_ACTIVE", "remaining": warp_cooldown, "msg": "结婚戒指传送冷却中!"} - if current_sp < WARP_SP_COST: - return {"ok": false, "reason": "NOT_ENOUGH_SP", "msg": "传送所需内力值不足 (需要 %d SP)!" % WARP_SP_COST} - - if spouse_location.is_empty() or not bool(spouse_location.get("is_online", false)): - return {"ok": false, "reason": "SPOUSE_OFFLINE", "msg": "伴侣当前不在线!"} - - var map_id: String = String(spouse_location.get("map", "")) - if map_id in FORBIDDEN_MAPS: - return {"ok": false, "reason": "FORBIDDEN_ZONE", "msg": "伴侣当前处于特殊封闭区域,无法瞬移传送到其身边!"} - - var pos: Vector3 = spouse_location.get("pos", Vector3.ZERO) - - # 扣除 SP 并进入冷却 - warp_cooldown = WARP_COOLDOWN - couple_warped.emit(map_id, pos) - - return { - "ok": true, - "target_map": map_id, - "target_pos": pos, - "sp_cost": WARP_SP_COST, - "cooldown": WARP_COOLDOWN, - "msg": "心有灵犀!正在瞬间传送到伴侣身边..." - } - -# 逐帧更新冷却 -func update(dt: float) -> void: - if warp_cooldown > 0.0: - warp_cooldown = maxf(0.0, warp_cooldown - dt) - -func get_info() -> Dictionary: - return { - "is_married": is_married, - "spouse_name": spouse_name, - "love_points": love_points, - "warp_cooldown": warp_cooldown - } diff --git a/project/marriage_system.gd.uid b/project/marriage_system.gd.uid deleted file mode 100644 index 6f57b4a2..00000000 --- a/project/marriage_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d3hi3rrcgdf7m diff --git a/project/marriage_wedding_love_system.gd b/project/marriage_wedding_love_system.gd deleted file mode 100644 index e61d5d41..00000000 --- a/project/marriage_wedding_love_system.gd +++ /dev/null @@ -1,177 +0,0 @@ -# marriage_wedding_love_system.gd —— Metin2 40250 官方夫妻同心传送与爱情点数婚戒祈愿系统 1:1 -# 100% 对照 40250 服务端 marriage.cpp, marriage.h, char_item.cpp (ITEM_MARRIAGE_RING 70302), char.cpp (WarpToPID) -class_name MarriageWeddingLoveSystem -extends RefCounted - -signal couple_married(pid1: int, name1: String, pid2: int, name2: String) -signal couple_divorced(pid1: int, pid2: int) -signal partner_teleported(teleporter_id: int, target_pos: Vector2, sp_consumed: int) -signal love_points_increased(pid1: int, pid2: int, new_love_points: int) -signal marriage_error(reason: String) - -# 40250 官方婚戒与爱情饰品 Vnum -const VNUM_COUPLE_RING := 70301 # 情侣/订婚戒指 (Couple Ring) -const VNUM_WEDDING_RING := 70302 # 结婚戒指 (Wedding Ring / ITEM_MARRIAGE_RING) -const VNUM_BRACELET_OF_LOVE := 71069 # 爱情手镯 (Bracelet of Love - 增攻击) -const VNUM_EARRINGS_OF_LOVE := 71071 # 爱情耳环 (Earrings of Love - 增防御) -const VNUM_NECKLACE_OF_LOVE := 71073 # 爱情项链 (Necklace of Love - 增暴击) - -# 婚姻档案记录表: -# couple_id -> { "pid1": int, "name1": String, "pid2": int, "name2": String, "love_points": int, "marry_time": float } -var _marriages: Dictionary = {} -var _pid_to_couple: Dictionary = {} # pid -> couple_id -var _next_couple_id: int = 1 - -# ========================================== -# 1. 婚姻缔结与解约 (marriage.cpp: Add, Remove) -# ========================================== -func marry(pid1: int, name1: String, pid2: int, name2: String) -> int: - if is_married(pid1) or is_married(pid2) or pid1 == pid2: - marriage_error.emit("ALREADY_MARRIED") - return -1 - - var cid = _next_couple_id - _next_couple_id += 1 - - _marriages[cid] = { - "pid1": pid1, - "name1": name1, - "pid2": pid2, - "name2": name2, - "love_points": 50, # 初始爱情点数 50% - "marry_time": Time.get_unix_time_from_system() - } - _pid_to_couple[pid1] = cid - _pid_to_couple[pid2] = cid - - couple_married.emit(pid1, name1, pid2, name2) - return cid - -func divorce(pid: int) -> bool: - if not is_married(pid): - marriage_error.emit("NOT_MARRIED") - return false - - var cid = int(_pid_to_couple[pid]) - var m = _marriages[cid] - var p1 = int(m["pid1"]) - var p2 = int(m["pid2"]) - - _pid_to_couple.erase(p1) - _pid_to_couple.erase(p2) - _marriages.erase(cid) - - couple_divorced.emit(p1, p2) - return true - -func is_married(pid: int) -> bool: - return _pid_to_couple.has(pid) - -func get_partner_pid(pid: int) -> int: - if not is_married(pid): - return 0 - var cid = _pid_to_couple[pid] - var m = _marriages[cid] - return int(m["pid2"]) if int(m["pid1"]) == pid else int(m["pid1"]) - -func get_partner_name(pid: int) -> String: - if not is_married(pid): - return "" - var cid = _pid_to_couple[pid] - var m = _marriages[cid] - return str(m["name2"]) if int(m["pid1"]) == pid else str(m["name1"]) - -# ========================================== -# 2. 爱情点数与同心杀怪成长 (marriage.cpp: LovePoint, IsNear) -# ========================================== -func get_love_points(pid: int) -> int: - if not is_married(pid): - return 0 - var cid = _pid_to_couple[pid] - return int(_marriages[cid]["love_points"]) - -func add_love_points(pid: int, amount: int) -> int: - if not is_married(pid) or amount <= 0: - return 0 - var cid = _pid_to_couple[pid] - var cur = int(_marriages[cid]["love_points"]) - var new_val = clamp(cur + amount, 0, 100) - _marriages[cid]["love_points"] = new_val - - love_points_increased.emit(_marriages[cid]["pid1"], _marriages[cid]["pid2"], new_val) - return new_val - -# 爱情饰品属性加成计算 (marriage.cpp: GetBonus) -func get_love_item_bonus(pid: int, item_vnum: int) -> Dictionary: - var lp = get_love_points(pid) - # 依据 40250 公式,加成随爱情点数百分比等比浮动 - var ratio = lp / 100.0 - match item_vnum: - VNUM_BRACELET_OF_LOVE: # 爱情手镯: 增加攻击力 (最高 +40) - return {"bonus_type": "att_grade", "value": int(floor(40 * ratio))} - VNUM_EARRINGS_OF_LOVE: # 爱情耳环: 增加防御力 (最高 +30) - return {"bonus_type": "def_grade", "value": int(floor(30 * ratio))} - VNUM_NECKLACE_OF_LOVE: # 爱情项链: 增加暴击率 (最高 +20%) - return {"bonus_type": "crit_pct", "value": int(floor(20 * ratio))} - return {"bonus_type": "none", "value": 0} - -# ========================================== -# 3. 结婚戒指跨图同心传送 (char_item.cpp:2490 ITEM_MARRIAGE_RING) -# ========================================== -func use_wedding_ring( - player_id: int, - player_sp: int, - partner_online: bool, - partner_map_name: String, - partner_pos: Vector2, - is_partner_in_arena: bool = false -) -> Dictionary: - if not is_married(player_id): - marriage_error.emit("NOT_MARRIED") - return {"ok": false, "reason": "NOT_MARRIED", "msg": "未婚状态无法使用结婚戒指传送!"} - - if not partner_online: - marriage_error.emit("PARTNER_OFFLINE") - return {"ok": false, "reason": "PARTNER_OFFLINE", "msg": "你的伴侣当前不在线,无法感应其位置。"} - - # 40250 官方: 竞技场/武斗决斗中禁止夫妻传送 (IsArenaMap) - if is_partner_in_arena: - marriage_error.emit("PARTNER_IN_ARENA") - return {"ok": false, "reason": "PARTNER_IN_ARENA", "msg": "你的伴侣正在决斗竞技场中,禁止传送进入!"} - - # 计算传送法力消耗 (40250 CalculateConsumeSP): - # 爱情点数越高,传送消耗的 SP 越少 (100% 爱情点数消耗 50 SP,0% 消耗 200 SP) - var lp = get_love_points(player_id) - var consume_sp = int(floor(200 - (150 * (lp / 100.0)))) - - if player_sp < consume_sp: - marriage_error.emit("NOT_ENOUGH_SP") - return { - "ok": false, - "reason": "NOT_ENOUGH_SP", - "msg": "法力不足!夫妻同心传送需要消耗 %d 点法力值 (SP)。" % consume_sp - } - - partner_teleported.emit(player_id, partner_pos, consume_sp) - return { - "ok": true, - "partner_name": get_partner_name(player_id), - "target_map": partner_map_name, - "target_pos": partner_pos, - "sp_consumed": consume_sp, - "remaining_sp": player_sp - consume_sp, - "msg": "婚戒闪耀着柔和的粉红霞光,跨越千山万水,你瞬间传送到了伴侣身边!" - } - -# 序列化与反序列化 -func serialize() -> Dictionary: - return { - "marriages": _marriages.duplicate(true), - "pid_to_couple": _pid_to_couple.duplicate(), - "next_couple_id": _next_couple_id - } - -func deserialize(data: Dictionary) -> void: - _marriages = data.get("marriages", {}).duplicate(true) - _pid_to_couple = data.get("pid_to_couple", {}).duplicate() - _next_couple_id = int(data.get("next_couple_id", 1)) diff --git a/project/marriage_wedding_love_system.gd.uid b/project/marriage_wedding_love_system.gd.uid deleted file mode 100644 index b75fc0dc..00000000 --- a/project/marriage_wedding_love_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ceu3k5ctf7cn1 diff --git a/project/mercenary_companion_system.gd b/project/mercenary_companion_system.gd deleted file mode 100644 index 160132b7..00000000 --- a/project/mercenary_companion_system.gd +++ /dev/null @@ -1,279 +0,0 @@ -# mercenary_companion_system.gd —— Metin2 40250 离线单机假人/陪练佣兵助战系统 1:1 -# 对照 40250 服务端 char_companion.cpp, party.cpp, affect.h -class_name MercenaryCompanionSystem -extends RefCounted - -signal mercenary_summoned(merc_id: int, name: String) -signal mercenary_dismissed() -signal buff_applied_to_player(buff_name: String, duration: float, value: float) -signal heal_applied_to_player(heal_amount: int) -signal mercenary_attacked(target_id: String, damage: int) -signal mercenary_downed() -signal mercenary_revived() - -enum MercenaryType { - SHAMAN_HEALER = 1, # 龙神萨满·苏菲 (辅助/治疗) - WARRIOR_TANK = 2 # 近卫战狂·阿雷斯 (肉盾/前锋) -} - -enum Stance { - FOLLOW = 0, # 被动跟随 - ASSIST = 1, # 协同攻击 - STAY = 2 # 原地待命 -} - -const REVIVE_COOLDOWN: float = 60.0 # 死亡自动复活冷却 (秒) -const TELEPORT_DISTANCE: float = 25.0 # 超出此距离瞬间传送归队 -const HEAL_COOLDOWN: float = 10.0 # 治疗术内置冷却 -const BUFF_COOLDOWN: float = 30.0 # Buff 持续与刷新周期 - -const MERCENARY_DATA: Dictionary = { - MercenaryType.SHAMAN_HEALER: { - "name": "龙神萨满·苏菲", - "job": 3, - "base_hp": 8000, - "base_sp": 6000, - "atk": 350, - "def": 280, - "buffs": [ - {"name": "巨龙之赐", "type": "def_bonus", "value": 0.20, "duration": 120.0}, - {"name": "龙之协助", "type": "crit_bonus", "value": 0.15, "duration": 120.0} - ] - }, - MercenaryType.WARRIOR_TANK: { - "name": "近卫战狂·阿雷斯", - "job": 0, - "base_hp": 15000, - "base_sp": 2000, - "atk": 650, - "def": 500, - "buffs": [ - {"name": "强身战气", "type": "atk_bonus", "value": 0.15, "duration": 120.0}, - {"name": "狂暴姿态", "type": "speed_bonus", "value": 0.20, "duration": 120.0} - ] - } -} - -var active_type: int = 0 -var merc_name: String = "" -var level: int = 1 -var max_hp: int = 1000 -var current_hp: int = 1000 -var max_sp: int = 500 -var current_sp: int = 500 -var atk: int = 200 -var def: int = 150 -var is_active: bool = false -var is_alive: bool = false -var current_stance: int = Stance.ASSIST -var pos: Vector3 = Vector3.ZERO - -# 计时器 -var heal_timer: float = 0.0 -var buff_timer: float = 0.0 -var revive_timer: float = 0.0 - -# 召唤指定佣兵 -func summon_mercenary(merc_type: int, player_pos: Vector3, player_level: int = 50) -> Dictionary: - if not MERCENARY_DATA.has(merc_type): - return {"ok": false, "reason": "INVALID_MERC_TYPE", "msg": "未知的佣兵类型!"} - - var cfg = MERCENARY_DATA[merc_type] - active_type = merc_type - merc_name = cfg["name"] - level = player_level - - # 等级动态成长数值 - var growth_mult = 1.0 + float(level - 1) * 0.05 - max_hp = int(float(cfg["base_hp"]) * growth_mult) - current_hp = max_hp - max_sp = int(float(cfg["base_sp"]) * growth_mult) - current_sp = max_sp - atk = int(float(cfg["atk"]) * growth_mult) - def = int(float(cfg["def"]) * growth_mult) - - is_active = true - is_alive = true - current_stance = Stance.ASSIST - pos = player_pos + Vector3(-2.0, 0.0, -2.0) - - heal_timer = 0.0 - buff_timer = 0.0 - revive_timer = 0.0 - - mercenary_summoned.emit(active_type, merc_name) - - return { - "ok": true, - "type": active_type, - "name": merc_name, - "level": level, - "hp": current_hp, - "atk": atk, - "def": def, - "pos": pos, - "msg": "佣兵【%s】应召前来助战!" % merc_name - } - -# 解散佣兵 -func dismiss_mercenary() -> Dictionary: - if not is_active: - return {"ok": false, "reason": "NOT_SUMMONED"} - - is_active = false - is_alive = false - active_type = 0 - mercenary_dismissed.emit() - return {"ok": true, "msg": "佣兵已暂时离队归营。"} - -# 切换战术姿态 -func set_stance(stance: int) -> Dictionary: - if not is_active: - return {"ok": false, "reason": "NOT_SUMMONED"} - current_stance = stance - return {"ok": true, "stance": current_stance} - -# 帧心跳更新 AI 与协同战斗 -func update_mercenary(delta: float, player_pos: Vector3, player_hp: int, player_max_hp: int, target_id: String = "", target_pos: Vector3 = Vector3.ZERO) -> Dictionary: - if not is_active: - return {"status": "inactive"} - - # 死亡复活倒计时 - if not is_alive: - revive_timer -= delta - if revive_timer <= 0.0: - revive() - return {"status": "just_revived"} - return {"status": "downed", "remaining_revive": revive_timer} - - # 计时器更新 - if heal_timer > 0.0: - heal_timer = max(0.0, heal_timer - delta) - if buff_timer > 0.0: - buff_timer = max(0.0, buff_timer - delta) - - # 1. 距离过远瞬移归队 - var dist_to_player = pos.distance_to(player_pos) - if dist_to_player > TELEPORT_DISTANCE: - pos = player_pos + Vector3(-1.5, 0.0, -1.5) - return {"status": "teleported_to_player"} - - # 2. 萨满智能辅助:给玩家刷新 Buff - if buff_timer <= 0.0: - buff_timer = BUFF_COOLDOWN - _apply_buffs_to_player() - - # 3. 萨满智能救死扶伤:当玩家 HP < 60% 触发急救 - if active_type == MercenaryType.SHAMAN_HEALER and heal_timer <= 0.0: - var hp_ratio = float(player_hp) / float(max(1, player_max_hp)) - if hp_ratio < 0.60: - heal_timer = HEAL_COOLDOWN - var heal_amt = int(float(player_max_hp) * 0.35) - heal_applied_to_player.emit(heal_amt) - return { - "status": "cast_heal", - "heal_amount": heal_amt, - "msg": "【%s】吟唱巨龙圣歌,为你恢复了 %d 点生命值!" % [merc_name, heal_amt] - } - - # 4. 战术行为分流 - match current_stance: - Stance.STAY: - return {"status": "staying"} - - Stance.FOLLOW: - # 保持紧随玩家身后 2.5 米 - if dist_to_player > 3.0: - pos = pos.move_toward(player_pos, 5.0 * delta) - return {"status": "following", "dist": dist_to_player} - return {"status": "standing_by"} - - Stance.ASSIST: - # 优先跟随;若有目标则协同集火 - if not target_id.is_empty(): - var dist_to_tgt = pos.distance_to(target_pos) - var desired_range = 10.0 if active_type == MercenaryType.SHAMAN_HEALER else 2.5 - if dist_to_tgt > desired_range: - pos = pos.move_toward(target_pos, 5.5 * delta) - return {"status": "closing_in_target", "target": target_id} - else: - # 施展协同攻击 - var dmg = atk - mercenary_attacked.emit(target_id, dmg) - return { - "status": "attacked_target", - "target": target_id, - "damage": dmg - } - else: - if dist_to_player > 3.0: - pos = pos.move_toward(player_pos, 5.0 * delta) - return {"status": "following_idle"} - - return {"status": "idle"} - -# 给主人施加专属辅助光环 -func _apply_buffs_to_player() -> void: - var cfg = MERCENARY_DATA.get(active_type, {}) - var buffs: Array = cfg.get("buffs", []) - for b in buffs: - buff_applied_to_player.emit(b["name"], b["duration"], b["value"]) - -# 佣兵受到伤害 -func receive_damage(raw_dmg: int) -> Dictionary: - if not is_active or not is_alive: - return {"ok": false} - - var net_dmg = max(1, raw_dmg - def) - current_hp = max(0, current_hp - net_dmg) - - if current_hp <= 0: - is_alive = false - revive_timer = REVIVE_COOLDOWN - mercenary_downed.emit() - return { - "ok": true, - "downed": true, - "msg": "佣兵【%s】力竭倒下!将在 60 秒后恢复伤势复活。" % merc_name - } - - return { - "ok": true, - "downed": false, - "remaining_hp": current_hp, - "net_dmg": net_dmg - } - -# 复活佣兵 -func revive(instant: bool = false) -> Dictionary: - if not is_active: - return {"ok": false, "reason": "NOT_SUMMONED"} - - is_alive = true - current_hp = int(float(max_hp) * (1.0 if instant else 0.5)) - revive_timer = 0.0 - mercenary_revived.emit() - return { - "ok": true, - "hp": current_hp, - "msg": "佣兵【%s】重新站起投入战斗!" % merc_name - } - -# 获取当前佣兵状态 -func get_mercenary_info() -> Dictionary: - return { - "is_active": is_active, - "is_alive": is_alive, - "type": active_type, - "name": merc_name, - "level": level, - "current_hp": current_hp, - "max_hp": max_hp, - "current_sp": current_sp, - "max_sp": max_sp, - "atk": atk, - "def": def, - "stance": current_stance, - "pos": pos, - "revive_timer": revive_timer - } diff --git a/project/mercenary_companion_system.gd.uid b/project/mercenary_companion_system.gd.uid deleted file mode 100644 index 88fc407c..00000000 --- a/project/mercenary_companion_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bupodjsq5qiam diff --git a/project/messenger_system.gd b/project/messenger_system.gd deleted file mode 100644 index 255bd71b..00000000 --- a/project/messenger_system.gd +++ /dev/null @@ -1,169 +0,0 @@ -# messenger_system.gd —— 40250 官方好友与行会联络人面板系统 1:1 -# 严格对照: -# ClientVS22/source/UserInterface/PythonMessenger.h, PythonMessenger.cpp -# Client/Eternexus/root/uimessenger.py -extends RefCounted - -const GROUP_FRIEND := 0 # 好友分组 (MESSENGER_GRUOP_INDEX_FRIEND) -const GROUP_GUILD := 1 # 行会分组 (MESSENGER_GRUOP_INDEX_GUILD) -const GROUP_FAMILY := 2 # 家族/伴侣分组 (MessengerFamilyGroup) - -const STATE_OFFLINE := 0 # 离线 -const STATE_ONLINE := 1 # 在线 -const STATE_MOBILE := 2 # 移动短信/挂机状态 - -# 联络人数据结构 -class MessengerContact: - var name: String - var group: int - var state: int - var love_point: int = -1 # 伴侣爱心值 (0~100) - var rank: String = "" # 行会职位 - var last_seen: int = 0 - - func _init(p_name: String, p_group: int, p_state: int = STATE_OFFLINE) -> void: - name = p_name - group = p_group - state = p_state - love_point = -1 - rank = "" - last_seen = 0 - -# 分组存储 -var friends: Dictionary = {} # name -> MessengerContact -var guild_members: Dictionary = {} # name -> MessengerContact -var family_partner: MessengerContact = null - -# 事件日志回调缓冲(模拟 PythonMessenger 向上层回调) -var event_logs: Array = [] - -var main_player_name: String = "Player" - -func _init(player_name: String = "Player") -> void: - main_player_name = player_name - friends.clear() - guild_members.clear() - family_partner = null - event_logs.clear() - -# 1. 好友添加 (PythonMessenger::AppendFriend) -func add_friend(friend_name: String, initial_state: int = STATE_ONLINE) -> Dictionary: - var res := { "success": false, "error_code": "OK", "message": "" } - - if friend_name.is_empty(): - res["error_code"] = "EMPTY_NAME" - res["message"] = "好友名称不能为空。" - return res - - if friend_name == main_player_name: - res["error_code"] = "CANNOT_ADD_SELF" - res["message"] = "无法将自己添加为好友。" - return res - - if friends.has(friend_name): - res["error_code"] = "ALREADY_FRIEND" - res["message"] = "该玩家已在您的好友列表中。" - return res - - var contact = MessengerContact.new(friend_name, GROUP_FRIEND, initial_state) - friends[friend_name] = contact - - event_logs.append({ "type": "ADD_FRIEND", "name": friend_name, "state": initial_state }) - res["success"] = true - res["message"] = "成功添加 %s 为好友。" % friend_name - return res - -# 2. 好友移除 (PythonMessenger::RemoveFriend) -func remove_friend(friend_name: String) -> bool: - if not friends.has(friend_name): - return false - friends.erase(friend_name) - event_logs.append({ "type": "REMOVE_FRIEND", "name": friend_name }) - return true - -# 3. 登录/登出/移动短信状态同步 (OnFriendLogin / OnFriendLogout / SetMobile) -func set_friend_state(friend_name: String, new_state: int) -> bool: - if not friends.has(friend_name): - return false - - var c: MessengerContact = friends[friend_name] - var old_state: int = c.state - c.state = new_state - - if new_state == STATE_ONLINE and old_state != STATE_ONLINE: - event_logs.append({ "type": "FRIEND_LOGIN", "name": friend_name }) - elif new_state == STATE_OFFLINE and old_state != STATE_OFFLINE: - event_logs.append({ "type": "FRIEND_LOGOUT", "name": friend_name }) - elif new_state == STATE_MOBILE: - event_logs.append({ "type": "FRIEND_MOBILE", "name": friend_name }) - - return true - -# 4. 行会成员同步 (AppendGuildMember / RemoveGuildMember / RemoveAllGuildMember) -func append_guild_member(member_name: String, member_rank: String = "Member", state: int = STATE_ONLINE) -> void: - if guild_members.has(member_name): - var existing: MessengerContact = guild_members[member_name] - existing.state = state - existing.rank = member_rank - return - - var contact = MessengerContact.new(member_name, GROUP_GUILD, state) - contact.rank = member_rank - guild_members[member_name] = contact - event_logs.append({ "type": "GUILD_MEMBER_ADD", "name": member_name, "rank": member_rank }) - -func remove_guild_member(member_name: String) -> bool: - if not guild_members.has(member_name): - return false - guild_members.erase(member_name) - event_logs.append({ "type": "GUILD_MEMBER_REMOVE", "name": member_name }) - return true - -func clear_all_guild_members() -> void: - guild_members.clear() - event_logs.append({ "type": "GUILD_MEMBER_CLEAR_ALL" }) - -# 5. 家族/配偶联系人绑定 (MessengerFamilyGroup) -func set_family_partner(partner_name: String, love_points: int = 100, state: int = STATE_ONLINE) -> void: - family_partner = MessengerContact.new(partner_name, GROUP_FAMILY, state) - family_partner.love_point = clamp(love_points, 0, 100) - event_logs.append({ "type": "FAMILY_PARTNER_SET", "name": partner_name, "love": family_partner.love_point }) - -func update_partner_love_points(points: int) -> void: - if family_partner != null: - family_partner.love_point = clamp(points, 0, 100) - -func remove_family_partner() -> void: - family_partner = null - event_logs.append({ "type": "FAMILY_PARTNER_REMOVE" }) - -# 6. 查询与过滤工具方法 -func is_friend(name: String) -> bool: - return friends.has(name) - -func get_friend_state(name: String) -> int: - if friends.has(name): - return friends[name].state - return STATE_OFFLINE - -func get_group_members(group_index: int) -> Array: - var list: Array = [] - match group_index: - GROUP_FRIEND: - for k in friends: - list.append(friends[k]) - GROUP_GUILD: - for k in guild_members: - list.append(guild_members[k]) - GROUP_FAMILY: - if family_partner != null: - list.append(family_partner) - return list - -func get_online_count(group_index: int) -> int: - var members := get_group_members(group_index) - var online := 0 - for m in members: - if m.state == STATE_ONLINE or m.state == STATE_MOBILE: - online += 1 - return online diff --git a/project/messenger_system.gd.uid b/project/messenger_system.gd.uid deleted file mode 100644 index 8a52a4d8..00000000 --- a/project/messenger_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cp88xu2y3jss2 diff --git a/project/metin_spawn_event_system.gd b/project/metin_spawn_event_system.gd deleted file mode 100644 index 143a6623..00000000 --- a/project/metin_spawn_event_system.gd +++ /dev/null @@ -1,174 +0,0 @@ -# metin_spawn_event_system.gd —— Metin2 40250 野外 8 档魔石陨落与怪潮暴动 1:1 -# 对照 40250 服务端 regen.cpp, char_battle.cpp, metin.cpp, Mobs 8008~8014, 8027 -class_name MetinSpawnEventSystem -extends RefCounted - -signal metin_spawned(vnum: int, name: String, pos: Vector3) -signal mob_wave_erupted(metin_vnum: int, wave_count: int, minion_vnum: int) -signal metin_destroyed(metin_vnum: int, pos: Vector3, drops: Array) - -const METIN_TABLE: Dictionary = { - 8008: {"name": "嫉妒之石", "level": 35, "max_hp": 70000, "def": 120, "wave_mob": 391, "wave_count": 4}, - 8009: {"name": "灵魂之石", "level": 40, "max_hp": 90000, "def": 140, "wave_mob": 401, "wave_count": 4}, - 8010: {"name": "暗影之石", "level": 45, "max_hp": 120000, "def": 160, "wave_mob": 501, "wave_count": 5}, - 8011: {"name": "坚韧之石", "level": 50, "max_hp": 150000, "def": 180, "wave_mob": 601, "wave_count": 5}, - 8012: {"name": "恶魔之石", "level": 55, "max_hp": 180000, "def": 200, "wave_mob": 701, "wave_count": 6}, - 8013: {"name": "坠落之石", "level": 60, "max_hp": 220000, "def": 230, "wave_mob": 2001, "wave_count": 6}, - 8014: {"name": "死亡之石", "level": 65, "max_hp": 260000, "def": 260, "wave_mob": 2101, "wave_count": 7}, - 8027: {"name": "谋杀之石", "level": 70, "max_hp": 300000, "def": 300, "wave_mob": 2201, "wave_count": 8} -} - -# 当前存活的魔石实体字典: id -> Dictionary -var active_metins: Dictionary = {} -var next_instance_id: int = 1 - -# 在指定位置刷新野外魔石 -func spawn_metin(vnum: int, pos: Vector3 = Vector3.ZERO) -> Dictionary: - if not METIN_TABLE.has(vnum): - return {"ok": false, "reason": "INVALID_METIN_VNUM", "msg": "未知的魔石代码!"} - - var cfg = METIN_TABLE[vnum] - var inst_id := next_instance_id - next_instance_id += 1 - - var entity: Dictionary = { - "id": inst_id, - "vnum": vnum, - "name": cfg["name"], - "level": cfg["level"], - "max_hp": cfg["max_hp"], - "current_hp": cfg["max_hp"], - "def": cfg["def"], - "wave_mob": cfg["wave_mob"], - "wave_count": cfg["wave_count"], - "pos": pos, - "phase_75_triggered": false, - "phase_50_triggered": false, - "phase_25_triggered": false, - "spawned_minions": [] - } - - active_metins[inst_id] = entity - metin_spawned.emit(vnum, cfg["name"], pos) - - return { - "ok": true, - "id": inst_id, - "vnum": vnum, - "name": cfg["name"], - "pos": pos, - "msg": "【%s】轰然坠落于大地!(坐标: %.1f, %.1f)" % [cfg["name"], pos.x, pos.z] - } - -# 攻击魔石并触发 25% 阶段怪潮爆发 -func attack_metin(inst_id: int, raw_dmg: int) -> Dictionary: - if not active_metins.has(inst_id): - return {"ok": false, "reason": "METIN_NOT_FOUND", "msg": "该魔石不存在或已击碎!"} - - var stone = active_metins[inst_id] - var net_dmg: int = max(1, raw_dmg - int(stone["def"])) - stone["current_hp"] = max(0, stone["current_hp"] - net_dmg) - - var cur_hp: int = stone["current_hp"] - var max_hp: int = stone["max_hp"] - var hp_ratio: float = float(cur_hp) / float(max_hp) - - var wave_triggered := false - var spawned_count := 0 - - # 75% 阶段怪潮爆发 - if hp_ratio <= 0.75 and not stone["phase_75_triggered"]: - stone["phase_75_triggered"] = true - spawned_count = _trigger_mob_wave(stone) - wave_triggered = true - - # 50% 阶段怪潮爆发 - if hp_ratio <= 0.50 and not stone["phase_50_triggered"]: - stone["phase_50_triggered"] = true - spawned_count = _trigger_mob_wave(stone) - wave_triggered = true - - # 25% 阶段怪潮爆发 - if hp_ratio <= 0.25 and not stone["phase_25_triggered"]: - stone["phase_25_triggered"] = true - spawned_count = _trigger_mob_wave(stone) - wave_triggered = true - - # 魔石击碎判定 - if cur_hp <= 0: - var drops := _generate_metin_drops(stone["vnum"]) - var stone_pos: Vector3 = stone["pos"] - var stone_vnum: int = stone["vnum"] - var stone_name: String = stone["name"] - - active_metins.erase(inst_id) - metin_destroyed.emit(stone_vnum, stone_pos, drops) - - return { - "ok": true, - "destroyed": true, - "net_dmg": net_dmg, - "remaining_hp": 0, - "drops": drops, - "msg": "【%s】被彻底击碎!神圣光芒散落,爆出技能书与灵石!" % stone_name - } - - return { - "ok": true, - "destroyed": false, - "net_dmg": net_dmg, - "remaining_hp": cur_hp, - "hp_ratio": hp_ratio, - "wave_triggered": wave_triggered, - "spawned_count": spawned_count - } - -# 激发一波护卫怪群 -func _trigger_mob_wave(stone: Dictionary) -> int: - var count: int = stone["wave_count"] - var mob_vnum: int = stone["wave_mob"] - - for i in range(count): - var offset := Vector3(randf_range(-4.0, 4.0), 0.0, randf_range(-4.0, 4.0)) - stone["spawned_minions"].append({ - "vnum": mob_vnum, - "pos": stone["pos"] + offset - }) - - mob_wave_erupted.emit(stone["vnum"], count, mob_vnum) - return count - -# 生成魔石击碎全额掉落 -func _generate_metin_drops(metin_vnum: int) -> Array: - var cfg = METIN_TABLE[metin_vnum] - var drops: Array = [] - - # 1. 必掉技能书 (50300) - drops.append({ - "vnum": 50300, - "name": "技能书", - "count": 1 - }) - - # 2. 必掉灵魂之石 (50513) - drops.append({ - "vnum": 50513, - "name": "灵魂之石", - "count": 1 - }) - - # 3. 灵石 (+0~+4, 随机职业战士/刺客/修罗/萨满/防御/生命) - drops.append({ - "vnum": 28430, # 战士灵石+4 - "name": "战士灵石+4", - "count": 1 - }) - - # 4. 金币 - drops.append({ - "vnum": 1, - "name": "金币", - "count": randi_range(cfg["level"] * 500, cfg["level"] * 1500) - }) - - return drops diff --git a/project/metin_spawn_event_system.gd.uid b/project/metin_spawn_event_system.gd.uid deleted file mode 100644 index fb72fa11..00000000 --- a/project/metin_spawn_event_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c6ph5y2v1s1g1 diff --git a/project/mining_refinery_smelt_system.gd b/project/mining_refinery_smelt_system.gd deleted file mode 100644 index 7fed65a8..00000000 --- a/project/mining_refinery_smelt_system.gd +++ /dev/null @@ -1,192 +0,0 @@ -# mining_refinery_smelt_system.gd —— Metin2 40250 矿石采掘熔炼与首饰宝石镶嵌提纯炉系统 1:1 -# 对照 40250 服务端 mining.cpp, char_item.cpp, item_manager.cpp -class_name MiningRefinerySmeltSystem -extends RefCounted - -signal ore_smelted(success: bool, gem_vnum: int, gem_name: String) -signal jewelry_socket_opened(slot: int, socket_index: int) -signal gem_embedded(slot: int, gem_name: String, stat_boost: float) - -const SMELT_ORE_COUNT := 100 # 熔炼需 100 颗原石 -const SMELT_COST_GOLD := 10000 # 熔炼消耗 10,000 金币 -const MAX_JEWELRY_SOCKETS := 3 # 首饰最多开 3 个孔 - -# 原石与精炼宝石对照表 -const SMELT_TABLE: Dictionary = { - 50621: {"name": "钻石原石", "gem_vnum": 50634, "gem_name": "纯净钻石", "is_diamond": true}, - 50623: {"name": "乌木原石", "gem_vnum": 50636, "gem_name": "乌木精炼石", "is_diamond": false}, - 50626: {"name": "黄金原石", "gem_vnum": 50639, "gem_name": "黄金精炼石", "is_diamond": false}, - 50628: {"name": "白金原石", "gem_vnum": 50641, "gem_name": "白金精炼石", "is_diamond": false}, - 50630: {"name": "天之泪原石", "gem_vnum": 50643, "gem_name": "天之泪精炼石", "is_diamond": false} -} - -# 熔炼原石生成首饰宝石 -func smelt_ore(ore_slot: int, stone_slot: int, inventory: Array, player_data: Dictionary, is_guild_alchemist: bool = false, force_success: bool = false) -> Dictionary: - if ore_slot < 0 or ore_slot >= inventory.size() or inventory[ore_slot] == null: - return {"ok": false, "reason": "INVALID_ORE_SLOT"} - if stone_slot < 0 or stone_slot >= inventory.size() or inventory[stone_slot] == null: - return {"ok": false, "reason": "INVALID_STONE_SLOT"} - - var ore_item = inventory[ore_slot] - var stone_item = inventory[stone_slot] - - var ore_vnum = int(ore_item.get("vnum", 0)) - if not SMELT_TABLE.has(ore_vnum): - return {"ok": false, "reason": "NOT_AN_ORE", "msg": "该物品不是可熔炼的原矿石!"} - - var ore_count = int(ore_item.get("count", 1)) - if ore_count < SMELT_ORE_COUNT: - return {"ok": false, "reason": "NOT_ENOUGH_ORE", "msg": "原矿数量不足!需要 100 颗,当前仅有 %d 颗。" % ore_count} - - var gold = int(player_data.get("gold", 0)) - if gold < SMELT_COST_GOLD: - return {"ok": false, "reason": "NOT_ENOUGH_GOLD", "msg": "熔炼所需金币不足!需要 10,000 金币。"} - - # 消耗材料与金币 - player_data["gold"] = gold - SMELT_COST_GOLD - if ore_count > SMELT_ORE_COUNT: - ore_item["count"] = ore_count - SMELT_ORE_COUNT - else: - inventory[ore_slot] = null - - var stone_cnt = int(stone_item.get("count", 1)) - if stone_cnt > 1: - stone_item["count"] = stone_cnt - 1 - else: - inventory[stone_slot] = null - - # 成功率计算 (普通提纯炉 60%,公会炼金炉 70%) - var rate = 70 if is_guild_alchemist else 60 - var roll = randi_range(1, 100) - var is_succ = force_success or (roll <= rate) - - var target_gem = SMELT_TABLE[ore_vnum] - var gem_vnum = target_gem["gem_vnum"] - var gem_name = target_gem["gem_name"] - - if is_succ: - # 入包精炼宝石 - _add_gem_to_inventory(gem_vnum, gem_name, inventory) - ore_smelted.emit(true, gem_vnum, gem_name) - return { - "ok": true, - "success": true, - "gem_vnum": gem_vnum, - "gem_name": gem_name, - "msg": "提纯炉烈焰喷涌!成功熔炼出高纯度【%s】!" % gem_name - } - else: - ore_smelted.emit(false, gem_vnum, gem_name) - return { - "ok": true, - "success": false, - "msg": "熔炼失败!火候不纯,矿石与灵石化为了灰烬。" - } - -# 使用纯净钻石 (50634) 为首饰打孔 -func open_jewelry_socket(diamond_slot: int, target_slot: int, inventory: Array, force_success: bool = false) -> Dictionary: - if diamond_slot < 0 or diamond_slot >= inventory.size() or inventory[diamond_slot] == null: - return {"ok": false, "reason": "INVALID_DIAMOND_SLOT"} - if target_slot < 0 or target_slot >= inventory.size() or inventory[target_slot] == null: - return {"ok": false, "reason": "INVALID_TARGET_SLOT"} - - var d_item = inventory[diamond_slot] - var target = inventory[target_slot] - - if int(d_item.get("vnum", 0)) != 50634: - return {"ok": false, "reason": "NOT_A_DIAMOND", "msg": "打孔需要使用【纯净钻石】(50634)!"} - - if not target.has("jewelry_sockets"): - target["jewelry_sockets"] = [] - - var sockets: Array = target["jewelry_sockets"] - if sockets.size() >= MAX_JEWELRY_SOCKETS: - return {"ok": false, "reason": "MAX_SOCKETS_REACHED", "msg": "该首饰已开辟 3 个宝石孔位,达到上限!"} - - # 消耗 1 颗钻石 - var cnt = int(d_item.get("count", 1)) - if cnt > 1: - d_item["count"] = cnt - 1 - else: - inventory[diamond_slot] = null - - # 阶梯成功率: 第 1 孔 100%,第 2 孔 80%,第 3 孔 60% - var rates = [100, 80, 60] - var rate = rates[sockets.size()] - var roll = randi_range(1, 100) - var is_succ = force_success or (roll <= rate) - - if not is_succ: - return {"ok": true, "success": false, "msg": "打孔失败!钻石碎裂,未能开辟新孔位。"} - - sockets.append(null) # 开辟一个空孔位 - jewelry_socket_opened.emit(target_slot, sockets.size()) - - return { - "ok": true, - "success": true, - "socket_index": sockets.size(), - "total_sockets": sockets.size(), - "msg": "打孔成功!首饰开辟了第 %d 个宝石镶嵌孔位!" % sockets.size() - } - -# 将精炼宝石镶入首饰已开孔位 -func embed_gem_into_socket(gem_slot: int, target_slot: int, inventory: Array) -> Dictionary: - if gem_slot < 0 or gem_slot >= inventory.size() or inventory[gem_slot] == null: - return {"ok": false, "reason": "INVALID_GEM_SLOT"} - if target_slot < 0 or target_slot >= inventory.size() or inventory[target_slot] == null: - return {"ok": false, "reason": "INVALID_TARGET_SLOT"} - - var gem_item = inventory[gem_slot] - var target = inventory[target_slot] - - if not target.has("jewelry_sockets") or target["jewelry_sockets"].is_empty(): - return {"ok": false, "reason": "NO_SOCKETS", "msg": "该首饰尚未开孔!请先使用纯净钻石打孔。"} - - var sockets: Array = target["jewelry_sockets"] - var empty_idx = -1 - for i in range(sockets.size()): - if sockets[i] == null: - empty_idx = i - break - - if empty_idx == -1: - return {"ok": false, "reason": "ALL_SOCKETS_OCCUPIED", "msg": "所有宝石孔位已镶满!"} - - var gem_vnum = int(gem_item.get("vnum", 0)) - var gem_name = gem_item.get("name", "宝石") - - # 消耗 1 颗宝石 - var cnt = int(gem_item.get("count", 1)) - if cnt > 1: - gem_item["count"] = cnt - 1 - else: - inventory[gem_slot] = null - - # 镶入孔位 (每颗宝石提供 +10% 首饰属性增强) - sockets[empty_idx] = { - "vnum": gem_vnum, - "name": gem_name, - "stat_boost": 0.10 - } - - gem_embedded.emit(target_slot, gem_name, 0.10) - - return { - "ok": true, - "socket_idx": empty_idx, - "gem_name": gem_name, - "stat_boost": 0.10, - "msg": "【%s】完美嵌入第 %d 孔!激活 +10%% 首饰属性加成!" % [gem_name, empty_idx + 1] - } - -func _add_gem_to_inventory(vnum: int, iname: String, inventory: Array) -> void: - for i in range(inventory.size()): - if inventory[i] != null and int(inventory[i].get("vnum", 0)) == vnum: - inventory[i]["count"] = int(inventory[i].get("count", 1)) + 1 - return - - for i in range(inventory.size()): - if inventory[i] == null: - inventory[i] = {"vnum": vnum, "name": iname, "count": 1} - return diff --git a/project/mining_refinery_smelt_system.gd.uid b/project/mining_refinery_smelt_system.gd.uid deleted file mode 100644 index 17f41ef4..00000000 --- a/project/mining_refinery_smelt_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://do3dfunqgfm6w diff --git a/project/mob_combat_reaction_system.gd b/project/mob_combat_reaction_system.gd deleted file mode 100644 index 71254d95..00000000 --- a/project/mob_combat_reaction_system.gd +++ /dev/null @@ -1,72 +0,0 @@ -# mob_combat_reaction_system.gd —— 怪物主动仇恨视野、追击脱战与连击击退硬直(40250 官方 1:1 对齐) -# 对齐源码: -# - 40250 server/game/src/char_state.cpp:250-380 (StateBattle, StateIdle, 追击与脱战) -# - 40250 server/game/src/battle.cpp:110-180 (battle_hit, 攻击判定与受击推力) -# - 40250 ClientVS22/source/UserInterface/ActorInstanceBattle.cpp (__HitGood, __HitGreate) -class_name MobCombatReactionSystem -extends RefCounted - -const DEFAULT_AGGRO_RADIUS := 18.0 # 主动怪索敌视野半径 (18 米) -const MAX_LEASH_DISTANCE := 40.0 # 最大追击拉脱距离 (40 米) -const KNOCKBACK_FORCE_M := 1.5 # 四连击击退推力距离 (1.5 米) - -enum MobAIState { - IDLE, # 巡逻/游荡 - CHASE, # 仇恨追击玩家 - RETREAT, # 超过拉脱距离,放弃仇恨返回出生点 -} - -## 检查是否进入主动怪仇恨视野 -static func check_aggro(mob_pos: Vector3, player_pos: Vector3, is_aggressive := true, aggro_radius := DEFAULT_AGGRO_RADIUS) -> bool: - if not is_aggressive: - return false - var dist := mob_pos.distance_to(player_pos) - return dist <= aggro_radius - -## 检查是否超出追击拉脱最大距离 -static func check_leash_exceeded(mob_pos: Vector3, spawn_pos: Vector3, max_leash := MAX_LEASH_DISTANCE) -> bool: - var dist := mob_pos.distance_to(spawn_pos) - return dist > max_leash - -## 执行拉脱脱战重置 (40250 char_state.cpp 怪物脱战回位并回满生命值) -static func perform_leash_reset(mob_data: Dictionary, spawn_pos: Vector3) -> Dictionary: - var max_hp := int(mob_data.get("max_hp", 1000)) - mob_data["hp"] = max_hp - mob_data["target_vid"] = 0 - mob_data["state"] = MobAIState.RETREAT - return { - "leashed": true, - "reset_hp": max_hp, - "return_pos": spawn_pos, - "msg": "怪物超出追击范围,脱离战斗并回满生命值返回出生点!" - } - -## 计算普通连击与受击反应 (40250 battle.cpp: 第1-2击普通受击,第3-4击击退击倒) -## combo_step: 1..4 当前连击段数 -## attack_heading_rad: 攻击朝向弧度 (用于计算受力击退向量) -static func calculate_combo_reaction(combo_step: int, mob_pos: Vector3, attack_heading_rad: float) -> Dictionary: - var normalized_step := ((combo_step - 1) % 4) + 1 - if normalized_step in [1, 2]: - # 前两击:普通受击硬直 (HitReaction.good) - return { - "reaction_type": "DAMAGE_STUN", - "is_knockback": false, - "push_offset": Vector3.ZERO, - "anim": "damage", - "stun_duration": 0.2, # 0.2 秒受击硬直 - } - else: - # 第三、四击:强力击退并击倒 (HitReaction.greate) - # 顺着攻击朝向将怪物向后击退 1.5 米 - var push_dir := Vector3(sin(attack_heading_rad), 0, cos(attack_heading_rad)).normalized() - var push_vec := push_dir * KNOCKBACK_FORCE_M - var new_pos := mob_pos + push_vec - return { - "reaction_type": "KNOCKBACK_FLYING", - "is_knockback": true, - "push_offset": push_vec, - "target_pos": new_pos, - "anim": "knockdown", - "standup_anim": "standup", - "knockdown_duration": 0.8, # 倒地至起身耗时 - } diff --git a/project/mob_combat_reaction_system.gd.uid b/project/mob_combat_reaction_system.gd.uid deleted file mode 100644 index 5f29d241..00000000 --- a/project/mob_combat_reaction_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cn2gu25b1jmb1 diff --git a/project/monarch_election_ui_system.gd b/project/monarch_election_ui_system.gd deleted file mode 100644 index 682ebf89..00000000 --- a/project/monarch_election_ui_system.gd +++ /dev/null @@ -1,230 +0,0 @@ -# monarch_election_ui_system.gd —— 40250 帝国君主大选投票公示系统 1:1 -# 严格对照: -# metin2/src/server/game/src/monarch.h, monarch.cpp (CMonarch) -# metin2/src/server/share/locale/english/quest/xxx_monarch.quest -# 40250/Server Client TMP4/mysql/player/monarch_candidacy.MYD, monarch_election.MYD -extends RefCounted - -const EMPIRE_SHINSOO := 1 # 赤蛇国 (进国) -const EMPIRE_CHUNJO := 2 # 白虎国 (顺天) -const EMPIRE_JINNO := 3 # 苍龙国 (忍国) - -const STATUS_CLOSED := 0 # 选举未开放 -const STATUS_CANDIDACY := 1 # 候选人报名登记期 -const STATUS_VOTING := 2 # 全民大选投票期 -const STATUS_CONCLUDED := 3 # 大选揭晓,君主即位 - -const MIN_CANDIDATE_LEVEL := 50 -const MIN_VOTER_LEVEL := 50 -const CANDIDACY_FEE := 1000000 # 报名保证金 1,000,000 金币 -const MAX_CANDIDATES_PER_EMPIRE := 8 # 每国最多 8 名候选人 - -class CandidateInfo: - var pid: int - var name: String - var empire: int - var level: int - var votes: int = 0 - var manifesto: String = "" - - func _init(p_pid: int, p_name: String, p_emp: int, p_lvl: int, p_manifesto: String) -> void: - pid = p_pid - name = p_name - empire = p_emp - level = p_lvl - votes = 0 - manifesto = p_manifesto - -# 选举状态字典: empire_id -> status -var election_states: Dictionary = { - EMPIRE_SHINSOO: STATUS_CLOSED, - EMPIRE_CHUNJO: STATUS_CLOSED, - EMPIRE_JINNO: STATUS_CLOSED -} - -# 候选人名册: empire_id -> Array of CandidateInfo -var candidates: Dictionary = { - EMPIRE_SHINSOO: [], - EMPIRE_CHUNJO: [], - EMPIRE_JINNO: [] -} - -# 选民投票记录: empire_id -> { voter_pid: candidate_pid } -var voter_records: Dictionary = { - EMPIRE_SHINSOO: {}, - EMPIRE_CHUNJO: {}, - EMPIRE_JINNO: {} -} - -# 现任君主数据: empire_id -> { "pid": int, "name": String, "treasury": int, "tax_rate": int } -var current_monarchs: Dictionary = { - EMPIRE_SHINSOO: { "pid": 0, "name": "", "treasury": 10000000, "tax_rate": 3 }, - EMPIRE_CHUNJO: { "pid": 0, "name": "", "treasury": 10000000, "tax_rate": 3 }, - EMPIRE_JINNO: { "pid": 0, "name": "", "treasury": 10000000, "tax_rate": 3 }, -} - -func _init() -> void: - reset_all_elections() - -func reset_all_elections() -> void: - for emp in [EMPIRE_SHINSOO, EMPIRE_CHUNJO, EMPIRE_JINNO]: - election_states[emp] = STATUS_CLOSED - candidates[emp] = [] - voter_records[emp] = {} - -# 1. 管理员开启大选阶段 (GM: Kaiser Wahl Einstellung) -func set_election_stage(empire: int, new_status: int) -> bool: - if not election_states.has(empire): - return false - election_states[empire] = new_status - return true - -# 2. 候选人报名参选 (monarch_candidacy) -func register_candidacy( - pid: int, - name: String, - empire: int, - level: int, - player_gold: int, - manifesto: String -) -> Dictionary: - var res := { "success": false, "gold_cost": 0, "error_code": "OK", "message": "" } - - if election_states.get(empire) != STATUS_CANDIDACY: - res["error_code"] = "CANDIDACY_NOT_OPEN" - res["message"] = "当前未处于君主参选登记期。" - return res - - if level < MIN_CANDIDATE_LEVEL: - res["error_code"] = "LEVEL_TOO_LOW" - res["message"] = "参选君主等级必须达到 Lv 50。" - return res - - if player_gold < CANDIDACY_FEE: - res["error_code"] = "INSUFFICIENT_GOLD" - res["message"] = "报名参选需缴纳 1,000,000 金币保证金。" - return res - - var emp_candidates: Array = candidates[empire] - if emp_candidates.size() >= MAX_CANDIDATES_PER_EMPIRE: - res["error_code"] = "CANDIDATES_FULL" - res["message"] = "本届君主候选人名额已满 (最多 8 人)。" - return res - - for c in emp_candidates: - if c.pid == pid: - res["error_code"] = "ALREADY_REGISTERED" - res["message"] = "您已成功报名,无需重复登记。" - return res - - var cand = CandidateInfo.new(pid, name, empire, level, manifesto) - emp_candidates.append(cand) - - res["success"] = true - res["gold_cost"] = CANDIDACY_FEE - res["message"] = "恭喜!您已成功登记为第 %d 帝国君主候选人。" % empire - return res - -# 3. 选民投票 (monarch_election) -func cast_vote(voter_pid: int, voter_empire: int, voter_level: int, target_candidate_pid: int) -> Dictionary: - var res := { "success": false, "error_code": "OK", "message": "" } - - if election_states.get(voter_empire) != STATUS_VOTING: - res["error_code"] = "VOTING_NOT_OPEN" - res["message"] = "当前未处于全民投票大选阶段。" - return res - - if voter_level < MIN_VOTER_LEVEL: - res["error_code"] = "VOTER_LEVEL_TOO_LOW" - res["message"] = "等级未达到 Lv 50,尚无大选投票权。" - return res - - var records: Dictionary = voter_records[voter_empire] - if records.has(voter_pid): - res["error_code"] = "ALREADY_VOTED" - res["message"] = "您在本届选举中已经投过神圣的一票,不可重复投票。" - return res - - # 查找目标候选人 - var target_cand: CandidateInfo = null - for c in candidates[voter_empire]: - if c.pid == target_candidate_pid: - target_cand = c - break - - if target_cand == null: - res["error_code"] = "CANDIDATE_NOT_FOUND" - res["message"] = "未找到该候选人。" - return res - - # 计票 - target_cand.votes += 1 - records[voter_pid] = target_candidate_pid - - res["success"] = true - res["message"] = "投票成功!感谢您支持候选人 %s。" % target_cand.name - return res - -# 4. 统计揭晓并任命新君主 -func conclude_election(empire: int) -> Dictionary: - var res := { "success": false, "winner_pid": 0, "winner_name": "", "votes": 0 } - if not election_states.has(empire): - return res - - var emp_candidates: Array = candidates[empire] - if emp_candidates.is_empty(): - election_states[empire] = STATUS_CLOSED - return res - - # 选出最高票者 - var top_cand: CandidateInfo = emp_candidates[0] - for c in emp_candidates: - if c.votes > top_cand.votes: - top_cand = c - - # 就任君主 - current_monarchs[empire]["pid"] = top_cand.pid - current_monarchs[empire]["name"] = top_cand.name - election_states[empire] = STATUS_CONCLUDED - - res["success"] = true - res["winner_pid"] = top_cand.pid - res["winner_name"] = top_cand.name - res["votes"] = top_cand.votes - return res - -# 5. 君主行使特权:调整帝国商业税率 (1% ~ 10%) -func set_tax_rate(caller_pid: int, empire: int, new_tax: int) -> Dictionary: - var res := { "success": false, "error_code": "OK", "message": "" } - var monarch = current_monarchs[empire] - - if caller_pid != monarch["pid"]: - res["error_code"] = "NOT_MONARCH" - res["message"] = "只有现任帝国君主有权调整商业税率。" - return res - - var clamped_tax: int = clamp(new_tax, 1, 10) - monarch["tax_rate"] = clamped_tax - - res["success"] = true - res["tax_rate"] = clamped_tax - res["message"] = "帝国商业税率已调整为 %d%%。" % clamped_tax - return res - -# 6. 君主行使特权:释放全帝国神佑祝福 -func activate_monarch_blessing(caller_pid: int, empire: int) -> Dictionary: - var res := { "success": false, "error_code": "OK", "blessing_buff": {}, "message": "" } - var monarch = current_monarchs[empire] - - if caller_pid != monarch["pid"]: - res["error_code"] = "NOT_MONARCH" - return res - - res["success"] = true - res["blessing_buff"] = { - "exp_bonus": 10, - "att_bonus": 10, - "duration_sec": 7200 - } - res["message"] = "君主神佑降临!帝国全体子民获得 +10% 经验与 +10% 攻击力加成 (持续2小时)。" - return res diff --git a/project/monarch_election_ui_system.gd.uid b/project/monarch_election_ui_system.gd.uid deleted file mode 100644 index 8c68b46f..00000000 --- a/project/monarch_election_ui_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://hy3q4nc8nwif diff --git a/project/monarch_empire_treasury_system.gd b/project/monarch_empire_treasury_system.gd deleted file mode 100644 index 5cfb9b43..00000000 --- a/project/monarch_empire_treasury_system.gd +++ /dev/null @@ -1,294 +0,0 @@ -# monarch_empire_treasury_system.gd —— Metin2 40250 官方君主国王特权与王国国库祈愿系统 1:1 -# 100% 对照 40250 服务端 monarch.cpp, monarch.h, quest/monarch.quest, tables.h -class_name MonarchEmpireTreasurySystem -extends RefCounted - -signal monarch_healed_empire(empire: int, cost: int, restored_citizens_count: int) -signal monarch_power_up_activated(empire: int, cost: int, duration: float) -signal monarch_defense_up_activated(empire: int, cost: int, duration: float) -signal treasury_tax_collected(empire: int, amount: int, new_balance: int) -signal monarch_appointed(empire: int, player_id: int, player_name: String) -signal monarch_error(reason: String) - -# 40250 官方三大帝国常数 -const EMPIRE_NONE := 0 -const EMPIRE_SHINSOO := 1 # 辛秀帝国 (红国) -const EMPIRE_CHUNJO := 2 # 春祖帝国 (黄国) -const EMPIRE_JINNO := 3 # 镇奴帝国 (蓝国) - -const EMPIRE_NAMES: Dictionary = { - EMPIRE_SHINSOO: "辛秀帝国 (Shinsoo - 红国)", - EMPIRE_CHUNJO: "春祖帝国 (Chunjo - 黄国)", - EMPIRE_JINNO: "镇奴帝国 (Jinno - 蓝国)" -} - -# 国库金币上限 (40250 monarch.cpp: 20亿上限) -const MAX_TREASURY_GOLD := 2000000000 - -# 技能费用与冷却 -const COST_HEAL_EMPIRE := 100000 # 恩泽圣愈国库消耗 100,000 金币 -const COST_POWER_UP := 500000 # 帝国狂暴国库消耗 500,000 金币 -const COST_DEFENSE_UP := 500000 # 铁壁光环国库消耗 500,000 金币 - -const COOLDOWN_HEAL_EMPIRE := 600.0 # 恩泽圣愈冷却 10 分钟 (600 秒) -const COOLDOWN_POWER_UP := 600.0 # 战力觉醒冷却 10 分钟 -const COOLDOWN_DEFENSE_UP := 600.0 # 铁壁守护冷却 10 分钟 - -const DURATION_POWER_UP := 1800.0 # 战力狂暴持续 30 分钟 (1800 秒) -const DURATION_DEFENSE_UP := 1800.0 # 铁壁守护持续 30 分钟 - -# 数据存储 -var _monarch_pids: Dictionary = {1: 0, 2: 0, 3: 0} # empire -> player_id -var _monarch_names: Dictionary = {1: "", 2: "", 3: ""} -var _treasury: Dictionary = {1: 0, 2: 0, 3: 0} # empire -> gold - -var _heal_cooldown: Dictionary = {1: 0.0, 2: 0.0, 3: 0.0} -var _power_up_cooldown: Dictionary = {1: 0.0, 2: 0.0, 3: 0.0} -var _defense_up_cooldown: Dictionary = {1: 0.0, 2: 0.0, 3: 0.0} - -var _power_up_active: Dictionary = {1: false, 2: false, 3: false} -var _defense_up_active: Dictionary = {1: false, 2: false, 3: false} - -var _power_up_timer: Dictionary = {1: 0.0, 2: 0.0, 3: 0.0} -var _defense_up_timer: Dictionary = {1: 0.0, 2: 0.0, 3: 0.0} - -# ========================================== -# 1. 国王任命与身份核验 (monarch.cpp: IsMonarch) -# ========================================== -func appoint_monarch(empire: int, player_id: int, player_name: String) -> bool: - if not _is_valid_empire(empire): - monarch_error.emit("INVALID_EMPIRE") - return false - _monarch_pids[empire] = player_id - _monarch_names[empire] = player_name - monarch_appointed.emit(empire, player_id, player_name) - return true - -func is_monarch(player_id: int, empire: int) -> bool: - if not _is_valid_empire(empire): - return false - return int(_monarch_pids.get(empire, 0)) == player_id - -func get_monarch_info(empire: int) -> Dictionary: - if not _is_valid_empire(empire): - return {} - return { - "empire": empire, - "empire_name": EMPIRE_NAMES[empire], - "player_id": _monarch_pids.get(empire, 0), - "player_name": _monarch_names.get(empire, ""), - "treasury_gold": _treasury.get(empire, 0) - } - -# ========================================== -# 2. 国库金币与税赋 (monarch.cpp: AddMoney, DecMoney) -# ========================================== -func get_treasury_gold(empire: int) -> int: - if not _is_valid_empire(empire): - return 0 - return int(_treasury.get(empire, 0)) - -func add_treasury_gold(empire: int, amount: int) -> bool: - if not _is_valid_empire(empire) or amount <= 0: - return false - var current = int(_treasury.get(empire, 0)) - var new_val = min(current + amount, MAX_TREASURY_GOLD) - _treasury[empire] = new_val - return true - -func collect_monster_tax(empire: int, monster_gold_drop: int, tax_rate: float = 0.02) -> int: - if not _is_valid_empire(empire) or monster_gold_drop <= 0: - return 0 - var tax = int(floor(monster_gold_drop * tax_rate)) - if tax > 0: - add_treasury_gold(empire, tax) - treasury_tax_collected.emit(empire, tax, _treasury[empire]) - return tax - -func dec_treasury_gold(empire: int, amount: int) -> bool: - if not _is_valid_empire(empire) or amount <= 0: - return false - var current = int(_treasury.get(empire, 0)) - if current < amount: - return false - _treasury[empire] = current - amount - return true - -# ========================================== -# 3. 君主全境治国神技 (monarch.cpp: HealMyEmpire, PowerUp, DefenseUp) -# ========================================== - -# 恩泽圣愈:全境国民 HP/SP 瞬间全满 (FHealMyEmpire) -func heal_my_empire( - player_id: int, - empire: int, - online_citizens: Array[Dictionary] -) -> Dictionary: - if not is_monarch(player_id, empire): - monarch_error.emit("NOT_MONARCH") - return {"ok": false, "reason": "NOT_MONARCH", "msg": "你并非本帝国加冕的君主,无权调动国库施加恩泽!"} - - if float(_heal_cooldown.get(empire, 0.0)) > 0.0: - monarch_error.emit("COOLDOWN_ACTIVE") - return { - "ok": false, - "reason": "COOLDOWN_ACTIVE", - "msg": "恩泽圣愈冷却中!还需等待 %d 秒。" % int(_heal_cooldown[empire]) - } - - if get_treasury_gold(empire) < COST_HEAL_EMPIRE: - monarch_error.emit("NOT_ENOUGH_TREASURY") - return { - "ok": false, - "reason": "NOT_ENOUGH_TREASURY", - "msg": "国库资金匮乏!施展恩泽圣愈需要 %d 国库金币,当前只有 %d 金币。" % [COST_HEAL_EMPIRE, get_treasury_gold(empire)] - } - - # 扣除国库金币 - dec_treasury_gold(empire, COST_HEAL_EMPIRE) - _heal_cooldown[empire] = COOLDOWN_HEAL_EMPIRE - - # 治愈同帝国所有国民 (40250 ch->PointChange(POINT_HP, max - hp)) - var healed_count := 0 - for citizen in online_citizens: - if int(citizen.get("empire", 0)) == empire: - citizen["hp"] = citizen.get("max_hp", 1000) - citizen["sp"] = citizen.get("max_sp", 500) - healed_count += 1 - - monarch_healed_empire.emit(empire, COST_HEAL_EMPIRE, healed_count) - return { - "ok": true, - "healed_count": healed_count, - "remaining_treasury": get_treasury_gold(empire), - "msg": "君主高举天子权杖,浩荡圣光普照四野!全境 %d 位国民生命法力全部盈满!" % healed_count - } - -# 帝国战力狂暴 (PowerUp: 攻击力 +10%) -func activate_power_up(player_id: int, empire: int) -> Dictionary: - if not is_monarch(player_id, empire): - monarch_error.emit("NOT_MONARCH") - return {"ok": false, "reason": "NOT_MONARCH", "msg": "只有君主方可激发帝国战力觉醒!"} - - if float(_power_up_cooldown.get(empire, 0.0)) > 0.0: - monarch_error.emit("COOLDOWN_ACTIVE") - return { - "ok": false, - "reason": "COOLDOWN_ACTIVE", - "msg": "帝国狂暴冷却中!还需等待 %d 秒。" % int(_power_up_cooldown[empire]) - } - - if get_treasury_gold(empire) < COST_POWER_UP: - monarch_error.emit("NOT_ENOUGH_TREASURY") - return {"ok": false, "reason": "NOT_ENOUGH_TREASURY", "msg": "国库资金不足 %d 金币!" % COST_POWER_UP} - - dec_treasury_gold(empire, COST_POWER_UP) - _power_up_cooldown[empire] = COOLDOWN_POWER_UP - _power_up_active[empire] = true - _power_up_timer[empire] = DURATION_POWER_UP - - monarch_power_up_activated.emit(empire, COST_POWER_UP, DURATION_POWER_UP) - return { - "ok": true, - "duration": DURATION_POWER_UP, - "att_grade_bonus": 0.10, - "remaining_treasury": get_treasury_gold(empire), - "msg": "战神咆哮!君主调动帝国龙脉之力,全体国民攻击力提升 10%,持续 30 分钟!" - } - -# 帝国铁壁守护 (DefenseUp: 防御力 +10%) -func activate_defense_up(player_id: int, empire: int) -> Dictionary: - if not is_monarch(player_id, empire): - monarch_error.emit("NOT_MONARCH") - return {"ok": false, "reason": "NOT_MONARCH", "msg": "只有君主方可激发帝国铁壁守护!"} - - if float(_defense_up_cooldown.get(empire, 0.0)) > 0.0: - monarch_error.emit("COOLDOWN_ACTIVE") - return { - "ok": false, - "reason": "COOLDOWN_ACTIVE", - "msg": "铁壁守护冷却中!还需等待 %d 秒。" % int(_defense_up_cooldown[empire]) - } - - if get_treasury_gold(empire) < COST_DEFENSE_UP: - monarch_error.emit("NOT_ENOUGH_TREASURY") - return {"ok": false, "reason": "NOT_ENOUGH_TREASURY", "msg": "国库资金不足 %d 金币!" % COST_DEFENSE_UP} - - dec_treasury_gold(empire, COST_DEFENSE_UP) - _defense_up_cooldown[empire] = COOLDOWN_DEFENSE_UP - _defense_up_active[empire] = true - _defense_up_timer[empire] = DURATION_DEFENSE_UP - - monarch_defense_up_activated.emit(empire, COST_DEFENSE_UP, DURATION_DEFENSE_UP) - return { - "ok": true, - "duration": DURATION_DEFENSE_UP, - "def_grade_bonus": 0.10, - "remaining_treasury": get_treasury_gold(empire), - "msg": "坚不可摧!君主召唤玄龟金钟大阵,全体国民防御力提升 10%,持续 30 分钟!" - } - -# 查询帝国国民当前享受的光环加成 -func get_citizen_empire_buffs(empire: int) -> Dictionary: - if not _is_valid_empire(empire): - return {"att_bonus": 0.0, "def_bonus": 0.0} - return { - "att_bonus": 0.10 if bool(_power_up_active.get(empire, false)) else 0.0, - "def_bonus": 0.10 if bool(_defense_up_active.get(empire, false)) else 0.0, - "power_up_time_left": float(_power_up_timer.get(empire, 0.0)), - "defense_up_time_left": float(_defense_up_timer.get(empire, 0.0)) - } - -# 定时器时间步进 -func update(delta: float) -> void: - for emp in [1, 2, 3]: - # 冷却倒计时 - if _heal_cooldown[emp] > 0.0: - _heal_cooldown[emp] = max(_heal_cooldown[emp] - delta, 0.0) - if _power_up_cooldown[emp] > 0.0: - _power_up_cooldown[emp] = max(_power_up_cooldown[emp] - delta, 0.0) - if _defense_up_cooldown[emp] > 0.0: - _defense_up_cooldown[emp] = max(_defense_up_cooldown[emp] - delta, 0.0) - - # 增益持续倒计时 - if _power_up_active[emp]: - _power_up_timer[emp] -= delta - if _power_up_timer[emp] <= 0.0: - _power_up_timer[emp] = 0.0 - _power_up_active[emp] = false - - if _defense_up_active[emp]: - _defense_up_timer[emp] -= delta - if _defense_up_timer[emp] <= 0.0: - _defense_up_timer[emp] = 0.0 - _defense_up_active[emp] = false - -func _is_valid_empire(empire: int) -> bool: - return empire == EMPIRE_SHINSOO or empire == EMPIRE_CHUNJO or empire == EMPIRE_JINNO - -# 序列化与反序列化 -func serialize() -> Dictionary: - return { - "monarch_pids": _monarch_pids.duplicate(), - "monarch_names": _monarch_names.duplicate(), - "treasury": _treasury.duplicate(), - "heal_cooldown": _heal_cooldown.duplicate(), - "power_up_cooldown": _power_up_cooldown.duplicate(), - "defense_up_cooldown": _defense_up_cooldown.duplicate(), - "power_up_active": _power_up_active.duplicate(), - "defense_up_active": _defense_up_active.duplicate(), - "power_up_timer": _power_up_timer.duplicate(), - "defense_up_timer": _defense_up_timer.duplicate() - } - -func deserialize(data: Dictionary) -> void: - _monarch_pids = data.get("monarch_pids", {1: 0, 2: 0, 3: 0}).duplicate() - _monarch_names = data.get("monarch_names", {1: "", 2: "", 3: ""}).duplicate() - _treasury = data.get("treasury", {1: 0, 2: 0, 3: 0}).duplicate() - _heal_cooldown = data.get("heal_cooldown", {1: 0.0, 2: 0.0, 3: 0.0}).duplicate() - _power_up_cooldown = data.get("power_up_cooldown", {1: 0.0, 2: 0.0, 3: 0.0}).duplicate() - _defense_up_cooldown = data.get("defense_up_cooldown", {1: 0.0, 2: 0.0, 3: 0.0}).duplicate() - _power_up_active = data.get("power_up_active", {1: false, 2: false, 3: false}).duplicate() - _defense_up_active = data.get("defense_up_active", {1: false, 2: false, 3: false}).duplicate() - _power_up_timer = data.get("power_up_timer", {1: 0.0, 2: 0.0, 3: 0.0}).duplicate() - _defense_up_timer = data.get("defense_up_timer", {1: 0.0, 2: 0.0, 3: 0.0}).duplicate() diff --git a/project/monarch_empire_treasury_system.gd.uid b/project/monarch_empire_treasury_system.gd.uid deleted file mode 100644 index 2280e3f5..00000000 --- a/project/monarch_empire_treasury_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://qwwogfll4yjo diff --git a/project/monster_card_system.gd b/project/monster_card_system.gd deleted file mode 100644 index 130dbea2..00000000 --- a/project/monster_card_system.gd +++ /dev/null @@ -1,167 +0,0 @@ -# monster_card_system.gd —— Metin2 40250 怪物图鉴与百科全书系统 1:1 -# 对照 40250 服务端 monster_card.cpp, uimonstercard.py, char_battle.cpp -class_name MonsterCardSystem -extends RefCounted - -signal card_collected(mob_vnum: int, mob_name: String, count: int) -signal zone_completed(zone_name: String, bonuses: Dictionary) - -# 区域图鉴数据库配置 -const BESTIARY_DATA: Dictionary = { - "village": { - "zone_name": "新手原野", - "bonus": {"max_hp": 300}, - "monsters": { - 101: { - "name": "野狼", - "level": 1, - "element": "无", - "weakness": "物理", - "drops": ["木剑+0", "小红药水"], - "card_vnum": 70201 - }, - 391: { - "name": "白灵女将", - "level": 25, - "element": "风", - "weakness": "火", - "drops": ["狼剑+9", "红药水(中)"], - "card_vnum": 70202 - } - } - }, - "desert": { - "zone_name": "永壁沙漠", - "bonus": {"monster_dmg_pct": 3}, - "monsters": { - 2108: { - "name": "沙漠强盗", - "level": 50, - "element": "地", - "weakness": "风", - "drops": ["红药水(大)", "胡萝卜"], - "card_vnum": 70203 - }, - 2191: { - "name": "巨型沙漠龟", - "level": 67, - "element": "地", - "weakness": "风", - "drops": ["沙漠龟宝箱", "灵魂之石"], - "card_vnum": 70204 - } - } - }, - "catacomb": { - "zone_name": "地下墓穴", - "bonus": {"def": 50}, - "monsters": { - 2597: { - "name": "塔耳塔洛斯", - "level": 82, - "element": "暗", - "weakness": "圣", - "drops": ["冷酷之石"], - "card_vnum": 70205 - }, - 2598: { - "name": "冥王阿兹瑞尔", - "level": 91, - "element": "暗", - "weakness": "圣", - "drops": ["阿兹瑞尔宝箱", "泰坦之盾"], - "card_vnum": 70206 - } - } - } -} - -var collected_cards: Dictionary = {} # mob_vnum -> count -var completed_zones: Dictionary = {} # zone_key -> bool - -# 使用并收集怪物卡片 -func register_card(card_vnum: int) -> Dictionary: - # 查找对应怪物 - var target_mob_vnum := -1 - var target_mob_name := "" - var target_zone := "" - - for z_key in BESTIARY_DATA.keys(): - var z_data = BESTIARY_DATA[z_key] - for mv in z_data["monsters"].keys(): - var minfo = z_data["monsters"][mv] - if minfo["card_vnum"] == card_vnum: - target_mob_vnum = mv - target_mob_name = minfo["name"] - target_zone = z_key - break - if target_mob_vnum != -1: - break - - if target_mob_vnum == -1: - return {"ok": false, "reason": "INVALID_CARD", "msg": "这不是有效的怪物图鉴卡片!"} - - var new_count: int = int(collected_cards.get(target_mob_vnum, 0)) + 1 - collected_cards[target_mob_vnum] = new_count - - card_collected.emit(target_mob_vnum, target_mob_name, new_count) - - # 检查所属区域是否全部完成收集 - var is_newly_completed := false - var zone_bonus: Dictionary = {} - if not completed_zones.get(target_zone, false): - var all_done := true - var z_mobs = BESTIARY_DATA[target_zone]["monsters"] - for m_id in z_mobs.keys(): - if int(collected_cards.get(m_id, 0)) < 1: - all_done = false - break - - if all_done: - completed_zones[target_zone] = true - is_newly_completed = true - zone_bonus = BESTIARY_DATA[target_zone]["bonus"] - zone_completed.emit(BESTIARY_DATA[target_zone]["zone_name"], zone_bonus) - - return { - "ok": true, - "mob_vnum": target_mob_vnum, - "mob_name": target_mob_name, - "count": new_count, - "zone_completed": is_newly_completed, - "zone_bonus": zone_bonus, - "msg": "已将【%s】录入怪物图鉴!" % target_mob_name - } - -# 查看图鉴词条 (未解锁则模糊显示) -func get_monster_entry(mob_vnum: int) -> Dictionary: - for z_data in BESTIARY_DATA.values(): - if z_data["monsters"].has(mob_vnum): - var info: Dictionary = z_data["monsters"][mob_vnum].duplicate(true) - var count: int = int(collected_cards.get(mob_vnum, 0)) - if count >= 1: - info["is_unlocked"] = true - info["collected_count"] = count - return info - else: - return { - "is_unlocked": false, - "name": "???", - "level": "??", - "element": "??", - "weakness": "??", - "drops": ["未知"], - "collected_count": 0 - } - - return {"is_unlocked": false, "reason": "NOT_FOUND"} - -# 汇总所有已完成区域图鉴的常驻被动战力加成 -func get_active_collection_bonuses() -> Dictionary: - var total_bonuses: Dictionary = {} - for z_key in completed_zones.keys(): - if completed_zones[z_key]: - var b = BESTIARY_DATA[z_key]["bonus"] - for k in b.keys(): - total_bonuses[k] = total_bonuses.get(k, 0) + int(b[k]) - return total_bonuses diff --git a/project/monster_card_system.gd.uid b/project/monster_card_system.gd.uid deleted file mode 100644 index 01c60409..00000000 --- a/project/monster_card_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://kqcj87jm8elo diff --git a/project/mounted_combat_skill_system.gd b/project/mounted_combat_skill_system.gd deleted file mode 100644 index f887e19b..00000000 --- a/project/mounted_combat_skill_system.gd +++ /dev/null @@ -1,224 +0,0 @@ -# mounted_combat_skill_system.gd —— Metin2 40250 野外军马冲杀与马上技能协同打击 1:1 -# 对照 40250 服务端 char_horse.cpp, char_battle.cpp, skill.cpp -class_name MountedCombatSkillSystem -extends RefCounted - -signal horse_mounted(tier: int, horse_level: int, speed_bonus: int) -signal horse_unmounted() -signal mounted_attack_executed(target_count: int, total_dmg: int) -signal horse_skill_cast(skill_vnum: int, skill_name: String, dmg: int, hit_count: int) -signal horse_fed(item_name: String, recovered_stamina: int) - -enum HorseTier { - NONE = 0, - NORMAL = 1, # 1~10 级 (代步) - COMBAT = 2, # 11~20 级 (马上普攻) - MILITARY = 3 # 21~30 级 (马上 3 大绝技) -} - -const SKILL_HORSE_CHARGE := 121 # 马上突刺 (Horse Charge) -const SKILL_HORSE_STOMP := 122 # 踏马震地 (Horse Stomp) -const SKILL_HORSE_WAVE := 123 # 马尾狂风 (Horse Wave) - -const HORSE_SKILLS: Dictionary = { - SKILL_HORSE_CHARGE: { - "name": "马背突刺", - "mult": 2.5, - "cd": 12.0, - "sp": 60, - "range": 12.0, - "desc": "战马极速直线突进贯穿群怪,造成击飞与重创" - }, - SKILL_HORSE_STOMP: { - "name": "踏马震地", - "mult": 2.0, - "cd": 15.0, - "sp": 75, - "range": 6.0, - "desc": "战马双蹄扬起践踏大地,造成 360 度范围伤害与强力眩晕" - }, - SKILL_HORSE_WAVE: { - "name": "马尾狂风", - "mult": 1.8, - "cd": 10.0, - "sp": 50, - "range": 5.0, - "desc": "战马回旋摆尾卷起狂风,大范围震退近身敌群" - } -} - -const FOOD_ITEMS: Dictionary = { - 50054: {"name": "干草", "stamina": 30}, - 50055: {"name": "胡萝卜", "stamina": 60}, - 50056: {"name": "红参", "stamina": 100} -} - -var is_mounted: bool = false -var horse_level: int = 1 -var horse_tier: int = HorseTier.NONE -var speed_bonus: int = 0 -var stamina: int = 100 -var max_stamina: int = 100 - -var skill_cooldowns: Dictionary = { - SKILL_HORSE_CHARGE: 0.0, - SKILL_HORSE_STOMP: 0.0, - SKILL_HORSE_WAVE: 0.0 -} - -# 骑乘战马 -func mount_horse(level: int = 21) -> Dictionary: - if is_mounted: - return {"ok": false, "reason": "ALREADY_MOUNTED"} - - horse_level = clamp(level, 1, 30) - if horse_level >= 21: - horse_tier = HorseTier.MILITARY - speed_bonus = 50 - elif horse_level >= 11: - horse_tier = HorseTier.COMBAT - speed_bonus = 40 - else: - horse_tier = HorseTier.NORMAL - speed_bonus = 30 - - is_mounted = true - horse_mounted.emit(horse_tier, horse_level, speed_bonus) - - return { - "ok": true, - "tier": horse_tier, - "level": horse_level, - "speed_bonus": speed_bonus, - "msg": "跨上战马!移动速度提升 %d 点。" % speed_bonus - } - -# 下马 -func unmount_horse() -> Dictionary: - if not is_mounted: - return {"ok": false, "reason": "NOT_MOUNTED"} - - is_mounted = false - horse_tier = HorseTier.NONE - speed_bonus = 0 - horse_unmounted.emit() - return {"ok": true, "msg": "翻身下马。"} - -# 心跳刷新技能冷却 -func update(delta: float) -> void: - for sk in skill_cooldowns.keys(): - if skill_cooldowns[sk] > 0.0: - skill_cooldowns[sk] = max(0.0, skill_cooldowns[sk] - delta) - -# 马背普通连斩反击 (需战斗马以上) -func execute_mounted_attack(base_atk: int, targets: Array) -> Dictionary: - if not is_mounted: - return {"ok": false, "reason": "NOT_MOUNTED"} - - if horse_tier < HorseTier.COMBAT: - return {"ok": false, "reason": "TIER_TOO_LOW", "msg": "普通马匹无法在马背上挥砍!需升级至 11 级战斗战马。"} - - if stamina <= 0: - return {"ok": false, "reason": "EXHAUSTED", "msg": "战马体力耗尽,无法施展马战挥砍!请喂食干草或胡萝卜。"} - - # 每次攻击消耗 1 点马匹耐力 - stamina = max(0, stamina - 1) - - var hit_count = 0 - var total_dmg = 0 - # 战马提供 1.2x 基础冲击加成 - var dmg_per_hit = int(float(base_atk) * 1.2) - - for tgt in targets: - hit_count += 1 - total_dmg += dmg_per_hit - - mounted_attack_executed.emit(hit_count, total_dmg) - - return { - "ok": true, - "hit_count": hit_count, - "dmg_per_hit": dmg_per_hit, - "total_damage": total_dmg, - "stamina": stamina - } - -# 释放马上专属技能 (需军用武装战马) -func cast_horse_skill(skill_vnum: int, base_atk: int, player_data: Dictionary, targets: Array) -> Dictionary: - if not is_mounted: - return {"ok": false, "reason": "NOT_MOUNTED"} - - if horse_tier < HorseTier.MILITARY: - return {"ok": false, "reason": "TIER_TOO_LOW", "msg": "只有 21 级以上的【军用战马】才能释放马背专属技能!"} - - if not HORSE_SKILLS.has(skill_vnum): - return {"ok": false, "reason": "INVALID_HORSE_SKILL"} - - var cfg = HORSE_SKILLS[skill_vnum] - var cd: float = skill_cooldowns[skill_vnum] - if cd > 0.0: - return {"ok": false, "reason": "SKILL_ON_COOLDOWN", "msg": "技能正在冷却中!剩余 %.1f 秒" % cd} - - var sp_cost: int = cfg["sp"] - var cur_sp: int = int(player_data.get("sp", 0)) - if cur_sp < sp_cost: - return {"ok": false, "reason": "NOT_ENOUGH_SP", "msg": "内力法力不足!需要 %d 点 SP" % sp_cost} - - if stamina < 5: - return {"ok": false, "reason": "NOT_ENOUGH_STAMINA", "msg": "战马体力不足,无法施展大招!"} - - # 消耗资源 - player_data["sp"] = cur_sp - sp_cost - stamina = max(0, stamina - 5) - skill_cooldowns[skill_vnum] = cfg["cd"] - - var skill_dmg = int(float(base_atk) * cfg["mult"]) - var hit_count = targets.size() - - horse_skill_cast.emit(skill_vnum, cfg["name"], skill_dmg, hit_count) - - return { - "ok": true, - "skill_vnum": skill_vnum, - "skill_name": cfg["name"], - "damage": skill_dmg, - "hit_count": hit_count, - "stamina": stamina, - "remaining_sp": player_data["sp"], - "msg": "战马嘶鸣!施展了马上绝技【%s】!" % cfg["name"] - } - -# 喂食战马恢复体力 -func feed_horse(food_vnum: int, inventory: Array) -> Dictionary: - if not FOOD_ITEMS.has(food_vnum): - return {"ok": false, "reason": "INVALID_FOOD"} - - var slot = -1 - for i in range(inventory.size()): - if inventory[i] != null and int(inventory[i].get("vnum", 0)) == food_vnum: - slot = i - break - - if slot == -1: - return {"ok": false, "reason": "FOOD_NOT_IN_INVENTORY", "msg": "背包中没有此马粮!"} - - var item = inventory[slot] - var cnt = int(item.get("count", 1)) - if cnt > 1: - item["count"] = cnt - 1 - else: - inventory[slot] = null - - var food = FOOD_ITEMS[food_vnum] - var recovered = food["stamina"] - stamina = min(max_stamina, stamina + recovered) - - horse_fed.emit(food["name"], recovered) - - return { - "ok": true, - "food_name": food["name"], - "recovered": recovered, - "current_stamina": stamina, - "msg": "喂食了【%s】,战马体力恢复至 %d/%d!" % [food["name"], stamina, max_stamina] - } diff --git a/project/mounted_combat_skill_system.gd.uid b/project/mounted_combat_skill_system.gd.uid deleted file mode 100644 index a92bfa8d..00000000 --- a/project/mounted_combat_skill_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cllhvinbr2s86 diff --git a/project/mysterious_merchant_blackmarket_system.gd b/project/mysterious_merchant_blackmarket_system.gd deleted file mode 100644 index 0c60ad51..00000000 --- a/project/mysterious_merchant_blackmarket_system.gd +++ /dev/null @@ -1,259 +0,0 @@ -# mysterious_merchant_blackmarket_system.gd —— Metin2 40250 野外随机神秘游商与黑市折扣市场系统 1:1 -# 对照 40250 服务端 shop.cpp, regen.cpp, char_item.cpp -class_name MysteriousMerchantBlackmarketSystem -extends RefCounted - -signal merchant_appeared(map_name: String, pos: Vector2, duration_seconds: float) -signal merchant_departed() -signal item_bought(item_name: String, cost: int) -signal item_sold(item_name: String, earned: int) -signal stock_refreshed(stock_list: Array) - -const NPC_VNUM := 20095 # 野外神秘游商 (Wandering Merchant) -const DEFAULT_DURATION := 1800.0 # 停留 30 分钟 (1800 秒) -const HIGH_PRICE_BUYBACK_MULTIPLIER := 1.3 # 高价回收 +30% 黄金溢价 (1.30x) -const ITEM_ANTIFLAG_SELL := 1 << 8 # 256 禁售标志位 - -# 3 大野外候选刷新点 (沙漠绿洲、雪山孤峰、烈焰核心) -const SPAWN_LOCATIONS: Array = [ - {"map": "Desert", "name": "沙漠绿洲", "pos": Vector2(120, 350)}, - {"map": "Mount_Sohan", "name": "雪山孤峰", "pos": Vector2(450, 680)}, - {"map": "Doyyumhwan", "name": "烈焰核心", "pos": Vector2(600, 210)} -] - -# 黑市半价珍品底库预设 (40250 官方核心珍宝) -const DEFAULT_BLACK_MARKET_POOL: Array = [ - { - "vnum": 51501, - "name": "龙石炼金原石(巨龙之心)", - "base_price": 2000000, - "price": 1000000, # 5折半价 - "stock": 5, - "count": 1, - "type": 16 # ITEM_METIN / 炼金 - }, - { - "vnum": 28430, - "name": "+4 灵石礼包", - "base_price": 5000000, - "price": 2500000, # 5折半价 - "stock": 3, - "count": 1, - "type": 10 # ITEM_STONE - }, - { - "vnum": 50300, - "name": "古代进阶技能秘籍", - "base_price": 3000000, - "price": 1500000, # 5折半价 - "stock": 3, - "count": 1, - "type": 17 # ITEM_SKILLBOOK - }, - { - "vnum": 25040, - "name": "洗炼祝福包(双份)", - "base_price": 1000000, - "price": 500000, # 5折半价 - "stock": 10, - "count": 2, - "type": 3 # ITEM_USE - }, - { - "vnum": 71084, - "name": "传说洗练神丹", - "base_price": 4000000, - "price": 2000000, # 5折半价 - "stock": 2, - "count": 1, - "type": 3 # ITEM_USE - } -] - -# 当前游商运行状态 -var is_active: bool = false -var current_map: String = "" -var current_pos: Vector2 = Vector2.ZERO -var time_remaining: float = 0.0 -var total_duration: float = DEFAULT_DURATION -var current_stock: Array = [] - -# 随机刷出野外神秘游商 -func spawn_random_merchant(duration: float = DEFAULT_DURATION, loc_index: int = -1) -> Dictionary: - var loc_idx: int = loc_index - if loc_idx < 0 or loc_idx >= SPAWN_LOCATIONS.size(): - loc_idx = randi() % SPAWN_LOCATIONS.size() - - var chosen_loc: Dictionary = SPAWN_LOCATIONS[loc_idx] - return spawn_merchant(chosen_loc["map"], chosen_loc["pos"], duration) - -# 指定地点召唤/刷出游商 -func spawn_merchant(map_name: String, pos: Vector2, duration: float = DEFAULT_DURATION) -> Dictionary: - is_active = true - current_map = map_name - current_pos = pos - time_remaining = duration - total_duration = duration - - refresh_stock() - - merchant_appeared.emit(current_map, current_pos, duration) - return { - "ok": true, - "map": current_map, - "pos": current_pos, - "duration": duration, - "npc_vnum": NPC_VNUM - } - -# 游商离去或收摊 -func despawn_merchant() -> void: - if not is_active: - return - is_active = false - current_map = "" - current_pos = Vector2.ZERO - time_remaining = 0.0 - current_stock.clear() - merchant_departed.emit() - -# 物理帧或逻辑心跳更新 -func update(delta: float) -> void: - if not is_active: - return - time_remaining -= delta - if time_remaining <= 0.0: - despawn_merchant() - -# 重新生成黑市货架 (重置折扣和限量库存) -func refresh_stock() -> void: - current_stock.clear() - for item_template in DEFAULT_BLACK_MARKET_POOL: - var good: Dictionary = item_template.duplicate(true) - current_stock.append(good) - stock_refreshed.emit(current_stock) - -# 获取当前商品清单 -func get_stock() -> Array: - return current_stock.duplicate(true) - -# 购买黑市商品 -func buy_item(stock_idx: int, player_data: Dictionary, inventory: Array) -> Dictionary: - if not is_active: - return {"ok": false, "reason": "MERCHANT_NOT_ACTIVE", "msg": "神秘游商已离去或尚未现身!"} - - if stock_idx < 0 or stock_idx >= current_stock.size(): - return {"ok": false, "reason": "INVALID_STOCK_INDEX", "msg": "商品编号无效!"} - - var good: Dictionary = current_stock[stock_idx] - var current_qty := int(good.get("stock", 0)) - if current_qty <= 0: - return {"ok": false, "reason": "OUT_OF_STOCK", "msg": "该珍品黑市配额已售罄!"} - - var price := int(good.get("price", 0)) - var player_gold := int(player_data.get("gold", 0)) - if player_gold < price: - return {"ok": false, "reason": "NOT_ENOUGH_GOLD", "msg": "金币不足,无法购买黑市珍品!"} - - # 寻找背包空槽 - var target_slot := -1 - for i in range(inventory.size()): - if inventory[i] == null or (inventory[i] is Dictionary and inventory[i].is_empty()): - target_slot = i - break - - if target_slot == -1: - return {"ok": false, "reason": "INVENTORY_FULL", "msg": "背包已满,请清理后再购买!"} - - # 扣费与发货 - player_data["gold"] = player_gold - price - good["stock"] = current_qty - 1 - - var bought_item: Dictionary = { - "vnum": good["vnum"], - "name": good["name"], - "count": good.get("count", 1), - "type": good.get("type", 0) - } - inventory[target_slot] = bought_item - - item_bought.emit(good["name"], price) - - return { - "ok": true, - "slot": target_slot, - "item": bought_item, - "cost": price, - "remaining_stock": good["stock"], - "msg": "成功以黑市特惠价购入 %s!" % good["name"] - } - -# 计算高价回收金额 (+30% 溢价) -func calculate_sell_price(item: Dictionary) -> int: - var anti_flags := int(item.get("anti_flags", item.get("anti_flag", 0))) - if anti_flags & ITEM_ANTIFLAG_SELL != 0: - return 0 # 禁售品 - - var base_price := int(item.get("shop_buy_price", item.get("gold", item.get("price", 1000)))) - var count := maxi(1, int(item.get("count", 1))) - - # 40250 官方标准回收价为买入价的 1/5 (20%) - var standard_unit_sell: int = maxi(1, int(base_price / 5.0)) - - # 神秘游商提供 +30% 溢价 (1.30 倍) - var blackmarket_unit_sell: int = int(round(standard_unit_sell * HIGH_PRICE_BUYBACK_MULTIPLIER)) - - return maxi(1, blackmarket_unit_sell * count) - -# 向神秘游商高价出售闲置珍品 -func sell_item(inv_slot: int, player_data: Dictionary, inventory: Array) -> Dictionary: - if not is_active: - return {"ok": false, "reason": "MERCHANT_NOT_ACTIVE", "msg": "神秘游商已离去,无法交易!"} - - if inv_slot < 0 or inv_slot >= inventory.size() or inventory[inv_slot] == null: - return {"ok": false, "reason": "INVALID_SLOT", "msg": "背包槽位为空或无效!"} - - var item: Dictionary = inventory[inv_slot] - var anti_flags := int(item.get("anti_flags", item.get("anti_flag", 0))) - if anti_flags & ITEM_ANTIFLAG_SELL != 0: - return {"ok": false, "reason": "ITEM_ANTIFLAG_SELL", "msg": "该物品属于非卖品,无法出售!"} - - var earn_gold := calculate_sell_price(item) - if earn_gold <= 0: - return {"ok": false, "reason": "VALUATION_ZERO", "msg": "该物品估价为 0,游商不予回收!"} - - var item_name: String = str(item.get("name", "珍宝")) - - # 结算金币与移除物品 - player_data["gold"] = int(player_data.get("gold", 0)) + earn_gold - inventory[inv_slot] = null - - item_sold.emit(item_name, earn_gold) - - return { - "ok": true, - "earned": earn_gold, - "item_name": item_name, - "msg": "神秘游商高价收购了【%s】,支付了 %d 金币(+30%% 溢价)!" % [item_name, earn_gold] - } - -# 存档序列化 -func serialize() -> Dictionary: - return { - "is_active": is_active, - "current_map": current_map, - "pos_x": current_pos.x, - "pos_y": current_pos.y, - "time_remaining": time_remaining, - "total_duration": total_duration, - "stock": current_stock.duplicate(true) - } - -# 读档反序列化 -func deserialize(data: Dictionary) -> void: - is_active = bool(data.get("is_active", false)) - current_map = str(data.get("current_map", "")) - current_pos = Vector2(float(data.get("pos_x", 0.0)), float(data.get("pos_y", 0.0))) - time_remaining = float(data.get("time_remaining", 0.0)) - total_duration = float(data.get("total_duration", DEFAULT_DURATION)) - current_stock = data.get("stock", []).duplicate(true) diff --git a/project/mysterious_merchant_blackmarket_system.gd.uid b/project/mysterious_merchant_blackmarket_system.gd.uid deleted file mode 100644 index 57c4d2b4..00000000 --- a/project/mysterious_merchant_blackmarket_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://2j2dfg6igg2c diff --git a/project/npc_shop_system.gd b/project/npc_shop_system.gd deleted file mode 100644 index a14f0d36..00000000 --- a/project/npc_shop_system.gd +++ /dev/null @@ -1,187 +0,0 @@ -# npc_shop_system.gd —— NPC 商店库存配置与买卖经济闭环(40250 官方 1:1 对齐) -# 对齐源码: -# - 40250 server/game/src/shop.cpp:450-580 (Buy, Sell, ITEM_ANTIFLAG_SELL, dwPrice /= 5) -# - 40250 root/uishop.py (ShopDialog, SetItemData, Buy, Sell) -# - 40250 uiscript/shopdialog.py (40 格商店货架) -class_name NpcShopSystem -extends RefCounted - -const SHOP_HOST_ITEM_MAX_NUM := 40 -const ITEM_ANTIFLAG_SELL := 1 << 8 -const ITEM_FLAG_COUNT_PER_1GOLD := 1 << 3 - -# NPC Vnum 常量 -const NPC_WEAPON_SHOP := 9001 # 武器商人 -const NPC_ARMOR_SHOP := 9002 # 防具商人 -const NPC_GENERAL_STORE := 9003 # 杂货商人 -const NPC_FISHERMAN := 9009 # 渔夫 -const NPC_DEONKBAE := 20015 # 矿工德安克贝 - -# 40250 官方标准商店货架预设表 -const NPC_SHOPS := { - NPC_GENERAL_STORE: [ - {"pos": 0, "vnum": 27001, "count": 200, "price": 1200, "name": "小型红药水"}, - {"pos": 1, "vnum": 27002, "count": 200, "price": 4800, "name": "中型红药水"}, - {"pos": 2, "vnum": 27003, "count": 200, "price": 16000, "name": "大型红药水"}, - {"pos": 3, "vnum": 27004, "count": 200, "price": 2400, "name": "小型蓝药水"}, - {"pos": 4, "vnum": 27005, "count": 200, "price": 9600, "name": "中型蓝药水"}, - {"pos": 5, "vnum": 27006, "count": 200, "price": 32000, "name": "大型蓝药水"}, - {"pos": 6, "vnum": 22001, "count": 1, "price": 10000, "name": "回城卷轴"}, - {"pos": 7, "vnum": 22010, "count": 1, "price": 12000, "name": "地标传送卷轴"}, - {"pos": 8, "vnum": 27800, "count": 50, "price": 2000, "name": "鱼饵(蚯蚓)"}, - {"pos": 9, "vnum": 27400, "count": 1, "price": 50000, "name": "钓竿+0"}, - {"pos": 10, "vnum": 29101, "count": 1, "price": 80000, "name": "矿镐+0"}, - ], - NPC_WEAPON_SHOP: [ - {"pos": 0, "vnum": 10, "count": 1, "price": 1000, "name": "长剑+0"}, - {"pos": 1, "vnum": 1000, "count": 1, "price": 1000, "name": "匕首+0"}, - {"pos": 2, "vnum": 2000, "count": 1, "price": 1000, "name": "短弓+0"}, - {"pos": 3, "vnum": 3000, "count": 1, "price": 1000, "name": "长戟+0"}, - {"pos": 4, "vnum": 5000, "count": 1, "price": 1000, "name": "古铜铃+0"}, - {"pos": 5, "vnum": 7000, "count": 1, "price": 1000, "name": "折扇+0"}, - {"pos": 6, "vnum": 8000, "count": 200, "price": 100, "name": "木箭"}, - ], - NPC_ARMOR_SHOP: [ - {"pos": 0, "vnum": 11200, "count": 1, "price": 1000, "name": "武士甲+0"}, - {"pos": 1, "vnum": 11400, "count": 1, "price": 1000, "name": "刺客服+0"}, - {"pos": 2, "vnum": 11600, "count": 1, "price": 1000, "name": "幽灵袍+0"}, - {"pos": 3, "vnum": 11800, "count": 1, "price": 1000, "name": "萨满裙+0"}, - {"pos": 4, "vnum": 12200, "count": 1, "price": 1000, "name": "铁盔+0"}, - {"pos": 5, "vnum": 13000, "count": 1, "price": 1000, "name": "战斗盾+0"}, - ] -} - -## 获取指定 NPC 的商店商品列表 -static func get_shop_items(npc_vnum: int) -> Array: - return NPC_SHOPS.get(npc_vnum, []).duplicate(true) - -## 检查 NPC 是否拥有商店 -static func has_shop(npc_vnum: int) -> bool: - return NPC_SHOPS.has(npc_vnum) - -## 计算物品出售折价 (40250 shop.cpp:520: dwPrice /= 5) -## 官方折价逻辑:一般物品回收价为买入价的 1/5(向上取整保底 1 金币) -static func calculate_sell_price(item_proto: Dictionary, count := 1) -> int: - var anti_flag := int(item_proto.get("anti_flags", item_proto.get("anti_flag", 0))) - if anti_flag & ITEM_ANTIFLAG_SELL != 0: - return 0 # 禁售物品不可回收 - - var buy_price := int(item_proto.get("shop_buy_price", item_proto.get("gold", 100))) - var flag := int(item_proto.get("flags", item_proto.get("flag", 0))) - var unit_price: int = 1 - - if flag & ITEM_FLAG_COUNT_PER_1GOLD != 0: - unit_price = 1 - else: - unit_price = maxi(1, buy_price / 5) - - return unit_price * maxi(1, count) - -## 执行商店购买事务 (40250 shop.cpp Buy) -## player_ctx: {"gold": int, "inventory": Array} -static func buy_item(shop_pos: int, count: int, npc_vnum: int, player_ctx: Dictionary) -> Dictionary: - var items := get_shop_items(npc_vnum) - var target_good: Dictionary = {} - for it in items: - if int(it.get("pos", -1)) == shop_pos: - target_good = it - break - - if target_good.is_empty(): - return {"ok": false, "code": "SHOP_INVALID_POS", "msg": "该商品位置无效!"} - - var buy_count := maxi(1, count) - var price_per_pack := int(target_good.get("price", 0)) - var pack_count := int(target_good.get("count", 1)) - var total_cost: int = price_per_pack * buy_count - - var current_gold := int(player_ctx.get("gold", 0)) - if current_gold < total_cost: - return { - "ok": false, - "code": "NOT_ENOUGH_GOLD", - "msg": "金币不足,无法购买!(需要 %d 金币,当前拥有 %d)" % [total_cost, current_gold] - } - - # 检查背包剩余空位 - var inv: Array = player_ctx.get("inventory", []) - var free_cell := -1 - for cell in 90: # 0..89 背包槽 - var occupied := false - for inv_item in inv: - if int(inv_item.get("cell", -1)) == cell: - occupied = true - break - if not occupied: - free_cell = cell - break - - if free_cell == -1: - return {"ok": false, "code": "INVENTORY_FULL", "msg": "背包空间不足,无法放入商品!"} - - # 扣款并给物品 - player_ctx["gold"] = current_gold - total_cost - var new_item := { - "cell": free_cell, - "vnum": int(target_good.get("vnum", 0)), - "count": pack_count * buy_count, - "sockets": [0, 0, 0], - "attrs": [] - } - inv.append(new_item) - player_ctx["inventory"] = inv - - return { - "ok": true, - "code": "SUCCESS", - "cost": total_cost, - "cell": free_cell, - "item": new_item, - "msg": "购买成功!花费了 %d 金币。" % total_cost - } - -## 执行商店出售事务 (40250 shop.cpp Sell) -## inv_cell: 背包槽位 -static func sell_item(inv_cell: int, count: int, player_ctx: Dictionary, proto_getter: Callable) -> Dictionary: - var inv: Array = player_ctx.get("inventory", []) - var target_idx := -1 - var target_item: Dictionary = {} - - for i in inv.size(): - if int(inv[i].get("cell", -1)) == inv_cell: - target_idx = i - target_item = inv[i] - break - - if target_item.is_empty(): - return {"ok": false, "code": "NO_ITEM", "msg": "背包该格没有物品!"} - - var vnum := int(target_item.get("vnum", 0)) - var proto: Dictionary = proto_getter.call(vnum) if proto_getter.is_valid() else {} - - # 校验是否禁售 - var anti_flag := int(proto.get("anti_flags", proto.get("anti_flag", 0))) - if anti_flag & ITEM_ANTIFLAG_SELL != 0: - return {"ok": false, "code": "ITEM_ANTIFLAG_SELL", "msg": "该物品为非卖品,无法向商人出售!"} - - var cur_count := int(target_item.get("count", 1)) - var sell_count := mini(cur_count, maxi(1, count)) - var earn_gold := calculate_sell_price(proto, sell_count) - - # 扣减物品 - if cur_count <= sell_count: - inv.remove_at(target_idx) - else: - target_item["count"] = cur_count - sell_count - - player_ctx["inventory"] = inv - player_ctx["gold"] = int(player_ctx.get("gold", 0)) + earn_gold - - return { - "ok": true, - "code": "SUCCESS", - "gold_earned": earn_gold, - "sold_vnum": vnum, - "sold_count": sell_count, - "msg": "出售成功!获得了 %d 金币。" % earn_gold - } diff --git a/project/npc_shop_system.gd.uid b/project/npc_shop_system.gd.uid deleted file mode 100644 index 80f3e3bd..00000000 --- a/project/npc_shop_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c2gkxa7firckb diff --git a/project/offline_bazaar_market_system.gd b/project/offline_bazaar_market_system.gd deleted file mode 100644 index e6238652..00000000 --- a/project/offline_bazaar_market_system.gd +++ /dev/null @@ -1,311 +0,0 @@ -# offline_bazaar_market_system.gd —— Metin2 40250 单人单机广场离线摆摊与虚拟交易市场 1:1 -# 对照 40250 服务端 char_shop.cpp, shop.cpp, shop_manager.cpp -class_name OfflineBazaarMarketSystem -extends RefCounted - -signal stall_browsed(stall_id: int, stall_name: String) -signal item_bought_from_stall(stall_id: int, item_name: String, count: int, total_price: int) -signal player_stall_opened(title: String, item_count: int) -signal player_stall_item_sold(item_name: String, count: int, price: int) -signal player_stall_closed(revenue_collected: int) - -const ITEM_BUNDLE_VNUM := 50200 # 摆摊包袱 (Bundle) - -# 广场内置 3 大经典 NPC/虚拟玩家货摊 -var npc_stalls: Dictionary = { - 1: { - "id": 1, - "owner": "阿隆", - "title": "【稀缺材料专营店】", - "pos": Vector3(12.0, 0.0, 8.0), - "items": [ - {"vnum": 27993, "name": "珍珠", "price": 500000, "stock": 5}, - {"vnum": 27992, "name": "白石", "price": 300000, "stock": 10}, - {"vnum": 30040, "name": "蜘蛛眼", "price": 50000, "stock": 20}, - {"vnum": 50513, "name": "灵魂之石", "price": 1200000, "stock": 3} - ] - }, - 2: { - "id": 2, - "owner": "铁峰", - "title": "【+4 强化灵石阁】", - "pos": Vector3(18.0, 0.0, 8.0), - "items": [ - {"vnum": 28430, "name": "战士灵石+4", "price": 800000, "stock": 4}, - {"vnum": 28431, "name": "忍者灵石+4", "price": 800000, "stock": 4}, - {"vnum": 28432, "name": "修罗灵石+4", "price": 800000, "stock": 4}, - {"vnum": 28435, "name": "魔石灵石+4", "price": 1500000, "stock": 2} - ] - }, - 3: { - "id": 3, - "owner": "云游仙商", - "title": "【珍品洗炼附魔阁】", - "pos": Vector3(15.0, 0.0, 14.0), - "items": [ - {"vnum": 71084, "name": "属性改变秘籍", "price": 2000000, "stock": 5}, - {"vnum": 71085, "name": "添加属性秘籍", "price": 1500000, "stock": 5}, - {"vnum": 70024, "name": "祝福宝珠", "price": 3000000, "stock": 2}, - {"vnum": 71003, "name": "祝福卷轴", "price": 600000, "stock": 15} - ] - } -} - -# 玩家自己开设的离线个人商摊 -var player_stall: Dictionary = { - "is_open": false, - "title": "", - "items": [], # Array of { "vnum", "name", "count", "price" } - "revenue": 0, - "total_sales_count": 0 -} - -# 获取所有广场摊位列表 -func get_all_stalls() -> Array: - var list: Array = [] - for sid in npc_stalls.keys(): - var s = npc_stalls[sid] - list.append({ - "id": s["id"], - "owner": s["owner"], - "title": s["title"], - "pos": s["pos"], - "item_count": s["items"].size() - }) - if player_stall["is_open"]: - list.append({ - "id": 999, - "owner": "玩家自己", - "title": player_stall["title"], - "pos": Vector3(0.0, 0.0, 0.0), - "item_count": player_stall["items"].size() - }) - return list - -# 浏览特定摊位商品 -func browse_stall(stall_id: int) -> Dictionary: - if not npc_stalls.has(stall_id): - if stall_id == 999 and player_stall["is_open"]: - stall_browsed.emit(999, player_stall["title"]) - return {"ok": true, "stall": player_stall} - return {"ok": false, "reason": "STALL_NOT_FOUND", "msg": "未找到该摊位!"} - - var stall = npc_stalls[stall_id] - stall_browsed.emit(stall_id, stall["title"]) - return { - "ok": true, - "id": stall_id, - "owner": stall["owner"], - "title": stall["title"], - "items": stall["items"] - } - -# 从 NPC/虚拟摊位购买物品 -func buy_from_stall(stall_id: int, item_idx: int, count: int, player_data: Dictionary, inventory: Array) -> Dictionary: - if not npc_stalls.has(stall_id): - return {"ok": false, "reason": "STALL_NOT_FOUND", "msg": "未找到该摊位!"} - - var stall = npc_stalls[stall_id] - var items: Array = stall["items"] - if item_idx < 0 or item_idx >= items.size(): - return {"ok": false, "reason": "ITEM_INDEX_INVALID", "msg": "无效的商品索引!"} - - var target_goods = items[item_idx] - var available_stock: int = int(target_goods.get("stock", 0)) - if available_stock < count: - return {"ok": false, "reason": "OUT_OF_STOCK", "msg": "库存不足!剩余: %d" % available_stock} - - var total_cost: int = int(target_goods["price"]) * count - var current_gold: int = int(player_data.get("gold", 0)) - if current_gold < total_cost: - return {"ok": false, "reason": "NOT_ENOUGH_GOLD", "msg": "金币不足!需要: %d, 当前: %d" % [total_cost, current_gold]} - - # 检查背包空位 - var empty_slot = -1 - for i in range(inventory.size()): - if inventory[i] == null: - empty_slot = i - break - - if empty_slot == -1: - return {"ok": false, "reason": "INVENTORY_FULL", "msg": "背包已满,无法容纳新商品!"} - - # 扣费 - player_data["gold"] = current_gold - total_cost - target_goods["stock"] = available_stock - count - - # 入包 - var new_item: Dictionary = { - "vnum": target_goods["vnum"], - "name": target_goods["name"], - "count": count - } - inventory[empty_slot] = new_item - - item_bought_from_stall.emit(stall_id, target_goods["name"], count, total_cost) - - return { - "ok": true, - "item": new_item, - "slot": empty_slot, - "cost": total_cost, - "remaining_gold": player_data["gold"], - "msg": "成功购买【%s】x%d,花费 %d 金币!" % [target_goods["name"], count, total_cost] - } - -# 玩家开设自己的离线摆摊 -# items_to_sell: Array of { "slot": int, "price": int, "count": int } -func open_player_stall(title: String, items_to_sell: Array, inventory: Array, require_bundle: bool = true) -> Dictionary: - if player_stall["is_open"]: - return {"ok": false, "reason": "ALREADY_HAS_STALL", "msg": "你已开设了一个摊位!"} - - if items_to_sell.is_empty(): - return {"ok": false, "reason": "NO_ITEMS_TO_SELL", "msg": "请至少上架一件物品!"} - - # 摆摊包袱校验 (50200) - var bundle_slot = -1 - if require_bundle: - for i in range(inventory.size()): - if inventory[i] != null and int(inventory[i].get("vnum", 0)) == ITEM_BUNDLE_VNUM: - bundle_slot = i - break - if bundle_slot == -1: - return {"ok": false, "reason": "NEED_BUNDLE_ITEM", "msg": "需要【摆摊包袱】(50200) 才能在广场开设个人货摊!"} - - # 收集上架物品并从背包移出 - var stall_items: Array = [] - for spec in items_to_sell: - var slot_id: int = int(spec["slot"]) - var price: int = int(spec["price"]) - var sell_count: int = int(spec.get("count", 1)) - - if slot_id < 0 or slot_id >= inventory.size() or inventory[slot_id] == null: - continue - - var inv_item: Dictionary = inventory[slot_id] - var cur_count: int = int(inv_item.get("count", 1)) - - if sell_count >= cur_count: - # 全量上架 - var listed_item = inv_item.duplicate(true) - listed_item["price"] = price - stall_items.append(listed_item) - inventory[slot_id] = null - else: - # 部分上架 - var listed_item = inv_item.duplicate(true) - listed_item["count"] = sell_count - listed_item["price"] = price - stall_items.append(listed_item) - inv_item["count"] = cur_count - sell_count - - if stall_items.is_empty(): - return {"ok": false, "reason": "NO_VALID_ITEMS", "msg": "没有有效可上架的物品!"} - - # 消耗摆摊包袱 - if bundle_slot != -1: - var b_item = inventory[bundle_slot] - var b_cnt = int(b_item.get("count", 1)) - if b_cnt > 1: - b_item["count"] = b_cnt - 1 - else: - inventory[bundle_slot] = null - - player_stall["is_open"] = true - player_stall["title"] = title if not title.strip_edges().is_empty() else "【离线便民杂货店】" - player_stall["items"] = stall_items - player_stall["revenue"] = 0 - player_stall["total_sales_count"] = 0 - - player_stall_opened.emit(player_stall["title"], stall_items.size()) - - return { - "ok": true, - "title": player_stall["title"], - "items_count": stall_items.size(), - "msg": "个人摊位【%s】开设成功!上架了 %d 件珍品。" % [player_stall["title"], stall_items.size()] - } - -# 模拟单机挂机离线交易成交 -func simulate_offline_sales(elapsed_minutes: float) -> Dictionary: - if not player_stall["is_open"] or player_stall["items"].is_empty(): - return {"sold_count": 0, "earned_gold": 0} - - var sold_items: Array = [] - var earned: int = 0 - var remaining_items: Array = [] - - # 根据挂机时长计算可能成交的件数 - var max_potential_sales = int(elapsed_minutes / 5.0) + 1 - var actual_sales = 0 - - # 随离线时长提升成交几率(满30分钟保底成交) - var sell_prob = clampf(0.5 + (elapsed_minutes / 30.0) * 0.5, 0.0, 1.0) - for item in player_stall["items"]: - if actual_sales < max_potential_sales and randf() <= sell_prob: - actual_sales += 1 - var item_rev = int(item["price"]) * int(item.get("count", 1)) - earned += item_rev - player_stall["revenue"] += item_rev - player_stall["total_sales_count"] += 1 - sold_items.append(item) - player_stall_item_sold.emit(item["name"], int(item.get("count", 1)), item_rev) - else: - remaining_items.append(item) - - player_stall["items"] = remaining_items - - return { - "sold_count": sold_items.size(), - "earned_gold": earned, - "remaining_items": remaining_items.size() - } - -# 领取摊位赚取的金币 -func collect_stall_revenue(player_data: Dictionary) -> Dictionary: - if not player_stall["is_open"]: - return {"ok": false, "reason": "NO_ACTIVE_STALL"} - - var rev: int = player_stall["revenue"] - if rev <= 0: - return {"ok": false, "reason": "NO_REVENUE", "msg": "当前没有可领取的摊位营收!"} - - player_data["gold"] = int(player_data.get("gold", 0)) + rev - player_stall["revenue"] = 0 - - return { - "ok": true, - "collected_gold": rev, - "current_gold": player_data["gold"], - "msg": "成功领取摊位营业收入 %d 金币!" % rev - } - -# 收摊并退回未售出物品 -func close_player_stall(inventory: Array, player_data: Dictionary) -> Dictionary: - if not player_stall["is_open"]: - return {"ok": false, "reason": "NO_ACTIVE_STALL"} - - var rev: int = player_stall["revenue"] - if rev > 0: - player_data["gold"] = int(player_data.get("gold", 0)) + rev - player_stall["revenue"] = 0 - - var returned_count: int = 0 - for item in player_stall["items"]: - # 寻空位退回 - for i in range(inventory.size()): - if inventory[i] == null: - inventory[i] = item - returned_count += 1 - break - - player_stall["is_open"] = false - player_stall["items"] = [] - player_stall_closed.emit(rev) - - return { - "ok": true, - "revenue_collected": rev, - "returned_items": returned_count, - "msg": "已顺利收摊!营业额 %d 金币已汇入背包,%d 件未售物品已退回。" % [rev, returned_count] - } diff --git a/project/offline_bazaar_market_system.gd.uid b/project/offline_bazaar_market_system.gd.uid deleted file mode 100644 index 7fb83c08..00000000 --- a/project/offline_bazaar_market_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b3jvh20s4ckbp diff --git a/project/party_management_system.gd b/project/party_management_system.gd deleted file mode 100644 index f57c7c72..00000000 --- a/project/party_management_system.gd +++ /dev/null @@ -1,216 +0,0 @@ -# party_management_system.gd —— 40250 8人组队面板、经验分配与领导力光环系统 1:1 -# 严格对照: -# metin2/src/server/game/src/party.h, party.cpp, char_party.cpp -# Client/Eternexus/root/uiparty.py -# ClientVS22/source/UserInterface/PythonPlayer.h (PARTY_AFFECT_*) -extends RefCounted - -const PARTY_MAX_MEMBER := 8 # 官方最大 8 人队伍 - -# 经验分配模式 (char_party.cpp) -const EXP_MODE_LEVEL := 0 # 按等级比例分配 (NON_PARITY) -const EXP_MODE_PARITY := 1 # 均分模式 (PARITY) - -# 队长战术领导力职权角色分配 (uiparty.py / PythonPlayer.h) -const ROLE_NORMAL := 0 # 普通队员 -const ROLE_ATTACKER := 1 # 攻击者 (增加物理攻击力) -const ROLE_TANKER := 2 # 肉盾 (增加生命值上限) -const ROLE_BUFFER := 3 # 增益者 (增加法术/技能攻击力) -const ROLE_SKILL_MASTER := 4 # 技能大师 (减少技能冷却时间) -const ROLE_DEFENDER := 5 # 防御者 (增加防御力) -const ROLE_BERSERKER := 6 # 狂战士 (增加攻击速度) - -# 角色光环属性加成表 -const ROLE_BUFF_BONUSES := { - ROLE_ATTACKER: { "stat": "att_grade", "val": 30 }, - ROLE_TANKER: { "stat": "max_hp", "val": 2500 }, - ROLE_BUFFER: { "stat": "magic_att", "val": 25 }, - ROLE_SKILL_MASTER: { "stat": "cooldown_reduction", "val": 15 }, - ROLE_DEFENDER: { "stat": "def_grade", "val": 40 }, - ROLE_BERSERKER: { "stat": "att_speed", "val": 15 } -} - -class PartyMember: - var pid: int - var name: String - var level: int - var is_leader: bool = false - var role: int = ROLE_NORMAL - var cur_hp: int = 1000 - var max_hp: int = 1000 - - func _init(p_pid: int, p_name: String, p_lvl: int, p_leader: bool = false) -> void: - pid = p_pid - name = p_name - level = p_lvl - is_leader = p_leader - role = ROLE_NORMAL - cur_hp = 1000 - max_hp = 1000 - - func get_hp_percent() -> float: - if max_hp <= 0: - return 0.0 - return clamp(float(cur_hp) / float(max_hp), 0.0, 1.0) - -var members: Dictionary = {} # pid -> PartyMember -var leader_pid: int = -1 -var exp_distribution_mode: int = EXP_MODE_PARITY - -func _init(p_leader_pid: int = -1, p_leader_name: String = "", p_leader_lvl: int = 1) -> void: - members.clear() - leader_pid = -1 - exp_distribution_mode = EXP_MODE_PARITY - if p_leader_pid > 0: - create_party(p_leader_pid, p_leader_name, p_leader_lvl) - -# 1. 创建队伍 -func create_party(p_pid: int, p_name: String, p_lvl: int) -> bool: - members.clear() - leader_pid = p_pid - var leader = PartyMember.new(p_pid, p_name, p_lvl, true) - members[p_pid] = leader - return true - -# 2. 邀请/加入队员 (CParty::Join) -func add_member(p_pid: int, p_name: String, p_lvl: int) -> Dictionary: - var res := { "success": false, "error_code": "OK", "message": "" } - - if members.size() >= PARTY_MAX_MEMBER: - res["error_code"] = "PARTY_FULL" - res["message"] = "队伍人数已达到 8 人上限。" - return res - - if members.has(p_pid): - res["error_code"] = "ALREADY_IN_PARTY" - res["message"] = "该玩家已在队伍中。" - return res - - var m = PartyMember.new(p_pid, p_name, p_lvl, false) - members[p_pid] = m - res["success"] = true - res["message"] = "%s 加入了队伍。" % p_name - return res - -# 3. 踢出队员 (CParty::Quit / Expel) -func expel_member(caller_pid: int, target_pid: int) -> Dictionary: - var res := { "success": false, "error_code": "OK", "message": "" } - - if caller_pid != leader_pid: - res["error_code"] = "NOT_LEADER" - res["message"] = "只有队长才能踢出队员。" - return res - - if not members.has(target_pid): - res["error_code"] = "MEMBER_NOT_FOUND" - return res - - if target_pid == leader_pid: - res["error_code"] = "CANNOT_EXPEL_LEADER" - res["message"] = "队长不能踢出自己。" - return res - - var target_name: String = members[target_pid].name - members.erase(target_pid) - res["success"] = true - res["message"] = "%s 已被移出队伍。" % target_name - return res - -# 4. 离队与队长顺延/解散 (CParty::Quit) -func leave_party(p_pid: int) -> Dictionary: - var res := { "success": false, "dissolved": false, "new_leader": -1 } - if not members.has(p_pid): - return res - - var was_leader: bool = (p_pid == leader_pid) - members.erase(p_pid) - - if members.is_empty(): - leader_pid = -1 - res["dissolved"] = true - res["success"] = true - return res - - # 若队长离队,移交给第一位队员 - if was_leader: - var new_lead_key: int = members.keys()[0] - leader_pid = new_lead_key - members[new_lead_key].is_leader = true - res["new_leader"] = new_lead_key - - res["success"] = true - return res - -# 5. 队长战术领导力角色光环配置 (CParty::SetRole) -func set_member_role(caller_pid: int, target_pid: int, role: int) -> Dictionary: - var res := { "success": false, "error_code": "OK", "message": "" } - - if caller_pid != leader_pid: - res["error_code"] = "NOT_LEADER" - res["message"] = "只有队长有权分配战术光环。" - return res - - if not members.has(target_pid): - res["error_code"] = "MEMBER_NOT_FOUND" - return res - - # 检查角色是否已被队伍中其他成员占用 (非 ROLE_NORMAL) - if role != ROLE_NORMAL: - for k in members: - if k != target_pid and members[k].role == role: - res["error_code"] = "ROLE_ALREADY_ASSIGNED" - res["message"] = "该战术光环已被其他队员占用。" - return res - - members[target_pid].role = role - res["success"] = true - res["role"] = role - res["buff"] = ROLE_BUFF_BONUSES.get(role, {}) - res["message"] = "成功为队员分配战术角色。" - return res - -# 6. 获取队员当前享有的领导力光环加成 -func get_member_buff(p_pid: int) -> Dictionary: - if not members.has(p_pid): - return {} - var r: int = members[p_pid].role - return ROLE_BUFF_BONUSES.get(r, {}) - -# 7. 切换经验分配模式 -func set_exp_distribution_mode(caller_pid: int, mode: int) -> bool: - if caller_pid != leader_pid: - return false - if mode == EXP_MODE_PARITY or mode == EXP_MODE_LEVEL: - exp_distribution_mode = mode - return true - return false - -# 8. 队伍击杀经验结算与分发 (1:1 CParty::DistributeExp) -func distribute_exp(total_exp: int) -> Dictionary: - var distribution := {} - if members.is_empty() or total_exp <= 0: - return distribution - - var count := members.size() - - if exp_distribution_mode == EXP_MODE_PARITY: - # 均分模式: 每个队员获得 total_exp / count - var per_member: int = total_exp / count - for k in members: - distribution[k] = per_member - else: - # 等级权重模式: 按队员等级比例分配 - var sum_levels: int = 0 - for k in members: - sum_levels += members[k].level - for k in members: - var share: int = int(float(total_exp) * float(members[k].level) / float(sum_levels)) - distribution[k] = share - - return distribution - -# 9. 同步队员生命值百分比 -func sync_member_hp(p_pid: int, cur: int, max_h: int) -> void: - if members.has(p_pid): - members[p_pid].cur_hp = cur - members[p_pid].max_hp = max_h diff --git a/project/party_management_system.gd.uid b/project/party_management_system.gd.uid deleted file mode 100644 index b8d1acec..00000000 --- a/project/party_management_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bx3vdhv1jsl2h diff --git a/project/pet_companion_system.gd b/project/pet_companion_system.gd deleted file mode 100644 index 8232fff9..00000000 --- a/project/pet_companion_system.gd +++ /dev/null @@ -1,153 +0,0 @@ -# pet_companion_system.gd —— Metin2 40250 伴随宠物封印召唤与战力注入系统 1:1 -# 对照 40250 服务端 PetSystem.cpp, char_pet.cpp -class_name PetCompanionSystem -extends RefCounted - -signal pet_summoned(pet_name: String, vnum: int) -signal pet_dismissed(pet_name: String) -signal pet_state_changed(state: String) - -const PET_TABLE: Dictionary = { - 53001: { - "name": "火凤凰", - "mob_vnum": 34001, - "bonuses": {"max_hp": 1500, "monster_dmg_pct": 15}, - "desc": "最大生命 +1500,对怪物伤害提升 15%" - }, - 53002: { - "name": "幼鹿", - "mob_vnum": 34002, - "bonuses": {"max_hp": 1000, "exp_pct": 15}, - "desc": "最大生命 +1000,获得经验值增加 15%" - }, - 53003: { - "name": "冰凤凰", - "mob_vnum": 34003, - "bonuses": {"max_hp": 1500, "def_pct": 15}, - "desc": "最大生命 +1500,物理防御提升 15%" - }, - 53005: { - "name": "小胖熊猫", - "mob_vnum": 34005, - "bonuses": {"max_hp": 1000, "critical_pct": 15}, - "desc": "最大生命 +1000,致命一击几率提升 15%" - }, - 53006: { - "name": "幼狮", - "mob_vnum": 34006, - "bonuses": {"max_hp": 1500, "att_pct": 10}, - "desc": "最大生命 +1500,物理攻击力提升 10%" - } -} - -const FOLLOW_STOP_DIST := 2.0 # 停止距离 -const FOLLOW_RUN_DIST := 3.5 # 开始跟随奔跑距离 -const TELEPORT_DIST := 25.0 # 瞬间拉回距离 -const PET_MOVE_SPEED := 4.5 # 移动速度 (m/s) - -var is_summoned: bool = false -var current_seal_vnum: int = 0 -var active_pet: Dictionary = {} -var pet_position: Vector3 = Vector3.ZERO -var pet_state: String = "wait" # "wait" 或 "run" -var remaining_time: float = 0.0 - -# 召唤 / 收回宠物 (右键点击宠物封印石) -func toggle_pet_seal(seal_item: Dictionary, player_pos: Vector3) -> Dictionary: - var vnum: int = int(seal_item.get("vnum", 0)) - if not PET_TABLE.has(vnum): - return {"ok": false, "reason": "NOT_A_PET_SEAL", "msg": "这不是有效的宠物封印!"} - - # 若当前已召唤同一只宠物,则收回 - if is_summoned and current_seal_vnum == vnum: - var pet_name = active_pet.get("name", "宠物") - dismiss_pet() - return { - "ok": true, - "action": "dismiss", - "pet_name": pet_name, - "msg": "已将【%s】收回封印石。" % pet_name - } - - # 若当前召唤了不同宠物,先收回 - if is_summoned: - dismiss_pet() - - # 召唤新宠物 - var cfg: Dictionary = PET_TABLE[vnum] - current_seal_vnum = vnum - is_summoned = true - remaining_time = float(seal_item.get("duration", 86400.0)) # 默认 24 小时 - pet_position = player_pos + Vector3(-1.5, 0.0, -1.5) - pet_state = "wait" - - active_pet = { - "vnum": vnum, - "name": cfg["name"], - "mob_vnum": cfg["mob_vnum"], - "bonuses": cfg["bonuses"].duplicate() - } - - pet_summoned.emit(cfg["name"], vnum) - - return { - "ok": true, - "action": "summon", - "pet_name": cfg["name"], - "bonuses": cfg["bonuses"], - "msg": "伴随宠物【%s】已召唤出战!" % cfg["name"] - } - -# 收回当前宠物 -func dismiss_pet() -> void: - if not is_summoned: - return - - var pname = active_pet.get("name", "") - is_summoned = false - current_seal_vnum = 0 - active_pet.clear() - pet_state = "wait" - - pet_dismissed.emit(pname) - -# 获取当前宠物注入的战力属性 -func get_active_bonuses() -> Dictionary: - if not is_summoned: - return {} - return active_pet.get("bonuses", {}).duplicate() - -# 3D 伴随移动逻辑 -func update_movement(player_pos: Vector3, dt: float) -> void: - if not is_summoned: - return - - var dist: float = pet_position.distance_to(player_pos) - - # 超远距离瞬间传回身边 - if dist >= TELEPORT_DIST: - pet_position = player_pos + Vector3(-1.5, 0.0, -1.5) - pet_state = "wait" - pet_state_changed.emit("wait") - return - - # 跟随靠近 - if dist > FOLLOW_STOP_DIST: - var dir := (player_pos - pet_position).normalized() - pet_position += dir * PET_MOVE_SPEED * dt - if pet_state != "run": - pet_state = "run" - pet_state_changed.emit("run") - else: - if pet_state != "wait": - pet_state = "wait" - pet_state_changed.emit("wait") - -# 逐帧倒计时衰减 -func update(dt: float) -> void: - if not is_summoned: - return - - remaining_time -= dt - if remaining_time <= 0.0: - dismiss_pet() diff --git a/project/pet_companion_system.gd.uid b/project/pet_companion_system.gd.uid deleted file mode 100644 index 520445ab..00000000 --- a/project/pet_companion_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ctj2muscnf5l3 diff --git a/project/pk_penalty_system.gd b/project/pk_penalty_system.gd deleted file mode 100644 index a127ab00..00000000 --- a/project/pk_penalty_system.gd +++ /dev/null @@ -1,158 +0,0 @@ -# pk_penalty_system.gd —— Metin2 40250 恶名红名掉落惩罚与露西之戒保护 1:1 -# 对照 40250 服务端 char_battle.cpp, char.cpp, Item 70043, 70102, 70048 -class_name PkPenaltySystem -extends RefCounted - -signal lucy_ring_protected() -signal item_dropped_on_death(item: Dictionary) -signal alignment_restored(new_alignment: int) - -const MIN_PENALTY_LEVEL := 50 -const ITEM_LUCY_RING := 70043 # 露西之戒 -const ITEM_ZEN_BEAN := 70102 # 禅豆 (+500 善恶值洗恶名) -const ITEM_CLOAK_OF_PEACE := 70048 # 和平披风 (隐匿红名) - -# 负善恶值掉落几率表 (40250 char_battle.cpp 1:1) -static func get_death_drop_chance(alignment: int) -> float: - if alignment >= 0: - return 0.0 - elif alignment >= -3999: # 凶恶 - return 0.05 - elif alignment >= -7999: # 歹毒 - return 0.10 - elif alignment >= -11999: # 恶贯满盈 - return 0.15 - else: # <= -12000 嗜血魔王 - return 0.25 - -# 死亡掉落综合判定 -func process_death_penalty( - player_level: int, - alignment: int, - equipment: Dictionary, - inventory: Array, - forced_roll: float = -1.0 -) -> Dictionary: - # 1. 50 级以下或非红名(>=0) 免除一切死亡掉落 - if player_level < MIN_PENALTY_LEVEL or alignment >= 0: - return { - "dropped": false, - "reason": "SAFE_LEVEL_OR_POSITIVE_KARMA", - "msg": "受新手保护或和平声望庇佑,免于死亡惩罚。" - } - - # 2. 检查是否佩戴露西之戒 (70043) - var has_lucy_ring := false - var ring_slot_key = "" - for eq_key in equipment.keys(): - var eq = equipment[eq_key] - if eq != null and int(eq.get("vnum", 0)) == ITEM_LUCY_RING: - has_lucy_ring = true - ring_slot_key = eq_key - break - - if has_lucy_ring: - # 露西之戒替死碎裂,阻断掉落 - equipment[ring_slot_key] = null - lucy_ring_protected.emit() - return { - "dropped": false, - "ring_shattered": true, - "msg": "【露西之戒】替你承受了死亡的厄运,戒指粉碎瓦解,身上物品完好无损!" - } - - # 3. 计算掉落几率 - var chance := get_death_drop_chance(alignment) - var roll := forced_roll if forced_roll >= 0.0 else randf() - - if roll > chance: - return { - "dropped": false, - "reason": "LUCKY_ROLL", - "msg": "幸运未触发掉落惩罚。" - } - - # 4. 寻找可掉落的物品 (已锁定的物品 is_locked 严禁掉落) - var droppable_candidates: Array = [] - - # 从未锁定装备中搜寻 - for eq_key in equipment.keys(): - var it = equipment[eq_key] - if it != null and not it.get("is_locked", false): - droppable_candidates.append({"type": "equip", "key": eq_key, "item": it}) - - # 从未锁定背包中搜寻 - for i in range(inventory.size()): - var it = inventory[i] - if it != null and not it.get("is_locked", false): - droppable_candidates.append({"type": "inv", "slot": i, "item": it}) - - if droppable_candidates.is_empty(): - return { - "dropped": false, - "reason": "ALL_ITEMS_PROTECTED", - "msg": "身上的所有贵重物品均处于安全加锁保护中,未发生掉落!" - } - - # 掉落一件物品 - var chosen = droppable_candidates[randi() % droppable_candidates.size()] - var dropped_item: Dictionary = chosen["item"].duplicate(true) - - if chosen["type"] == "equip": - equipment[chosen["key"]] = null - else: - inventory[chosen["slot"]] = null - - item_dropped_on_death.emit(dropped_item) - - return { - "dropped": true, - "dropped_item": dropped_item, - "msg": "恶名昭彰!你被击杀,珍贵物品【%s】散落到了地面!" % dropped_item.get("name", "物品") - } - -# 使用禅豆 (70102) 消除 500 点恶名 -func use_zen_bean( - slot_idx: int, - inventory: Array, - current_alignment: int -) -> Dictionary: - if slot_idx < 0 or slot_idx >= inventory.size(): - return {"ok": false, "reason": "INVALID_SLOT"} - - var it = inventory[slot_idx] - if it == null or int(it.get("vnum", 0)) != ITEM_ZEN_BEAN: - return {"ok": false, "reason": "NOT_ZEN_BEAN", "msg": "这不是禅豆!"} - - if current_alignment >= 0: - return { - "ok": false, - "reason": "NOT_EVIL", - "msg": "你的名声本就光明清白,无需服用禅豆洗去恶名。" - } - - # 消耗禅豆 - var cnt: int = int(it.get("count", 1)) - if cnt <= 1: - inventory[slot_idx] = null - else: - it["count"] = cnt - 1 - - # 回复 500 点善恶值 (上限为 0) - var new_alignment: int = mini(0, current_alignment + 500) - alignment_restored.emit(new_alignment) - - return { - "ok": true, - "old_alignment": current_alignment, - "new_alignment": new_alignment, - "restored": 500, - "msg": "服用了禅豆,心灵得到了净化,恶名值减少了 500 点!" - } - -# 判定和平披风 (70048) 是否隐匿红名 -func is_red_name_hidden(equipment: Dictionary) -> bool: - for eq in equipment.values(): - if eq != null and int(eq.get("vnum", 0)) == ITEM_CLOAK_OF_PEACE: - return true - return false diff --git a/project/pk_penalty_system.gd.uid b/project/pk_penalty_system.gd.uid deleted file mode 100644 index 4bb3cd1e..00000000 --- a/project/pk_penalty_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://creetk64lxdrf diff --git a/project/player_trade_exchange_system.gd b/project/player_trade_exchange_system.gd deleted file mode 100644 index bf0d74c2..00000000 --- a/project/player_trade_exchange_system.gd +++ /dev/null @@ -1,343 +0,0 @@ -# player_trade_exchange_system.gd —— 40250 官方玩家面对面安全交易双向锁定系统 1:1 -# 严格对照: -# metin2/src/server/game/src/exchange.h -# metin2/src/server/game/src/exchange.cpp -# metin2/src/server/game/src/char.cpp (OpenExchange, SetExchange, etc.) -extends RefCounted - -const EXCHANGE_ITEM_MAX_NUM := 12 # 官方交易面板 12 格 (4x3 布局) -const EXCHANGE_MAX_DISTANCE := 1000 # 官方交易最大距离 1000 - -# 玩家交易单边状态 -class TradeSide: - var player_id: int - var player_name: String - var is_ready: bool = false - var offered_gold: int = 0 - # 12 个格位: display_pos (0..11) -> item_data Dictionary or null - var display_slots: Array = [] - # 关联背包位置: display_pos -> inv_slot - var inv_positions: Dictionary = {} - - func _init(id: int, name: String) -> void: - player_id = id - player_name = name - is_ready = false - offered_gold = 0 - display_slots.clear() - for i in range(EXCHANGE_ITEM_MAX_NUM): - display_slots.append(null) - inv_positions.clear() - - func count_items() -> int: - var c := 0 - for it in display_slots: - if it != null: - c += 1 - return c - -var side_a: TradeSide = null -var side_b: TradeSide = null -var is_active: bool = false - -# 模拟玩家的背包与金币引用(便于原子化结算) -# player_data = { "id": int, "name": int, "gold": int, "inv": Array, "max_inv_slots": int } -var player_a_ref: Dictionary = {} -var player_b_ref: Dictionary = {} - -func _init() -> void: - side_a = null - side_b = null - is_active = false - -# 开启双向交易会话 (1:1 OpenExchange) -func start_exchange(player_a: Dictionary, player_b: Dictionary, distance: float = 200.0) -> Dictionary: - var res := { "success": false, "error_code": "OK", "message": "" } - - if distance > EXCHANGE_MAX_DISTANCE: - res["error_code"] = "TOO_FAR" - res["message"] = "双方距离太远,无法进行交易。" - return res - - if player_a.get("id") == player_b.get("id"): - res["error_code"] = "CANNOT_TRADE_SELF" - res["message"] = "无法与自己进行交易。" - return res - - # 检查状态 - if player_a.get("is_dead", false) or player_b.get("is_dead", false): - res["error_code"] = "PLAYER_DEAD" - res["message"] = "处于死亡状态的玩家无法交易。" - return res - - if player_a.get("is_in_safebox", false) or player_b.get("is_in_safebox", false) \ - or player_a.get("is_in_shop", false) or player_b.get("is_in_shop", false): - res["error_code"] = "BUSY_WINDOW" - res["message"] = "一方正处于其他交互窗口中,无法交易。" - return res - - player_a_ref = player_a - player_b_ref = player_b - side_a = TradeSide.new(player_a["id"], player_a.get("name", "PlayerA")) - side_b = TradeSide.new(player_b["id"], player_b.get("name", "PlayerB")) - is_active = true - - res["success"] = true - res["message"] = "交易窗口已开启。" - return res - -# 获取玩家所属的交易边 -func _get_side(player_id: int) -> TradeSide: - if side_a != null and side_a.player_id == player_id: - return side_a - elif side_b != null and side_b.player_id == player_id: - return side_b - return null - -# 获取交易对手边 -func _get_other_side(player_id: int) -> TradeSide: - if side_a != null and side_a.player_id == player_id: - return side_b - elif side_b != null and side_b.player_id == player_id: - return side_a - return null - -func _get_player_ref(player_id: int) -> Dictionary: - if player_a_ref.get("id") == player_id: - return player_a_ref - elif player_b_ref.get("id") == player_id: - return player_b_ref - return {} - -# 放入物品到交易栏 (1:1 CExchange::AddItem) -# 核心防调包规则:一旦修改物品,双方锁定状态立即重置为 false! -func add_item(player_id: int, inv_slot: int, display_pos: int) -> Dictionary: - var res := { "success": false, "error_code": "OK", "message": "" } - if not is_active: - res["error_code"] = "TRADE_NOT_ACTIVE" - return res - - var side := _get_side(player_id) - var other := _get_other_side(player_id) - var player_data := _get_player_ref(player_id) - if side == null or other == null or player_data.is_empty(): - res["error_code"] = "INVALID_PARTICIPANT" - return res - - if display_pos < 0 or display_pos >= EXCHANGE_ITEM_MAX_NUM: - res["error_code"] = "INVALID_DISPLAY_POS" - return res - - if side.display_slots[display_pos] != null: - res["error_code"] = "SLOT_ALREADY_OCCUPIED" - return res - - var inv: Array = player_data.get("inv", []) - if inv_slot < 0 or inv_slot >= inv.size() or inv[inv_slot] == null: - res["error_code"] = "ITEM_NOT_FOUND" - return res - - var it: Dictionary = inv[inv_slot] - - # 1:1 防交易检查: ANTI_GIVE 标记 / 灵魂绑定 / 装备中 - if it.get("anti_give", false) or it.get("is_soulbound", false) or it.get("is_equipped", false): - res["error_code"] = "ITEM_UNTRADEABLE" - res["message"] = "该物品受法则限制,不可交易或转移。" - return res - - # 放入交易槽 - side.display_slots[display_pos] = it.duplicate(true) - side.inv_positions[display_pos] = inv_slot - - # 40250 官方核心安全机制: 重置双方锁定状态! - side.is_ready = false - other.is_ready = false - - res["success"] = true - res["message"] = "物品已上架交易栏,双方就绪状态已重置。" - return res - -# 从交易栏取回物品 (1:1 CExchange::RemoveItem) -# 同样重置双方锁定状态! -func remove_item(player_id: int, display_pos: int) -> Dictionary: - var res := { "success": false, "error_code": "OK" } - if not is_active: - res["error_code"] = "TRADE_NOT_ACTIVE" - return res - - var side := _get_side(player_id) - var other := _get_other_side(player_id) - if side == null or other == null: - res["error_code"] = "INVALID_PARTICIPANT" - return res - - if display_pos < 0 or display_pos >= EXCHANGE_ITEM_MAX_NUM or side.display_slots[display_pos] == null: - res["error_code"] = "SLOT_EMPTY" - return res - - side.display_slots[display_pos] = null - side.inv_positions.erase(display_pos) - - # 40250 官方安全机制: 重置双方锁定状态! - side.is_ready = false - other.is_ready = false - - res["success"] = true - return res - -# 设置出价金币 (1:1 CExchange::AddGold) -# 同样重置双方锁定状态! -func set_gold(player_id: int, amount: int) -> Dictionary: - var res := { "success": false, "error_code": "OK", "message": "" } - if not is_active: - res["error_code"] = "TRADE_NOT_ACTIVE" - return res - - var side := _get_side(player_id) - var other := _get_other_side(player_id) - var player_data := _get_player_ref(player_id) - if side == null or other == null or player_data.is_empty(): - res["error_code"] = "INVALID_PARTICIPANT" - return res - - if amount < 0: - res["error_code"] = "INVALID_GOLD" - return res - - if player_data.get("gold", 0) < amount: - res["error_code"] = "INSUFFICIENT_GOLD" - res["message"] = "身上金币不足,无法出价。" - return res - - side.offered_gold = amount - - # 40250 官方安全机制: 重置双方锁定状态! - side.is_ready = false - other.is_ready = false - - res["success"] = true - res["message"] = "出价金额已更新,双方就绪状态已重置。" - return res - -# 点击锁定/同意 (1:1 CExchange::Accept) -# 双方均锁定后自动触发原子化结算 -func set_ready(player_id: int, ready_state: bool) -> Dictionary: - var res := { - "success": false, - "trade_completed": false, - "error_code": "OK", - "message": "" - } - if not is_active: - res["error_code"] = "TRADE_NOT_ACTIVE" - return res - - var side := _get_side(player_id) - var other := _get_other_side(player_id) - if side == null or other == null: - res["error_code"] = "INVALID_PARTICIPANT" - return res - - side.is_ready = ready_state - res["success"] = true - - # 若双方均已 Ready 锁定,执行原子化交付完成交易 (CExchange::Done) - if side_a.is_ready and side_b.is_ready: - var done_res = _execute_atomic_trade() - if done_res["success"]: - res["trade_completed"] = true - res["message"] = "交易成功!双方物品与金币已安全过户。" - else: - res["trade_completed"] = false - res["error_code"] = done_res["error_code"] - res["message"] = done_res["message"] - else: - res["message"] = "已就绪锁定,等待对方确认。" - - return res - -# 背包剩余空位计算 -func _get_free_inv_slots(player_data: Dictionary) -> int: - var inv: Array = player_data.get("inv", []) - var free_count := 0 - for it in inv: - if it == null: - free_count += 1 - return free_count - -# 原子化事务交付执行 (1:1 CExchange::Done & CheckSpace) -func _execute_atomic_trade() -> Dictionary: - var res := { "success": false, "error_code": "OK", "message": "" } - - # 1. 再次校验金币是否充足 - if player_a_ref.get("gold", 0) < side_a.offered_gold: - res["error_code"] = "PLAYER_A_LESS_GOLD" - res["message"] = "一方金币不足,交易终止。" - return res - if player_b_ref.get("gold", 0) < side_b.offered_gold: - res["error_code"] = "PLAYER_B_LESS_GOLD" - res["message"] = "一方金币不足,交易终止。" - return res - - # 2. 空间校验 (CheckSpace): - # 玩家 A 需容纳 B 给予的所有物品;玩家 B 需容纳 A 给予的所有物品 - # 考虑双方移出物品后腾出的背包空位 - var a_items_count := side_a.count_items() - var b_items_count := side_b.count_items() - - var a_free := _get_free_inv_slots(player_a_ref) + a_items_count - var b_free := _get_free_inv_slots(player_b_ref) + b_items_count - - if a_free < b_items_count: - res["error_code"] = "PLAYER_A_INSUFFICIENT_SPACE" - res["message"] = "玩家 A 背包空间不足,无法接收对方物品。" - return res - - if b_free < a_items_count: - res["error_code"] = "PLAYER_B_INSUFFICIENT_SPACE" - res["message"] = "玩家 B 背包空间不足,无法接收对方物品。" - return res - - # 3. 执行金币转移 - player_a_ref["gold"] = player_a_ref.get("gold", 0) - side_a.offered_gold + side_b.offered_gold - player_b_ref["gold"] = player_b_ref.get("gold", 0) - side_b.offered_gold + side_a.offered_gold - - # 4. 执行物品转移 - # 从 A 背包移除 A 出售的物品 - var inv_a: Array = player_a_ref.get("inv", []) - for dpos in side_a.inv_positions: - var inv_slot: int = side_a.inv_positions[dpos] - inv_a[inv_slot] = null - - # 从 B 背包移除 B 出售的物品 - var inv_b: Array = player_b_ref.get("inv", []) - for dpos in side_b.inv_positions: - var inv_slot: int = side_b.inv_positions[dpos] - inv_b[inv_slot] = null - - # 将 A 的物品放入 B 的背包空位 - for it in side_a.display_slots: - if it != null: - for i in range(inv_b.size()): - if inv_b[i] == null: - inv_b[i] = it.duplicate(true) - break - - # 将 B 的物品放入 A 的背包空位 - for it in side_b.display_slots: - if it != null: - for i in range(inv_a.size()): - if inv_a[i] == null: - inv_a[i] = it.duplicate(true) - break - - # 5. 关闭会话 - is_active = false - res["success"] = true - return res - -# 取消交易 (1:1 CExchange::Cancel) -func cancel_trade() -> void: - is_active = false - side_a = null - side_b = null diff --git a/project/player_trade_exchange_system.gd.uid b/project/player_trade_exchange_system.gd.uid deleted file mode 100644 index 030eda99..00000000 --- a/project/player_trade_exchange_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bw64vqxapkwfr diff --git a/project/point_reset_system.gd b/project/point_reset_system.gd deleted file mode 100644 index 3a2d674e..00000000 --- a/project/point_reset_system.gd +++ /dev/null @@ -1,130 +0,0 @@ -# point_reset_system.gd -# 40250 官方 1:1 属性点/技能重置洗点与两阶段确认系统 -# 对照: uipointreset.py, questiondialog2.py, char.cpp, cmd_general.cpp -class_name PointResetSystem -extends RefCounted - -const RESET_COST_YANG: int = 500 - -# 40250 官方四职业初始属性基准值 [CON, INT, STR, DEX] -const START_STAT: Dictionary = { - 0: [4, 3, 6, 3], # WARRIOR (战士) - 1: [3, 3, 4, 6], # ASSASSIN (刺客) - 2: [3, 5, 5, 3], # SURA (修罗) - 3: [4, 6, 3, 3] # SHAMAN (萨满) -} - -# 状态枚举 -enum DialogState { - CLOSED = 0, - INITIAL_PROMPT = 1, - CONFIRMATION = 2 -} - -var state: DialogState = DialogState.CLOSED - -var player_job: int = 0 -var player_gold: int = 0 -var current_stats: Dictionary = {"CON": 4, "INT": 3, "STR": 6, "DEX": 3} -var unallocated_stat_points: int = 0 - -# 技能重置支持 -var player_level: int = 1 -var current_skill_tree: int = 0 # 0=未选, 1=分支A, 2=分支B -var unallocated_skill_points: int = 0 -var active_skills: Dictionary = {} - -var last_packet_sent: String = "" - -signal dialog_opened(cost: int) -signal confirm_dialog_opened(warning_text: String) -signal dialog_closed() -signal reset_completed(new_stats: Dictionary, refund_points: int, remaining_gold: int) -signal reset_failed(reason: String) - -func init_player_data(job: int, level: int, gold: int, stats: Dictionary, stat_pts: int, skill_tree: int, skill_pts: int, skills: Dictionary) -> void: - player_job = job - player_level = level - player_gold = gold - current_stats = stats.duplicate() - unallocated_stat_points = stat_pts - current_skill_tree = skill_tree - unallocated_skill_points = skill_pts - active_skills = skills.duplicate() - -func open_dialog() -> void: - state = DialogState.INITIAL_PROMPT - dialog_opened.emit(RESET_COST_YANG) - -func open_confirm_dialog() -> bool: - if state != DialogState.INITIAL_PROMPT: - return false - - # 检查金币是否满足 500 Yang - if player_gold < RESET_COST_YANG: - reset_failed.emit("NOT_ENOUGH_MONEY") - close_dialog() - return false - - state = DialogState.CONFIRMATION - var warning_msg = "重置后所有属性点将被重置!确定继续吗?" - confirm_dialog_opened.emit(warning_msg) - return true - -func accept_reset() -> bool: - if state != DialogState.CONFIRMATION: - return false - - if player_gold < RESET_COST_YANG: - reset_failed.emit("NOT_ENOUGH_MONEY") - close_dialog() - return false - - # 扣除 500 金币 - player_gold -= RESET_COST_YANG - last_packet_sent = "/pointreset" - - # 回退属性点 - var base_stat = START_STAT.get(player_job, [4, 3, 6, 3]) - var refund_con = maxi(0, current_stats["CON"] - base_stat[0]) - var refund_int = maxi(0, current_stats["INT"] - base_stat[1]) - var refund_str = maxi(0, current_stats["STR"] - base_stat[2]) - var refund_dex = maxi(0, current_stats["DEX"] - base_stat[3]) - var total_refund = refund_con + refund_int + refund_str + refund_dex - - unallocated_stat_points += total_refund - current_stats["CON"] = base_stat[0] - current_stats["INT"] = base_stat[1] - current_stats["STR"] = base_stat[2] - current_stats["DEX"] = base_stat[3] - - reset_completed.emit(current_stats, total_refund, player_gold) - close_dialog() - return true - -# 官方 30 级以下老妇人技能树重洗 (Old Woman Skill Reset) -func reset_skills_at_old_woman() -> bool: - if player_level > 30: - reset_failed.emit("LEVEL_TOO_HIGH") - return false - - # 40250 老妇人洗技能费用公式: level * 1000 + 2000 Yang - var cost = player_level * 1000 + 2000 - if player_gold < cost: - reset_failed.emit("NOT_ENOUGH_MONEY") - return false - - player_gold -= cost - var total_skill_pts = 0 - for sk_id in active_skills: - total_skill_pts += active_skills[sk_id] - - active_skills.clear() - unallocated_skill_points += total_skill_pts - current_skill_tree = 0 # 允许重新转职选择专精分支 - - return true - -func close_dialog() -> void: - state = DialogState.CLOSED - dialog_closed.emit() diff --git a/project/point_reset_system.gd.uid b/project/point_reset_system.gd.uid deleted file mode 100644 index 2f49a1cb..00000000 --- a/project/point_reset_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://df7x3m5rxjd83 diff --git a/project/polymorph_marble_system.gd b/project/polymorph_marble_system.gd deleted file mode 100644 index fabbf7f6..00000000 --- a/project/polymorph_marble_system.gd +++ /dev/null @@ -1,200 +0,0 @@ -# polymorph_marble_system.gd —— Metin2 40250 官方变身宝珠与怪物精魂附体系统 1:1 -# 对照 40250 服务端 polymorph.cpp, polymorph.h, char.cpp, affect.h -class_name PolymorphMarbleSystem -extends RefCounted - -signal polymorph_started(mob_vnum: int, mob_name: String, duration: float, bonus_type: int, bonus_val: int) -signal polymorph_ended(mob_vnum: int, mob_name: String) - -const VNUM_POLYMORPH_MARBLE := 70104 # 变身宝珠 (Polymorph Marble) - -# 40250 变身法术技能境界 (Skill Master Type) -const SKILL_NORMAL := 0 # 普通熟练度 (1~19 级): 10 分钟 (600s) -const SKILL_MASTER := 1 # Master (M1~M10): 15 分钟 (900s) -const SKILL_GRAND_MASTER := 2 # Grand Master (G1~G10): 20 分钟 (1200s) -const SKILL_PERFECT_MASTER := 3 # Perfect Master (P): 25 分钟 (1500s) - -# 40250 polymorph.h 加成属性类型枚举 -const POLYMORPH_NO_BONUS := 0 -const POLYMORPH_ATK_BONUS := 1 # 攻击力增益 (POINT_ATT_BONUS) -const POLYMORPH_DEF_BONUS := 2 # 防御力增益 (POINT_DEF_BONUS) -const POLYMORPH_SPD_BONUS := 3 # 移动速度增益 (POINT_MOV_SPEED) - -# 40250 经典怪物精魂图鉴与加成配置表 -const MONSTER_POLY_TABLE: Dictionary = { - 501: { - "mob_vnum": 501, - "name": "野蛮步兵", - "bonus_type": POLYMORPH_ATK_BONUS, - "bonus_val": 40, - "stat_key": "attack_power" - }, - 502: { - "mob_vnum": 502, - "name": "野蛮弓手", - "bonus_type": POLYMORPH_ATK_BONUS, - "bonus_val": 35, - "stat_key": "attack_power" - }, - 492: { - "mob_vnum": 492, - "name": "白发蜘蛛", - "bonus_type": POLYMORPH_DEF_BONUS, - "bonus_val": 50, - "stat_key": "defense" - }, - 101: { - "mob_vnum": 101, - "name": "野狗", - "bonus_type": POLYMORPH_SPD_BONUS, - "bonus_val": 25, - "stat_key": "move_speed" - }, - 1901: { - "mob_vnum": 1901, - "name": "九尾妖狐", - "bonus_type": POLYMORPH_SPD_BONUS, - "bonus_val": 30, - "stat_key": "move_speed" - } -} - -var is_polymorphed: bool = false -var active_mob_vnum: int = 0 -var active_mob_name: String = "" -var time_remaining: float = 0.0 -var total_duration: float = 0.0 -var active_bonus_type: int = POLYMORPH_NO_BONUS -var active_bonus_val: int = 0 -var active_stat_key: String = "" - -# 计算变身持续时长 (40250 polymorph.cpp:44-59) -static func get_duration_by_mastery(master_type: int) -> float: - match master_type: - SKILL_NORMAL: - return 600.0 # 10 分钟 - SKILL_MASTER: - return 900.0 # 15 分钟 - SKILL_GRAND_MASTER: - return 1200.0 # 20 分钟 - SKILL_PERFECT_MASTER: - return 1500.0 # 25 分钟 - _: - return 600.0 - -# 使用变身宝珠进行怪物附体变身 -func polymorph_character(marble_item: Dictionary, skill_mastery: int, player_stats: Dictionary) -> Dictionary: - var vnum := int(marble_item.get("vnum", 0)) - if vnum != VNUM_POLYMORPH_MARBLE: - return {"ok": false, "reason": "NOT_A_POLYMORPH_MARBLE", "msg": "该物品不是变身宝珠!"} - - var mob_vnum := int(marble_item.get("mob_vnum", 0)) - if mob_vnum == 0: - var sockets: Array = marble_item.get("sockets", []) - if not sockets.is_empty(): - mob_vnum = int(sockets[0]) - - if not MONSTER_POLY_TABLE.has(mob_vnum): - return {"ok": false, "reason": "INVALID_MONSTER_VNUM", "msg": "未知的变身怪物精魂!"} - - # 若当前已处于变身状态,先恢复原形 - if is_polymorphed: - revert_polymorph(player_stats) - - var mob_info: Dictionary = MONSTER_POLY_TABLE[mob_vnum] - var duration: float = get_duration_by_mastery(skill_mastery) - - is_polymorphed = true - active_mob_vnum = mob_vnum - active_mob_name = mob_info["name"] - time_remaining = duration - total_duration = duration - active_bonus_type = int(mob_info["bonus_type"]) - active_bonus_val = int(mob_info["bonus_val"]) - active_stat_key = str(mob_info["stat_key"]) - - # 注入变身属性加成 - if not active_stat_key.is_empty() and active_bonus_val > 0: - player_stats[active_stat_key] = int(player_stats.get(active_stat_key, 0)) + active_bonus_val - - polymorph_started.emit(active_mob_vnum, active_mob_name, duration, active_bonus_type, active_bonus_val) - - return { - "ok": true, - "mob_vnum": active_mob_vnum, - "mob_name": active_mob_name, - "duration": duration, - "bonus_type": active_bonus_type, - "bonus_val": active_bonus_val, - "msg": "怪物精魂附体!成功变身为【%s】,持续 %.0f 秒。" % [active_mob_name, duration] - } - -# 解除变身恢复真身原型 -func revert_polymorph(player_stats: Dictionary) -> Dictionary: - if not is_polymorphed: - return {"ok": false, "reason": "NOT_POLYMORPHED", "msg": "当前未处于变身状态!"} - - # 清除施加的加成属性 - if not active_stat_key.is_empty() and active_bonus_val > 0: - player_stats[active_stat_key] = int(player_stats.get(active_stat_key, 0)) - active_bonus_val - - var old_mob := active_mob_vnum - var old_name := active_mob_name - - is_polymorphed = false - active_mob_vnum = 0 - active_mob_name = "" - time_remaining = 0.0 - total_duration = 0.0 - active_bonus_type = POLYMORPH_NO_BONUS - active_bonus_val = 0 - active_stat_key = "" - - polymorph_ended.emit(old_mob, old_name) - - return { - "ok": true, - "mob_vnum": old_mob, - "mob_name": old_name, - "msg": "变身时效已过,恢复人物原形。" - } - -# 逻辑时钟心跳 -func update(delta: float, player_stats: Dictionary) -> void: - if not is_polymorphed: - return - time_remaining -= delta - if time_remaining <= 0.0: - revert_polymorph(player_stats) - -# 40250 行为限制检查:变身期间禁止乘骑坐骑 -func can_mount_horse() -> bool: - return not is_polymorphed - -# 40250 行为限制检查:变身期间禁止释放原生职业技能 -func can_cast_player_skills() -> bool: - return not is_polymorphed - -# 序列化 -func serialize() -> Dictionary: - return { - "is_polymorphed": is_polymorphed, - "active_mob_vnum": active_mob_vnum, - "active_mob_name": active_mob_name, - "time_remaining": time_remaining, - "total_duration": total_duration, - "active_bonus_type": active_bonus_type, - "active_bonus_val": active_bonus_val, - "active_stat_key": active_stat_key - } - -# 反序列化 -func deserialize(data: Dictionary) -> void: - is_polymorphed = bool(data.get("is_polymorphed", false)) - active_mob_vnum = int(data.get("active_mob_vnum", 0)) - active_mob_name = str(data.get("active_mob_name", "")) - time_remaining = float(data.get("time_remaining", 0.0)) - total_duration = float(data.get("total_duration", 0.0)) - active_bonus_type = int(data.get("active_bonus_type", POLYMORPH_NO_BONUS)) - active_bonus_val = int(data.get("active_bonus_val", 0)) - active_stat_key = str(data.get("active_stat_key", "")) diff --git a/project/polymorph_marble_system.gd.uid b/project/polymorph_marble_system.gd.uid deleted file mode 100644 index 049d4c00..00000000 --- a/project/polymorph_marble_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dvslksqfmwfbv diff --git a/project/polymorph_system.gd b/project/polymorph_system.gd deleted file mode 100644 index 68b558ce..00000000 --- a/project/polymorph_system.gd +++ /dev/null @@ -1,149 +0,0 @@ -# polymorph_system.gd —— Metin2 40250 怪物变身球与变身术技能系统 1:1 -# 对照 40250 服务端 polymorph.cpp, char_item.cpp (Item 70104, Skill 129 Polymorph) -class_name PolymorphSystem -extends RefCounted - -signal polymorph_started(mob_vnum: int, duration: float) -signal polymorph_ended() - -const ITEM_POLYMORPH_MARBLE := 70104 -const SKILL_POLYMORPH := 129 -const AFFECT_POLYMORPH := 220 - -# 经典变身怪物属性表 (mob_vnum -> 属性加成与基础设定) -const MOB_DATA: Dictionary = { - 101: {"name": "野猪", "atk": 45, "def": 20, "speed": 120}, - 102: {"name": "野狼", "atk": 35, "def": 15, "speed": 135}, - 110: {"name": "黑熊", "atk": 60, "def": 35, "speed": 110}, - 114: {"name": "白虎", "atk": 85, "def": 40, "speed": 140}, - 501: {"name": "流浪汉", "atk": 70, "def": 30, "speed": 125} -} - -var is_polymorphed: bool = false -var current_mob_vnum: int = 0 -var remaining_time: float = 0.0 -var original_race: int = 0 # 0: 战男, 1: 忍女, 2: 修男, 3: 萨女等 -var poly_skill_level: int = 1 # 1 ~ 40 (1~19 普通, 20~29 M1~M10, 30~39 G1~G10, 40 P) - -# 变身属性收益缓存 -var bonus_atk: int = 0 -var bonus_def: int = 0 -var bonus_speed: int = 0 - -# 计算变身持续时间 (秒) -static func get_duration_by_skill(level: int) -> float: - if level >= 40: # P 级 - return 2100.0 # 35 分钟 - elif level >= 30: # G 级 - return 1500.0 # 25 分钟 - elif level >= 20: # M 级 - return 1200.0 # 20 分钟 - else: - return 600.0 + (level * 20.0) # 10~16 分钟 - -func can_polymorph(player_dead: bool, player_mounted: bool) -> Dictionary: - if player_dead: - return {"ok": false, "reason": "PLAYER_DEAD", "msg": "死亡状态下无法变身!"} - if player_mounted: - return {"ok": false, "reason": "PLAYER_MOUNTED", "msg": "骑马状态下无法变身,请先下马!"} - return {"ok": true} - -# 使用变身球 (Item 70104) 变身 -func use_polymorph_marble(item: Dictionary, player_dead: bool, player_mounted: bool, p_race: int = 0) -> Dictionary: - var check = can_polymorph(player_dead, player_mounted) - if not check["ok"]: - return check - - var vnum: int = int(item.get("vnum", 0)) - if vnum != ITEM_POLYMORPH_MARBLE: - return {"ok": false, "reason": "NOT_A_MARBLE", "msg": "这不是变身球!"} - - var mob_vnum: int = int(item.get("mob_vnum", 0)) - if mob_vnum == 0: - # 从 socket[0] 读 - var sockets: Array = item.get("sockets", []) - if sockets.size() > 0: - mob_vnum = int(sockets[0]) - - if mob_vnum == 0 or not MOB_DATA.has(mob_vnum): - # 默认野猪 101 - mob_vnum = 101 - - original_race = p_race - current_mob_vnum = mob_vnum - is_polymorphed = true - remaining_time = get_duration_by_skill(poly_skill_level) - - # 结算属性加成 (char_item.cpp) - var mdata: Dictionary = MOB_DATA[mob_vnum] - var skill_multiplier: float = 1.0 + (float(poly_skill_level) * 0.025) - bonus_atk = int(round(float(mdata["atk"]) * skill_multiplier)) - bonus_def = int(mdata["def"]) - bonus_speed = int(mdata["speed"]) - - polymorph_started.emit(current_mob_vnum, remaining_time) - - return { - "ok": true, - "mob_vnum": current_mob_vnum, - "mob_name": mdata["name"], - "duration": remaining_time, - "bonus_atk": bonus_atk, - "bonus_def": bonus_def, - "bonus_speed": bonus_speed - } - -# 变身状态限制检查 1:1 -func can_use_skill(skill_id: int) -> bool: - # 变身中禁止施放人物主动职业技能 - if is_polymorphed: - # 变身术本身、被动或马术/公共技能除外,职业技能一律拦截 - if skill_id != SKILL_POLYMORPH and skill_id < 100: - return false - return true - -func can_change_weapon() -> bool: - # 变身中禁止切换武器 (equip_rules.gd:316) - return not is_polymorphed - -func can_mount() -> bool: - # 变身中禁止骑马 - return not is_polymorphed - -# 逐帧更新倒计时 -func update(dt: float) -> void: - if not is_polymorphed: - return - - remaining_time -= dt - if remaining_time <= 0.0: - cancel_polymorph() - -# 取消/解除变身 -func cancel_polymorph() -> void: - if not is_polymorphed: - return - - is_polymorphed = false - current_mob_vnum = 0 - remaining_time = 0.0 - bonus_atk = 0 - bonus_def = 0 - bonus_speed = 0 - - polymorph_ended.emit() - -func on_player_death() -> void: - if is_polymorphed: - cancel_polymorph() - -func get_state() -> Dictionary: - return { - "is_polymorphed": is_polymorphed, - "mob_vnum": current_mob_vnum, - "remaining_time": remaining_time, - "poly_skill_level": poly_skill_level, - "bonus_atk": bonus_atk, - "bonus_def": bonus_def, - "bonus_speed": bonus_speed - } diff --git a/project/polymorph_system.gd.uid b/project/polymorph_system.gd.uid deleted file mode 100644 index c4bf709a..00000000 --- a/project/polymorph_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://veallj1tyi8c diff --git a/project/portable_service_system.gd b/project/portable_service_system.gd deleted file mode 100644 index 9fcec8be..00000000 --- a/project/portable_service_system.gd +++ /dev/null @@ -1,145 +0,0 @@ -# portable_service_system.gd —— Metin2 40250 随身便民道具与即时服务系统 1:1 -# 对照 40250 服务端 char_item.cpp, cmd_general.cpp, Items 70005, 70006, 70007 -class_name PortableServiceSystem -extends RefCounted - -signal service_opened(service_type: String) -signal portable_item_consumed(vnum: int, remaining: int) - -const ITEM_PORTABLE_SAFEBOX := 70005 # 随身仓库卷轴 -const ITEM_PORTABLE_BLACKSMITH := 70006 # 随身铁匠手稿 -const ITEM_PORTABLE_GENERAL_STORE := 70007 # 随身杂货铺契约 - -# 随身杂货铺基础货架商品列表 (大红、大蓝、加速药水、回城卷轴) -const PORTABLE_STORE_GOODS: Array = [ - {"vnum": 27003, "name": "红药水(大)", "price": 3200, "count": 200}, - {"vnum": 27006, "name": "蓝药水(大)", "price": 4800, "count": 200}, - {"vnum": 27102, "name": "绿药水(大)", "price": 10000, "count": 10}, - {"vnum": 22000, "name": "回城卷轴", "price": 2000, "count": 1} -] - -var active_service: String = "" # "safebox", "blacksmith", "store", or "" - -# 使用随身仓库 (70005) -func use_portable_safebox(slot_idx: int, inventory: Array) -> Dictionary: - var consume_res := _consume_portable_item(slot_idx, inventory, ITEM_PORTABLE_SAFEBOX) - if not consume_res["ok"]: - return consume_res - - active_service = "safebox" - service_opened.emit("safebox") - - return { - "ok": true, - "service": "safebox", - "remaining_scrolls": consume_res["remaining"], - "msg": "使用了【随身仓库卷轴】,已建立与城市货郎的远程仓库连接!" - } - -# 使用随身铁匠 (70006) -func use_portable_blacksmith(slot_idx: int, inventory: Array) -> Dictionary: - var consume_res := _consume_portable_item(slot_idx, inventory, ITEM_PORTABLE_BLACKSMITH) - if not consume_res["ok"]: - return consume_res - - active_service = "blacksmith" - service_opened.emit("blacksmith") - - return { - "ok": true, - "service": "blacksmith", - "remaining_scrolls": consume_res["remaining"], - "msg": "展开了【随身铁匠手稿】,便携精炼锻造炉已点燃!随时可强化装备!" - } - -# 使用随身杂货铺 (70007) -func use_portable_store(slot_idx: int, inventory: Array) -> Dictionary: - var consume_res := _consume_portable_item(slot_idx, inventory, ITEM_PORTABLE_GENERAL_STORE) - if not consume_res["ok"]: - return consume_res - - active_service = "store" - service_opened.emit("store") - - return { - "ok": true, - "service": "store", - "goods": PORTABLE_STORE_GOODS.duplicate(true), - "remaining_scrolls": consume_res["remaining"], - "msg": "出示了【随身杂货铺契约】,随身货郎已赶来!随时可补给药水或出售垃圾!" - } - -# 从随身杂货铺购买商品 -func buy_goods_from_portable_store( - goods_idx: int, - player_data: Dictionary, - inventory: Array -) -> Dictionary: - if active_service != "store": - return {"ok": false, "reason": "STORE_NOT_OPEN", "msg": "随身杂货铺尚未开启!"} - - if goods_idx < 0 or goods_idx >= PORTABLE_STORE_GOODS.size(): - return {"ok": false, "reason": "INVALID_GOODS", "msg": "商品不存在!"} - - var goods: Dictionary = PORTABLE_STORE_GOODS[goods_idx] - var price: int = int(goods["price"]) - var current_gold: int = int(player_data.get("gold", 0)) - - if current_gold < price: - return { - "ok": false, - "reason": "INSUFFICIENT_YANG", - "price": price, - "current_gold": current_gold, - "msg": "金币不足,无法购买!" - } - - # 寻找背包空位 - var empty_slot := -1 - for i in range(inventory.size()): - if inventory[i] == null: - empty_slot = i - break - - if empty_slot == -1: - return {"ok": false, "reason": "INVENTORY_FULL", "msg": "背包已满,无法容纳所购商品!"} - - # 扣费并放入物品 - player_data["gold"] = current_gold - price - inventory[empty_slot] = { - "vnum": goods["vnum"], - "name": goods["name"], - "count": goods.get("count", 1) - } - - return { - "ok": true, - "item": inventory[empty_slot], - "slot": empty_slot, - "remaining_gold": player_data["gold"], - "msg": "成功购买【%s】!" % goods["name"] - } - -# 关闭随身服务窗口 -func close_service() -> void: - active_service = "" - -# 内部通用道具消耗处理 -func _consume_portable_item(slot_idx: int, inventory: Array, expected_vnum: int) -> Dictionary: - if slot_idx < 0 or slot_idx >= inventory.size(): - return {"ok": false, "reason": "INVALID_SLOT"} - - var item = inventory[slot_idx] - if item == null or int(item.get("vnum", 0)) != expected_vnum: - return {"ok": false, "reason": "ITEM_MISMATCH", "msg": "该格子上不是对应的随身卷轴!"} - - var cnt: int = int(item.get("count", 1)) - var remaining: int = cnt - 1 - if remaining <= 0: - inventory[slot_idx] = null - else: - item["count"] = remaining - - portable_item_consumed.emit(expected_vnum, remaining) - - return {"ok": true, "remaining": remaining} diff --git a/project/portable_service_system.gd.uid b/project/portable_service_system.gd.uid deleted file mode 100644 index 5b8ad822..00000000 --- a/project/portable_service_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cwimad6y024ls diff --git a/project/private_shop_system.gd b/project/private_shop_system.gd deleted file mode 100644 index ee15eda2..00000000 --- a/project/private_shop_system.gd +++ /dev/null @@ -1,88 +0,0 @@ -# private_shop_system.gd —— 个人私有商店摆摊系统(40250 官方 1:1 对齐) -# 对齐源码: -# - 40250 server/game/src/char_item.cpp (ITEM_BUNDLE 50200, OpenMyShop) -# - 40250 root/uiprivateshopbuilder.py (PrivateShopBuilder) -# - 40250 uiscript/privateshopbuilder.py (40 格摊位格) -class_name PrivateShopSystem -extends RefCounted - -const VNUM_ITEM_BUNDLE := 50200 # 束发包袱皮 -const VNUM_SHOP_HOST_STALL := 30000 # 摆摊木箱/桌子外观模型 Vnum -const PRIVATE_SHOP_SLOTS_MAX := 39 # 官方上限 39 件商品 -const SIGN_LENGTH_MAX := 25 # 摊位招牌字数上限 - -## 检查物品是否为包袱皮 -static func is_bundle(vnum: int) -> bool: - return vnum == VNUM_ITEM_BUNDLE - -## 校验是否能开店 (40250 官方前置条件:非战斗、非骑乘、非死亡) -static func can_open_private_shop(is_fighting: bool, is_mounted: bool, is_dead: bool) -> Dictionary: - if is_dead: - return {"ok": false, "code": "CANNOT_DEAD", "msg": "死亡状态下无法摆摊!"} - if is_mounted: - return {"ok": false, "code": "CANNOT_MOUNTED", "msg": "骑乘状态下无法摆摊,请先下马!"} - if is_fighting: - return {"ok": false, "code": "CANNOT_FIGHTING", "msg": "战斗中无法摆摊!"} - return {"ok": true} - -## 校验摊位招牌名称 -static func validate_sign(sign_text: String) -> Dictionary: - var trimmed := sign_text.strip_edges() - if trimmed.is_empty(): - return {"ok": false, "code": "SIGN_EMPTY", "msg": "请输入有效的摊位名称!"} - if trimmed.length() > SIGN_LENGTH_MAX: - return {"ok": false, "code": "SIGN_TOO_LONG", "msg": "摊位名称不能超过 25 个字符!"} - return {"ok": true, "sign": trimmed} - -## 验证并将物品上架到摊位 -static func add_item_to_stock(stock_dict: Dictionary, slot_idx: int, inv_item: Dictionary, price: int) -> Dictionary: - if slot_idx < 0 or slot_idx >= PRIVATE_SHOP_SLOTS_MAX: - return {"ok": false, "code": "INVALID_SLOT", "msg": "无效的摊位格子!"} - if price <= 0: - return {"ok": false, "code": "INVALID_PRICE", "msg": "出售价格必须大于 0 金币!"} - if stock_dict.size() >= PRIVATE_SHOP_SLOTS_MAX and not stock_dict.has(slot_idx): - return {"ok": false, "code": "SHOP_FULL", "msg": "摊位已达到最大商品容量 (39 件)!"} - - var cell := int(inv_item.get("cell", -1)) - var vnum := int(inv_item.get("vnum", 0)) - var count := int(inv_item.get("count", 1)) - - stock_dict[slot_idx] = { - "slot": slot_idx, - "cell": cell, - "vnum": vnum, - "count": count, - "price": price, - } - return { - "ok": true, - "code": "SUCCESS", - "slot": slot_idx, - "price": price, - "msg": "商品成功上架到摊位第 %d 格,单价 %d 金币。" % [slot_idx + 1, price] - } - -## 撤下摊位商品 -static func remove_item_from_stock(stock_dict: Dictionary, slot_idx: int) -> bool: - if stock_dict.has(slot_idx): - stock_dict.erase(slot_idx) - return true - return false - -## 开市创建摆摊实体 -static func build_private_shop(sign_text: String, stock_dict: Dictionary, stall_pos: Vector3) -> Dictionary: - var v_sign := validate_sign(sign_text) - if not bool(v_sign.get("ok", false)): - return v_sign - if stock_dict.is_empty(): - return {"ok": false, "code": "NO_ITEMS", "msg": "摊位上没有任何商品,无法开市!"} - - return { - "ok": true, - "code": "SHOP_OPENED", - "stall_vnum": VNUM_SHOP_HOST_STALL, - "sign": v_sign.get("sign", ""), - "items_count": stock_dict.size(), - "position": stall_pos, - "msg": "【%s】正式开市!摊位已在当前位置建立。" % v_sign.get("sign", "") - } diff --git a/project/private_shop_system.gd.uid b/project/private_shop_system.gd.uid deleted file mode 100644 index 1bd88cf8..00000000 --- a/project/private_shop_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cgo6r1fqm0h40 diff --git a/project/privilege_empire_guild_system.gd b/project/privilege_empire_guild_system.gd deleted file mode 100644 index 2c016b4d..00000000 --- a/project/privilege_empire_guild_system.gd +++ /dev/null @@ -1,179 +0,0 @@ -# privilege_empire_guild_system.gd —— Metin2 40250 官方帝国与公会恩泽特权倍率系统 1:1 -# 100% 对照 40250 服务端 priv_manager.cpp, priv_manager.h, constants.cpp (c_apszPrivNames) -class_name PrivilegeEmpireGuildSystem -extends RefCounted - -signal empire_privilege_granted(empire: int, priv_type: int, value: int, duration: float) -signal guild_privilege_granted(guild_id: int, priv_type: int, value: int, duration: float) -signal character_privilege_granted(player_id: int, priv_type: int, value: int) -signal privilege_expired(level_type: String, target_id: int, priv_type: int) - -# 40250 官方特权类别常数 -const PRIV_ITEM_DROP := 1 # 物品掉落率加成 (아이템 드롭 확률) -const PRIV_GOLD_DROP := 2 # 金币掉落率加成 (돈 드롭 확률) -const PRIV_GOLD10_DROP := 3 # 暴发大金币堆掉率加成 (돈 폭풍 드롭 확률) -const PRIV_EXP := 4 # 杀怪经验值倍率加成 (경험치 배율) - -const PRIV_NAMES: Dictionary = { - PRIV_ITEM_DROP: "物品爆率加成", - PRIV_GOLD_DROP: "金币掉落加成", - PRIV_GOLD10_DROP: "黄金风暴大金币爆率", - PRIV_EXP: "经验值倍率加成" -} - -# 帝国定义 (0 为全服所有帝国) -const EMPIRE_ALL := 0 -const EMPIRE_SHINSOO := 1 -const EMPIRE_CHUNJO := 2 -const EMPIRE_JINNO := 3 - -# 数据结构: -# _empire_privs[type][empire] -> {"value": int, "time_remaining": float} -var _empire_privs: Dictionary = {} -# _guild_privs[type][guild_id] -> {"value": int, "time_remaining": float} -var _guild_privs: Dictionary = {} -# _char_privs[type][player_id] -> int -var _char_privs: Dictionary = {} - -func _init() -> void: - for t in [PRIV_ITEM_DROP, PRIV_GOLD_DROP, PRIV_GOLD10_DROP, PRIV_EXP]: - _empire_privs[t] = {} - _guild_privs[t] = {} - _char_privs[t] = {} - for emp in [0, 1, 2, 3]: - _empire_privs[t][emp] = {"value": 0, "time_remaining": 0.0} - -# ========================================== -# 1. 设置特权 (GiveEmpirePriv, GiveGuildPriv, GiveCharacterPriv) -# ========================================== - -# 设置帝国全域特权 (empire: 0 为全服, 1~3 为指定帝国) -func set_empire_privilege(empire: int, priv_type: int, value_percent: int, duration_sec: float) -> void: - if not _is_valid_priv_type(priv_type) or empire < 0 or empire > 3: - return - - var val = clamp(value_percent, 0, 1000) - var dur = max(0.0, duration_sec) - _empire_privs[priv_type][empire] = {"value": val, "time_remaining": dur} - empire_privilege_granted.emit(empire, priv_type, val, dur) - -# 设置公会专属特权 -func set_guild_privilege(guild_id: int, priv_type: int, value_percent: int, duration_sec: float) -> void: - if not _is_valid_priv_type(priv_type) or guild_id <= 0: - return - - var val = clamp(value_percent, 0, 1000) - var dur = max(0.0, duration_sec) - _guild_privs[priv_type][guild_id] = {"value": val, "time_remaining": dur} - guild_privilege_granted.emit(guild_id, priv_type, val, dur) - -# 设置个人永久/专属特权 -func set_character_privilege(player_id: int, priv_type: int, value_percent: int) -> void: - if not _is_valid_priv_type(priv_type) or player_id <= 0: - return - _char_privs[priv_type][player_id] = value_percent - character_privilege_granted.emit(player_id, priv_type, value_percent) - -# ========================================== -# 2. 移除特权 (RemoveEmpirePriv, RemoveGuildPriv, RemoveCharacterPriv) -# ========================================== -func remove_empire_privilege(empire: int, priv_type: int) -> void: - if _empire_privs.has(priv_type) and _empire_privs[priv_type].has(empire): - _empire_privs[priv_type][empire] = {"value": 0, "time_remaining": 0.0} - -func remove_guild_privilege(guild_id: int, priv_type: int) -> void: - if _guild_privs.has(priv_type): - _guild_privs[priv_type].erase(guild_id) - -func remove_character_privilege(player_id: int, priv_type: int) -> void: - if _char_privs.has(priv_type): - _char_privs[priv_type].erase(player_id) - -# ========================================== -# 3. 特权倍率综合计算 (priv_manager.cpp: GetPriv) -# ========================================== -func get_effective_privilege( - player_id: int, - empire: int, - guild_id: int, - priv_type: int, - has_exp_ring: bool = false, - has_thief_glove: bool = false -) -> int: - if not _is_valid_priv_type(priv_type): - return 0 - - # 40250 官方逻辑: - # 个人特权 - var val_ch: int = _char_privs[priv_type].get(player_id, 0) - - # 全服全境特权 (Empire 0) - var val_emp_all = 0 - if _empire_privs[priv_type].has(0) and _empire_privs[priv_type][0]["time_remaining"] > 0.0: - val_emp_all = int(_empire_privs[priv_type][0]["value"]) - - # 专属帝国特权 - var val_emp = 0 - if _empire_privs[priv_type].has(empire) and _empire_privs[priv_type][empire]["time_remaining"] > 0.0: - val_emp = int(_empire_privs[priv_type][empire]["value"]) - - # 公会特权 - var val_guild = 0 - if guild_id > 0 and _guild_privs[priv_type].has(guild_id): - if _guild_privs[priv_type][guild_id]["time_remaining"] > 0.0: - val_guild = int(_guild_privs[priv_type][guild_id]["value"]) - - # 40250 官方取四者最大加成值 (MAX) - var max_bonus = max(val_ch, max(val_emp_all, max(val_emp, val_guild))) - - # 装备道具额外叠加: 经验指环 (70024: 经验 +50%), 小偷手套 (70003: 爆率 +100%) - if priv_type == PRIV_EXP and has_exp_ring: - max_bonus += 50 - elif priv_type == PRIV_ITEM_DROP and has_thief_glove: - max_bonus += 100 - - return max_bonus - -# ========================================== -# 4. 心跳更新与倒计时自然失效 -# ========================================== -func update(delta: float) -> void: - for t in [PRIV_ITEM_DROP, PRIV_GOLD_DROP, PRIV_GOLD10_DROP, PRIV_EXP]: - # 帝国特权 - for emp in [0, 1, 2, 3]: - var ep = _empire_privs[t][emp] - if ep["time_remaining"] > 0.0: - ep["time_remaining"] -= delta - if ep["time_remaining"] <= 0.0: - ep["time_remaining"] = 0.0 - ep["value"] = 0 - privilege_expired.emit("empire", emp, t) - - # 公会特权 - var to_erase_guilds: Array[int] = [] - for gid in _guild_privs[t].keys(): - var gp = _guild_privs[t][gid] - if gp["time_remaining"] > 0.0: - gp["time_remaining"] -= delta - if gp["time_remaining"] <= 0.0: - to_erase_guilds.append(gid) - - for gid in to_erase_guilds: - _guild_privs[t].erase(gid) - privilege_expired.emit("guild", gid, t) - -func _is_valid_priv_type(t: int) -> bool: - return t >= PRIV_ITEM_DROP and t <= PRIV_EXP - -# 序列化与反序列化 -func serialize() -> Dictionary: - return { - "empire_privs": _empire_privs.duplicate(true), - "guild_privs": _guild_privs.duplicate(true), - "char_privs": _char_privs.duplicate(true) - } - -func deserialize(data: Dictionary) -> void: - _empire_privs = data.get("empire_privs", {}).duplicate(true) - _guild_privs = data.get("guild_privs", {}).duplicate(true) - _char_privs = data.get("char_privs", {}).duplicate(true) diff --git a/project/privilege_empire_guild_system.gd.uid b/project/privilege_empire_guild_system.gd.uid deleted file mode 100644 index d46b4144..00000000 --- a/project/privilege_empire_guild_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://r7ryp7cppxkg diff --git a/project/refine_effect_system.gd b/project/refine_effect_system.gd deleted file mode 100644 index ea05074d..00000000 --- a/project/refine_effect_system.gd +++ /dev/null @@ -1,115 +0,0 @@ -# refine_effect_system.gd —— +7/+8/+9 装备强化专属发光特效(40250 官方 1:1 对齐) -# 对齐源码: -# - 40250 ClientVS22/source/UserInterface/InstanceBase.cpp:2698-2740 (AttachEffect, EFFECT_REFINED) -# - 40250 ClientVS22/source/UserInterface/InstanceBase.h:268-297 (EFFECT_REFINED_NUM) -# - 40250 root/playersettingmodule.py:391-416 (RegisterEffect, sword_7..9.mse, armor_7..9.mse) -class_name RefineEffectSystem -extends RefCounted - -# 40250 官方发光分级常量 -const REFINE_SHINE_START_LEVEL := 7 # 从 +7 开始出现发光特效 - -const SHINE_TIER_7 := 7 -const SHINE_TIER_8 := 8 -const SHINE_TIER_9 := 9 - -# 官方发光特效配置文件映射表 (40250 playersettingmodule.py:391-416) -const WEAPON_SHINE_EFFECTS := { - SHINE_TIER_7: "d:/ymir work/pc/common/effect/sword/sword_7.mse", - SHINE_TIER_8: "d:/ymir work/pc/common/effect/sword/sword_8.mse", - SHINE_TIER_9: "d:/ymir work/pc/common/effect/sword/sword_9.mse", -} - -const ARMOR_SHINE_EFFECTS := { - SHINE_TIER_7: "d:/ymir work/pc/common/effect/armor/armor_7.mse", - SHINE_TIER_8: "d:/ymir work/pc/common/effect/armor/armor_8.mse", - SHINE_TIER_9: "d:/ymir work/pc/common/effect/armor/armor_9.mse", -} - -# 66~70 级高级黑钢战甲特效 (armor-4-2-1.mse, armor-4-2-2.mse) -const SPECIAL_ARMOR_EFFECTS := { - SHINE_TIER_7: "d:/ymir work/pc/common/effect/armor/armor-4-2-1.mse", - SHINE_TIER_8: "d:/ymir work/pc/common/effect/armor/armor-4-2-2.mse", - SHINE_TIER_9: "d:/ymir work/pc/common/effect/armor/armor-4-2.mse", -} - -# 骨骼挂载点定义 (InstanceBase.cpp:2698) -const ATTACH_BONE_WEAPON_RIGHT := "PART_WEAPON" # 右手武器 -const ATTACH_BONE_WEAPON_LEFT := "PART_WEAPON_LEFT" # 左手武器(如双持匕首) -const ATTACH_BONE_ARMOR := "Bip01" # 盔甲躯干 - -# 发光色调配置(用于 Godot 动态粒子与材质 Emission) -const SHINE_COLORS := { - SHINE_TIER_7: Color(0.9, 0.95, 1.0, 0.7), # +7: 浅白清冷光晕 - SHINE_TIER_8: Color(0.3, 0.7, 1.0, 0.85), # +8: 亮蓝闪烁流光 - SHINE_TIER_9: Color(1.0, 0.85, 0.2, 1.0), # +9: 强烈金黄/电光辉光 -} - -## 从物品 Vnum 解析强化等级 (Metin2 官方惯例:vnum % 10 为强化等级) -static func get_refine_level_from_vnum(vnum: int) -> int: - if vnum <= 0: - return 0 - return vnum % 10 - -## 检查该物品是否具备 +7/+8/+9 专属发光 -static func is_shining_item(vnum: int, item_type: int) -> bool: - if item_type not in [1, 2]: # 1: WEAPON, 2: ARMOR - return false - var lvl := get_refine_level_from_vnum(vnum) - return lvl >= REFINE_SHINE_START_LEVEL - -## 获取发光特效数据配置 -static func get_shine_spec(vnum: int, item_type: int, is_left_hand := false) -> Dictionary: - var lvl := get_refine_level_from_vnum(vnum) - if lvl < REFINE_SHINE_START_LEVEL: - return {"shining": false, "level": lvl} - - var attach_bone := "" - var effect_path := "" - var color: Color = SHINE_COLORS.get(lvl, Color.WHITE) - - if item_type == 1: # WEAPON - attach_bone = ATTACH_BONE_WEAPON_LEFT if is_left_hand else ATTACH_BONE_WEAPON_RIGHT - effect_path = WEAPON_SHINE_EFFECTS.get(lvl, "") - elif item_type == 2: # ARMOR - attach_bone = ATTACH_BONE_ARMOR - # 判断是否为 66 级以上黑钢特种战甲 (如 11290..11299) - if vnum >= 11290 and vnum <= 11899: - effect_path = SPECIAL_ARMOR_EFFECTS.get(lvl, "") - else: - effect_path = ARMOR_SHINE_EFFECTS.get(lvl, "") - - return { - "shining": true, - "level": lvl, - "attach_bone": attach_bone, - "effect_path": effect_path, - "color": color, - "particle_rate": 15 if lvl == 7 else (30 if lvl == 8 else 60), - "light_energy": 0.8 if lvl == 7 else (1.5 if lvl == 8 else 3.0), - } - -## 创建发光粒子节点(可在挂载模型时直接附加到 Skeleton3D) -static func create_shine_particles(spec: Dictionary) -> GPUParticles3D: - if not bool(spec.get("shining", false)): - return null - var particles := GPUParticles3D.new() - particles.name = "RefineShineParticles_L%d" % int(spec.get("level", 7)) - particles.amount = int(spec.get("particle_rate", 20)) - particles.lifetime = 0.6 - particles.explosiveness = 0.05 - particles.randomness = 0.2 - - # 创建材质 - var mat := StandardMaterial3D.new() - mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED - mat.albedo_color = spec.get("color", Color.WHITE) - mat.emission_enabled = true - mat.emission = spec.get("color", Color.WHITE) - mat.emission_energy_multiplier = float(spec.get("light_energy", 1.0)) - - var quad := QuadMesh.new() - quad.size = Vector2(0.12, 0.12) - quad.material = mat - particles.draw_pass_1 = quad - return particles diff --git a/project/refine_effect_system.gd.uid b/project/refine_effect_system.gd.uid deleted file mode 100644 index 1523ad6b..00000000 --- a/project/refine_effect_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://britmtog0aw6l diff --git a/project/safebox_storage_keeper_system.gd b/project/safebox_storage_keeper_system.gd deleted file mode 100644 index c1f3ec30..00000000 --- a/project/safebox_storage_keeper_system.gd +++ /dev/null @@ -1,243 +0,0 @@ -# safebox_storage_keeper_system.gd —— 40250 官方仓库保管员金条熔铸与密码保险箱系统 1:1 -# 严格对照: -# metin2/src/server/game/src/safebox.h, safebox.cpp -# metin2/src/server/game/src/char.cpp:5569-5605 (ReqSafeboxLoad) -# metin2/src/server/game/src/input_db.cpp:1225-1250 (SafeboxWrongPassword / SafeboxChangePasswordAnswer) -# metin2/src/server/db/src/ClientManager.cpp:515-535 (RESULT_SAFEBOX_LOAD: default "000000") -# metin2/src/server/share/conf/item_proto.txt (80003~80007 金银条) -extends RefCounted - -const SAFEBOX_MAX_NUM := 45 # 官方单页保险箱格数 (45 格) -const SAFEBOX_PASSWORD_MAX_LEN := 6 # 密码最长 6 位 -const DEFAULT_PASSWORD := "000000" # 官方默认初始密码 -const SAFEBOX_OPEN_DISTANCE := 1000 # 距离 NPC 9005 最大距离 1000 -const SAFEBOX_COOLDOWN_SEC := 10 # 开箱冷却 10 秒 (PASSES_PER_SEC(10)) - -# 金银条规格表 (Vnum -> { "name": String, "face_value": int, "smelt_cost": int }) -# 官方 1% 熔铸税 (cost = face_value * 1.01) -const GOLD_BAR_SPECS := { - 80003: { "name": "银条 (5万)", "face_value": 50000, "smelt_cost": 50500 }, - 80004: { "name": "银条 (10万)", "face_value": 100000, "smelt_cost": 101000 }, - 80005: { "name": "金条 (50万)", "face_value": 500000, "smelt_cost": 505000 }, - 80006: { "name": "金条 (100万)", "face_value": 1000000, "smelt_cost": 1010000 }, - 80007: { "name": "金条 (200万)", "face_value": 2000000, "smelt_cost": 2020000 }, -} - -var password: String = DEFAULT_PASSWORD -var is_open: bool = false -var last_open_time: int = -100 -var current_time: int = 1000 - -# 保险箱 45 格网格 (slot 0..44 -> Dictionary or null) -var safebox_grid: Array = [] - -func _init() -> void: - password = DEFAULT_PASSWORD - is_open = false - last_open_time = -100 - current_time = 1000 - safebox_grid.clear() - for i in range(SAFEBOX_MAX_NUM): - safebox_grid.append(null) - -# 1. 密码校验并打开保险箱 (1:1 CHARACTER::ReqSafeboxLoad) -func open_safebox(entered_pw: String, npc_distance: float = 200.0) -> Dictionary: - var res := { - "success": false, - "error_code": "OK", - "message": "" - } - - # 距离检查 - if npc_distance > SAFEBOX_OPEN_DISTANCE: - res["error_code"] = "TOO_FAR" - res["message"] = "<仓库> 距离太远,无法打开仓库。" - return res - - # 10秒防刷频冷却检查 - if (current_time - last_open_time) < SAFEBOX_COOLDOWN_SEC: - res["error_code"] = "COOLDOWN_ACTIVE" - res["message"] = "<仓库> 仓库在 10 秒内只能打开一次。" - return res - - # 密码长度检查 - if entered_pw.is_empty() or entered_pw.length() > SAFEBOX_PASSWORD_MAX_LEN: - res["error_code"] = "INVALID_PASSWORD_LEN" - res["message"] = "<仓库> 密码格式错误,必须为 6 位字符以内。" - return res - - # 密码匹配检查 (ClientManager.cpp:525) - if entered_pw != password: - res["error_code"] = "WRONG_PASSWORD" - res["message"] = "<仓库> 仓库密码错误!" - return res - - # 打开成功 - is_open = true - last_open_time = current_time - res["success"] = true - res["message"] = "<仓库> 仓库开启成功。" - return res - -# 2. 修改仓库密码 (1:1 SafeboxChangePassword) -func change_password(old_pw: String, new_pw: String) -> Dictionary: - var res := { - "success": false, - "error_code": "OK", - "message": "" - } - - if not is_open: - res["error_code"] = "SAFEBOX_NOT_OPEN" - res["message"] = "<仓库> 必须先开启仓库方可修改密码。" - return res - - if old_pw != password: - res["error_code"] = "OLD_PASSWORD_MISMATCH" - res["message"] = "<仓库> 旧密码不匹配,修改失败。" - return res - - if new_pw.is_empty() or new_pw.length() > SAFEBOX_PASSWORD_MAX_LEN: - res["error_code"] = "INVALID_NEW_PASSWORD" - res["message"] = "<仓库> 新密码长度不合法。" - return res - - password = new_pw - res["success"] = true - res["message"] = "<仓库> 仓库密码已成功更改。" - return res - -# 3. 关闭仓库 -func close_safebox() -> void: - is_open = false - -# 4. 存入物品 (Inventory -> Safebox) -func deposit_item(safebox_slot: int, item_data: Dictionary) -> Dictionary: - var res := { "success": false, "error_code": "OK", "message": "" } - if not is_open: - res["error_code"] = "NOT_OPEN" - return res - - if safebox_slot < 0 or safebox_slot >= SAFEBOX_MAX_NUM: - res["error_code"] = "INVALID_SLOT" - return res - - if safebox_grid[safebox_slot] != null: - # 尝试堆叠 - var existing: Dictionary = safebox_grid[safebox_slot] - if existing.get("vnum") == item_data.get("vnum") and existing.get("stackable", false): - existing["count"] = existing.get("count", 1) + item_data.get("count", 1) - res["success"] = true - res["message"] = "物品成功堆叠存入仓库。" - return res - else: - res["error_code"] = "SLOT_OCCUPIED" - res["message"] = "目标仓库格位已被占用。" - return res - - safebox_grid[safebox_slot] = item_data.duplicate(true) - res["success"] = true - res["message"] = "物品成功存入仓库。" - return res - -# 5. 取出物品 (Safebox -> Inventory) -func withdraw_item(safebox_slot: int) -> Dictionary: - var res := { "success": false, "item": null, "error_code": "OK" } - if not is_open: - res["error_code"] = "NOT_OPEN" - return res - - if safebox_slot < 0 or safebox_slot >= SAFEBOX_MAX_NUM: - res["error_code"] = "INVALID_SLOT" - return res - - if safebox_grid[safebox_slot] == null: - res["error_code"] = "EMPTY_SLOT" - return res - - var it: Dictionary = safebox_grid[safebox_slot] - safebox_grid[safebox_slot] = null - res["success"] = true - res["item"] = it - return res - -# 6. 保险箱内格位互换/移动 -func move_safebox_item(from_slot: int, to_slot: int) -> bool: - if not is_open: - return false - if from_slot < 0 or from_slot >= SAFEBOX_MAX_NUM or to_slot < 0 or to_slot >= SAFEBOX_MAX_NUM: - return false - if safebox_grid[from_slot] == null: - return false - - var item_a = safebox_grid[from_slot] - var item_b = safebox_grid[to_slot] - - if item_b != null and item_b.get("vnum") == item_a.get("vnum") and item_a.get("stackable", false): - item_b["count"] = item_b.get("count", 1) + item_a.get("count", 1) - safebox_grid[from_slot] = null - else: - safebox_grid[to_slot] = item_a - safebox_grid[from_slot] = item_b - return true - -# 7. 金条/银条熔铸 (Player Gold -> Gold Bar Item) -# 仓库保管员按 1% 手续费收取金币并铸造成可储存/交易金条 -func smelt_gold_bar(player_gold: int, bar_vnum: int) -> Dictionary: - var res := { - "success": false, - "new_gold": player_gold, - "bar_item": null, - "cost": 0, - "error_code": "OK", - "message": "" - } - - if not GOLD_BAR_SPECS.has(bar_vnum): - res["error_code"] = "INVALID_BAR_VNUM" - res["message"] = "未知的金银条规格。" - return res - - var spec: Dictionary = GOLD_BAR_SPECS[bar_vnum] - var cost: int = spec["smelt_cost"] - res["cost"] = cost - - if player_gold < cost: - res["error_code"] = "INSUFFICIENT_GOLD" - res["message"] = "金币不足,铸造 %s 需支付 %d 金币 (含1%%熔铸税)。" % [spec["name"], cost] - return res - - res["new_gold"] = player_gold - cost - res["bar_item"] = { - "vnum": bar_vnum, - "name": spec["name"], - "count": 1, - "face_value": spec["face_value"] - } - res["success"] = true - res["message"] = "成功熔铸 %s!扣除 %d 金币。" % [spec["name"], cost] - return res - -# 8. 金条/银条承兑支取现金 (Gold Bar Item -> Player Gold) -# 承兑取回票面全额金币 (1:1 face_value) -func encash_gold_bar(player_gold: int, bar_vnum: int) -> Dictionary: - var res := { - "success": false, - "new_gold": player_gold, - "cash_value": 0, - "error_code": "OK", - "message": "" - } - - if not GOLD_BAR_SPECS.has(bar_vnum): - res["error_code"] = "INVALID_BAR_VNUM" - res["message"] = "非官方认证金银条,无法承兑。" - return res - - var spec: Dictionary = GOLD_BAR_SPECS[bar_vnum] - var face_val: int = spec["face_value"] - res["cash_value"] = face_val - res["new_gold"] = player_gold + face_val - res["success"] = true - res["message"] = "成功承兑 %s,兑现出金币 %d!" % [spec["name"], face_val] - return res diff --git a/project/safebox_storage_keeper_system.gd.uid b/project/safebox_storage_keeper_system.gd.uid deleted file mode 100644 index 15f437cb..00000000 --- a/project/safebox_storage_keeper_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cjyglvb4q2evy diff --git a/project/save_snapshot_system.gd b/project/save_snapshot_system.gd deleted file mode 100644 index 7ccf6d54..00000000 --- a/project/save_snapshot_system.gd +++ /dev/null @@ -1,379 +0,0 @@ -# save_snapshot_system.gd —— Metin2 40250 单人单机角色全量快照存读档系统 1:1 -# 对照 40250 服务端 input_db.cpp, ClientVS22/net/Player.h, char.h -class_name SaveSnapshotSystem -extends RefCounted - -signal snapshot_saved(slot_index: int, filepath: String) -signal snapshot_loaded(slot_index: int, filepath: String) -signal snapshot_deleted(slot_index: int) -signal save_error(code: String, msg: String) - -const SCHEMA_VERSION: String = "40250.1" -const DEFAULT_SAVE_DIR: String = "user://saves" -const DEFAULT_MAX_SLOTS: int = 5 -const INVENTORY_SLOTS_COUNT: int = 90 -const BELT_SLOTS_COUNT: int = 16 -const EQUIPMENT_SLOTS_COUNT: int = 12 - -const JOB_NAMES: Dictionary = { - 0: "战士", - 1: "刺客", - 2: "修罗", - 3: "萨满" -} - -# 校验快照结构与版本完整性 -static func validate_snapshot(snapshot: Dictionary) -> Dictionary: - if not snapshot.has("meta"): - return {"ok": false, "reason": "MISSING_META", "msg": "缺失存档元信息 (meta)!"} - var meta = snapshot["meta"] - if not meta.has("schema_version") or meta["schema_version"] != SCHEMA_VERSION: - return {"ok": false, "reason": "VERSION_MISMATCH", "msg": "存档版本不匹配!"} - - if not snapshot.has("stats"): - return {"ok": false, "reason": "MISSING_STATS", "msg": "缺失角色基础属性 (stats)!"} - if not snapshot.has("inventory"): - return {"ok": false, "reason": "MISSING_INVENTORY", "msg": "缺失背包物品数据 (inventory)!"} - if not snapshot.has("equipment"): - return {"ok": false, "reason": "MISSING_EQUIPMENT", "msg": "缺失已装备防具与武器 (equipment)!"} - - return {"ok": true} - -# 将游戏内实时角色对象/字典打包为可持久化全量快照 Dictionary -func create_snapshot(player_data: Dictionary, slot_index: int = 1) -> Dictionary: - var name: String = player_data.get("name", "NewHero") - var job: int = int(player_data.get("job", 0)) - var level: int = int(player_data.get("level", 1)) - var gold: int = int(player_data.get("gold", 0)) - var playtime: int = int(player_data.get("playtime", 0)) - var map_name: String = player_data.get("map_name", "village") - - # 坐标序列化 - var pos_val = player_data.get("pos", Vector3.ZERO) - var pos_arr: Array = [0.0, 0.0, 0.0] - if pos_val is Vector3: - pos_arr = [pos_val.x, pos_val.y, pos_val.z] - elif pos_val is Array and pos_val.size() >= 3: - pos_arr = [float(pos_val[0]), float(pos_val[1]), float(pos_val[2])] - - # 背包序列化 (确保 90 格) - var raw_inv: Array = player_data.get("inventory", []) - var safe_inv: Array = [] - for i in range(INVENTORY_SLOTS_COUNT): - if i < raw_inv.size() and raw_inv[i] != null: - safe_inv.append(raw_inv[i].duplicate(true)) - else: - safe_inv.append(null) - - # 装备栏序列化 - var raw_equip: Dictionary = player_data.get("equipment", {}) - var safe_equip: Dictionary = {} - for k in raw_equip.keys(): - if raw_equip[k] != null: - safe_equip[str(k)] = raw_equip[k].duplicate(true) - else: - safe_equip[str(k)] = null - - # 腰带与药水栏序列化 - var belt_item = player_data.get("belt_item", null) - if belt_item != null: - belt_item = belt_item.duplicate(true) - var raw_belt_inv: Array = player_data.get("belt_inventory", []) - var safe_belt_inv: Array = [] - for i in range(BELT_SLOTS_COUNT): - if i < raw_belt_inv.size() and raw_belt_inv[i] != null: - safe_belt_inv.append(raw_belt_inv[i].duplicate(true)) - else: - safe_belt_inv.append(null) - - # 时装槽序列化 - var raw_costumes: Dictionary = player_data.get("costumes", {}) - var safe_costumes: Dictionary = {} - for k in raw_costumes.keys(): - if raw_costumes[k] != null: - safe_costumes[str(k)] = raw_costumes[k].duplicate(true) - else: - safe_costumes[str(k)] = null - - # 技能数据序列化 - var skills: Dictionary = player_data.get("skills", {}).duplicate(true) - - # 龙石炼金数据 - var alchemy: Dictionary = player_data.get("alchemy", {}).duplicate(true) - - # 怪物卡片/成就/头衔 - var monster_cards: Array = player_data.get("monster_cards", []).duplicate(true) - var achievements: Array = player_data.get("achievements", []).duplicate(true) - var active_title: String = player_data.get("active_title", "") - - var snapshot: Dictionary = { - "meta": { - "schema_version": SCHEMA_VERSION, - "slot_index": slot_index, - "saved_at": int(Time.get_unix_time_from_system()), - "saved_datetime": Time.get_datetime_string_from_system() - }, - "summary": { - "name": name, - "job": job, - "job_name": JOB_NAMES.get(job, "未知"), - "level": level, - "gold": gold, - "map_name": map_name, - "playtime": playtime - }, - "stats": { - "name": name, - "job": job, - "level": level, - "exp": int(player_data.get("exp", 0)), - "hp": int(player_data.get("hp", 100)), - "max_hp": int(player_data.get("max_hp", 100)), - "sp": int(player_data.get("sp", 100)), - "max_sp": int(player_data.get("max_sp", 100)), - "stamina": int(player_data.get("stamina", 1000)), - "alignment": int(player_data.get("alignment", 0)), - "str": int(player_data.get("str", 15)), - "dex": int(player_data.get("dex", 15)), - "con": int(player_data.get("con", 15)), - "int_stat": int(player_data.get("int_stat", 15)), - "stat_points": int(player_data.get("stat_points", 0)), - "skill_points": int(player_data.get("skill_points", 0)), - "gold": gold, - "pos": pos_arr, - "map_name": map_name, - "playtime": playtime - }, - "equipment": safe_equip, - "inventory": safe_inv, - "belt_system": { - "belt_item": belt_item, - "belt_inventory": safe_belt_inv - }, - "costumes": safe_costumes, - "skills": skills, - "alchemy": alchemy, - "progression": { - "monster_cards": monster_cards, - "achievements": achievements, - "active_title": active_title - } - } - - return snapshot - -# 从全量快照反序列化回角色运行时字典 -func restore_snapshot(snapshot: Dictionary) -> Dictionary: - var val_res = validate_snapshot(snapshot) - if not val_res["ok"]: - return val_res - - var stats: Dictionary = snapshot["stats"] - var pos_arr: Array = stats.get("pos", [0.0, 0.0, 0.0]) - var restored_pos := Vector3(pos_arr[0], pos_arr[1], pos_arr[2]) - - var restored: Dictionary = stats.duplicate(true) - restored["pos"] = restored_pos - - # 恢复背包 (90 格) - var raw_inv: Array = snapshot.get("inventory", []) - var restored_inv: Array = [] - for i in range(INVENTORY_SLOTS_COUNT): - if i < raw_inv.size() and raw_inv[i] != null: - restored_inv.append(raw_inv[i].duplicate(true)) - else: - restored_inv.append(null) - restored["inventory"] = restored_inv - - # 恢复装备栏 (转回 int 键) - var raw_equip: Dictionary = snapshot.get("equipment", {}) - var restored_equip: Dictionary = {} - for k in raw_equip.keys(): - var slot_id: int = int(k) - if raw_equip[k] != null: - restored_equip[slot_id] = raw_equip[k].duplicate(true) - else: - restored_equip[slot_id] = null - restored["equipment"] = restored_equip - - # 恢复腰带 - var belt_sys: Dictionary = snapshot.get("belt_system", {}) - restored["belt_item"] = belt_sys.get("belt_item", null) - var raw_belt_inv: Array = belt_sys.get("belt_inventory", []) - var restored_belt_inv: Array = [] - for i in range(BELT_SLOTS_COUNT): - if i < raw_belt_inv.size() and raw_belt_inv[i] != null: - restored_belt_inv.append(raw_belt_inv[i].duplicate(true)) - else: - restored_belt_inv.append(null) - restored["belt_inventory"] = restored_belt_inv - - # 恢复时装 - var raw_costumes: Dictionary = snapshot.get("costumes", {}) - var restored_costumes: Dictionary = {} - for k in raw_costumes.keys(): - var slot_id: int = int(k) - if raw_costumes[k] != null: - restored_costumes[slot_id] = raw_costumes[k].duplicate(true) - else: - restored_costumes[slot_id] = null - restored["costumes"] = restored_costumes - - # 恢复技能与炼金 - restored["skills"] = snapshot.get("skills", {}).duplicate(true) - restored["alchemy"] = snapshot.get("alchemy", {}).duplicate(true) - - # 恢复进度 - var prog: Dictionary = snapshot.get("progression", {}) - restored["monster_cards"] = prog.get("monster_cards", []).duplicate(true) - restored["achievements"] = prog.get("achievements", []).duplicate(true) - restored["active_title"] = prog.get("active_title", "") - - return { - "ok": true, - "player_data": restored - } - -# 获取存档槽位实际物理路径 -func get_slot_path(slot_index: int, custom_dir: String = "") -> String: - var base_dir := custom_dir if not custom_dir.is_empty() else DEFAULT_SAVE_DIR - return base_dir.path_join("save_slot_%d.json" % slot_index) - -# 保存指定槽位 -func save_slot(slot_index: int, player_data: Dictionary, custom_dir: String = "") -> Dictionary: - var path := get_slot_path(slot_index, custom_dir) - var parent_dir := path.get_base_dir() - - if not DirAccess.dir_exists_absolute(parent_dir): - var err := DirAccess.make_dir_recursive_absolute(parent_dir) - if err != OK: - save_error.emit("DIR_CREATE_FAILED", "无法创建存档目录:%s" % parent_dir) - return {"ok": false, "reason": "DIR_CREATE_FAILED", "path": path} - - var snapshot := create_snapshot(player_data, slot_index) - var json_string := JSON.stringify(snapshot, "\t") - - var file := FileAccess.open(path, FileAccess.WRITE) - if not file: - var err_code := FileAccess.get_open_error() - save_error.emit("FILE_WRITE_FAILED", "无法打开存档文件写入,错误码: %d" % err_code) - return {"ok": false, "reason": "FILE_WRITE_FAILED", "error": err_code, "path": path} - - file.store_string(json_string) - file.close() - - snapshot_saved.emit(slot_index, path) - return { - "ok": true, - "slot_index": slot_index, - "path": path, - "summary": snapshot["summary"], - "msg": "角色【%s】在槽位 %d 保存成功!" % [snapshot["summary"]["name"], slot_index] - } - -# 加载指定槽位 -func load_slot(slot_index: int, custom_dir: String = "") -> Dictionary: - var path := get_slot_path(slot_index, custom_dir) - if not FileAccess.file_exists(path): - save_error.emit("FILE_NOT_FOUND", "槽位 %d 的存档文件不存在!" % slot_index) - return {"ok": false, "reason": "FILE_NOT_FOUND", "path": path} - - var file := FileAccess.open(path, FileAccess.READ) - if not file: - var err_code := FileAccess.get_open_error() - save_error.emit("FILE_READ_FAILED", "无法打开存档文件读取,错误码: %d" % err_code) - return {"ok": false, "reason": "FILE_READ_FAILED", "error": err_code, "path": path} - - var content := file.get_as_text() - file.close() - - var parse_result = JSON.parse_string(content) - if not (parse_result is Dictionary): - save_error.emit("JSON_PARSE_ERROR", "存档文件格式损坏或无法解析!") - return {"ok": false, "reason": "JSON_PARSE_ERROR", "path": path} - - var restore_res = restore_snapshot(parse_result) - if not restore_res["ok"]: - save_error.emit(restore_res["reason"], restore_res["msg"]) - return restore_res - - snapshot_loaded.emit(slot_index, path) - return { - "ok": true, - "slot_index": slot_index, - "path": path, - "player_data": restore_res["player_data"], - "summary": parse_result.get("summary", {}), - "msg": "槽位 %d 角色【%s】读档成功!" % [slot_index, restore_res["player_data"]["name"]] - } - -# 删除指定槽位存档 -func delete_slot(slot_index: int, custom_dir: String = "") -> Dictionary: - var path := get_slot_path(slot_index, custom_dir) - if not FileAccess.file_exists(path): - return {"ok": false, "reason": "FILE_NOT_FOUND", "path": path} - - var err := DirAccess.remove_absolute(path) - if err != OK: - save_error.emit("DELETE_FAILED", "删除槽位 %d 存档失败,错误码: %d" % [slot_index, err]) - return {"ok": false, "reason": "DELETE_FAILED", "error": err, "path": path} - - snapshot_deleted.emit(slot_index) - return {"ok": true, "slot_index": slot_index, "path": path, "msg": "槽位 %d 存档已清空。" % slot_index} - -# 获取所有槽位概览信息 (无需加载全部背包即可在 UI 角色选择列表高效展示) -func get_slot_summaries(custom_dir: String = "", max_slots: int = DEFAULT_MAX_SLOTS) -> Array: - var summaries: Array = [] - for i in range(1, max_slots + 1): - var path := get_slot_path(i, custom_dir) - if not FileAccess.file_exists(path): - summaries.append({ - "slot_index": i, - "is_empty": true, - "path": path - }) - continue - - var file := FileAccess.open(path, FileAccess.READ) - if not file: - summaries.append({ - "slot_index": i, - "is_empty": false, - "corrupted": true, - "path": path - }) - continue - - var content := file.get_as_text() - file.close() - - var data = JSON.parse_string(content) - if data is Dictionary and data.has("summary"): - var sum = data["summary"].duplicate(true) - sum["slot_index"] = i - sum["is_empty"] = false - sum["saved_at"] = data.get("meta", {}).get("saved_at", 0) - sum["saved_datetime"] = data.get("meta", {}).get("saved_datetime", "") - sum["path"] = path - summaries.append(sum) - else: - summaries.append({ - "slot_index": i, - "is_empty": false, - "corrupted": true, - "path": path - }) - - return summaries - -# 导出为完整 JSON 字符串 -func export_json(player_data: Dictionary, slot_index: int = 1, pretty: bool = true) -> String: - var snapshot := create_snapshot(player_data, slot_index) - return JSON.stringify(snapshot, "\t" if pretty else "") - -# 从 JSON 字符串解析还原角色数据 -func import_json(json_str: String) -> Dictionary: - var parsed = JSON.parse_string(json_str) - if not (parsed is Dictionary): - return {"ok": false, "reason": "INVALID_JSON_ROOT", "msg": "JSON 数据根节点必须为字典!"} - return restore_snapshot(parsed) diff --git a/project/save_snapshot_system.gd.uid b/project/save_snapshot_system.gd.uid deleted file mode 100644 index d105dfe2..00000000 --- a/project/save_snapshot_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://5xu50htpcyb8 diff --git a/project/security_lock_system.gd b/project/security_lock_system.gd deleted file mode 100644 index 6d4a906f..00000000 --- a/project/security_lock_system.gd +++ /dev/null @@ -1,223 +0,0 @@ -# security_lock_system.gd —— Metin2 40250 仓库 6 位安全密码与贵重装备防丢锁定 1:1 -# 对照 40250 服务端 safebox.cpp, input_login.cpp, char.cpp -class_name SecurityLockSystem -extends RefCounted - -signal safebox_unlocked() -signal safebox_closed() -signal password_changed(new_pwd: String) -signal item_locked(item_name: String) -signal item_unlocked(item_name: String) - -const DEFAULT_SAFEBOX_PASSWORD := "000000" -const PASSWORD_LEN := 6 -const CHAR_DELETE_CODE_LEN := 7 -const MAX_FAILED_ATTEMPTS := 5 - -var safebox_password: String = DEFAULT_SAFEBOX_PASSWORD -var is_safebox_unlocked: bool = false -var failed_attempts: int = 0 -var is_locked_out: bool = false -var char_delete_code: String = "1234567" - -# 验证并开启仓库 -func open_safebox(input_pwd: String) -> Dictionary: - if is_locked_out: - return { - "ok": false, - "reason": "LOCKOUT", - "msg": "连续输错5次密码,仓库已被暂时锁定!请稍后再试。" - } - - if input_pwd == safebox_password: - failed_attempts = 0 - is_safebox_unlocked = true - safebox_unlocked.emit() - return { - "ok": true, - "msg": "仓库密码验证成功,仓库已开启!" - } - - failed_attempts += 1 - if failed_attempts >= MAX_FAILED_ATTEMPTS: - is_locked_out = true - return { - "ok": false, - "reason": "WRONG_PASSWORD_LOCKOUT", - "failed_attempts": failed_attempts, - "msg": "密码错误达到上限(5次),仓库已被安全保护锁定!" - } - - var remaining := MAX_FAILED_ATTEMPTS - failed_attempts - return { - "ok": false, - "reason": "WRONG_PASSWORD", - "failed_attempts": failed_attempts, - "remaining_attempts": remaining, - "msg": "仓库密码错误!还剩 %d 次尝试机会。" % remaining - } - -# 关闭仓库 -func close_safebox() -> void: - is_safebox_unlocked = false - safebox_closed.emit() - -# 重置防爆保护锁定 (如由 GM 或系统定时重置) -func reset_lockout() -> void: - is_locked_out = false - failed_attempts = 0 - -# 修改仓库 6 位密码 -func change_safebox_password(old_pwd: String, new_pwd: String) -> Dictionary: - if old_pwd != safebox_password: - return { - "ok": false, - "reason": "WRONG_OLD_PASSWORD", - "msg": "旧密码验证失败,无法修改!" - } - - if new_pwd.length() != PASSWORD_LEN or not new_pwd.is_valid_int() or int(new_pwd) < 0: - return { - "ok": false, - "reason": "INVALID_PASSWORD_FORMAT", - "msg": "仓库密码必须为严格 6 位纯数字!" - } - - if new_pwd == old_pwd: - return { - "ok": false, - "reason": "SAME_PASSWORD", - "msg": "新密码不能与旧密码相同!" - } - - safebox_password = new_pwd - password_changed.emit(new_pwd) - - return { - "ok": true, - "new_password": new_pwd, - "msg": "仓库密码已成功修改!" - } - -# 锁定装备/贵重物品 (防丢锁定) -func lock_item(item: Dictionary, pwd: String) -> Dictionary: - if pwd != safebox_password: - return { - "ok": false, - "reason": "WRONG_SECURITY_PASSWORD", - "msg": "安全密码错误,无法加锁!" - } - - if item.get("is_locked", false): - return { - "ok": false, - "reason": "ALREADY_LOCKED", - "msg": "该物品已处于锁定状态!" - } - - item["is_locked"] = true - var iname = item.get("name", "物品") - item_locked.emit(iname) - - return { - "ok": true, - "item": item, - "msg": "物品【%s】已安全加锁!" % iname - } - -# 解除物品锁定 -func unlock_item(item: Dictionary, pwd: String) -> Dictionary: - if pwd != safebox_password: - return { - "ok": false, - "reason": "WRONG_SECURITY_PASSWORD", - "msg": "安全密码错误,无法解锁!" - } - - if not item.get("is_locked", false): - return { - "ok": false, - "reason": "NOT_LOCKED", - "msg": "该物品未处于锁定状态!" - } - - item["is_locked"] = false - var iname = item.get("name", "物品") - item_unlocked.emit(iname) - - return { - "ok": true, - "item": item, - "msg": "物品【%s】已解除锁定!" % iname - } - -# 动作权限校验 —— 丢弃到地面 -func can_drop_item(item: Dictionary) -> Dictionary: - if item.get("is_locked", false): - return { - "allowed": false, - "reason": "ITEM_LOCKED", - "msg": "已加锁的贵重物品无法丢弃!" - } - return {"allowed": true} - -# 动作权限校验 —— 出售给 NPC -func can_sell_item(item: Dictionary) -> Dictionary: - if item.get("is_locked", false): - return { - "allowed": false, - "reason": "ITEM_LOCKED", - "msg": "已加锁的贵重物品无法出售给商人!" - } - return {"allowed": true} - -# 动作权限校验 —— 彻底销毁 -func can_destroy_item(item: Dictionary) -> Dictionary: - if item.get("is_locked", false): - return { - "allowed": false, - "reason": "ITEM_LOCKED", - "msg": "已加锁的贵重物品无法直接销毁!" - } - return {"allowed": true} - -# 动作权限校验 —— 玩家交易 -func can_trade_item(item: Dictionary) -> Dictionary: - if item.get("is_locked", false): - return { - "allowed": false, - "reason": "ITEM_LOCKED", - "msg": "已加锁的贵重物品无法用于玩家交易!" - } - return {"allowed": true} - -# 动作权限校验 —— 强化或熔炼消耗 -func can_refine_item(item: Dictionary) -> Dictionary: - if item.get("is_locked", false): - return { - "allowed": false, - "reason": "ITEM_LOCKED", - "msg": "已加锁的装备无法用于强化或附魔消耗!" - } - return {"allowed": true} - -# 角色删除 7 位安全码验证 (对照 40250 input_login.cpp) -func verify_character_deletion(input_code: String) -> Dictionary: - if input_code.length() != CHAR_DELETE_CODE_LEN or not input_code.is_valid_int() or int(input_code) < 0: - return { - "ok": false, - "reason": "INVALID_CODE_FORMAT", - "msg": "角色删除码必须为严格 7 位纯数字!" - } - - if input_code != char_delete_code: - return { - "ok": false, - "reason": "WRONG_DELETE_CODE", - "msg": "角色删除安全码错误!" - } - - return { - "ok": true, - "msg": "角色删除安全码验证通过,允许删除角色。" - } diff --git a/project/security_lock_system.gd.uid b/project/security_lock_system.gd.uid deleted file mode 100644 index 98c61c6c..00000000 --- a/project/security_lock_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b5u8bbhj5ybdi diff --git a/project/skill_book_reading_system.gd b/project/skill_book_reading_system.gd deleted file mode 100644 index 00eeb65b..00000000 --- a/project/skill_book_reading_system.gd +++ /dev/null @@ -1,213 +0,0 @@ -# skill_book_reading_system.gd —— 40250 官方大师技能书悟道研读与突破系统 1:1 -# 严格对照: -# metin2/src/server/game/src/char_skill.cpp:416-560 (CHARACTER::LearnSkillByBook) -# metin2/src/server/game/src/char_item.cpp:2128-2160 (ITEM_SKILLBOOK / 50300) -# metin2/src/server/game/src/affect.h (AFFECT_SKILL_BOOK_BONUS 512, AFFECT_SKILL_NO_BOOK_DELAY 513) -# metin2/src/server/share/conf/item_proto.txt (71001 驱魔卷轴, 71094 隐士之药) -extends RefCounted - -const ITEM_SKILLBOOK_VNUM := 50300 -const EXORCISM_SCROLL_VNUM := 71001 # 驱魔卷轴: AFFECT_SKILL_NO_BOOK_DELAY -const HERMIT_ADVICE_VNUM := 71094 # 隐士之药: AFFECT_SKILL_BOOK_BONUS - -const EXP_COST := 20000 # 40250 char_skill.cpp:430 need_exp = 20000 -const COOLDOWN_DURATION := 86400 # 24小时冷却 (LC_IsKorea 86400 秒) - -const BASE_SUCCESS_RATE := 35 # 基础成功率 35% (char_skill.cpp:524: percent = 65 -> 100 - 65 = 35%) - -# 技能进阶档次 (MasterType) -const SKILL_NORMAL := 0 # Lv 1 - 19 -const SKILL_MASTER := 1 # M1 (Lv 20) ~ M10 (Lv 29) -const SKILL_GRAND_MASTER := 2 # G1 (Lv 30) ~ G10 (Lv 39) -const SKILL_PERFECT_MASTER := 3 # P (Lv 40) - -# 角色技能书研读状态 -var skills: Dictionary = {} # skill_id -> {"level": int, "read_count": int, "next_read_time": int} -var affects: Dictionary = {} # affect_type -> bool -var player_exp: int = 100000 -var current_time: int = 1000000 - -# 累计统计 -var total_books_read: int = 0 -var total_books_succeeded: int = 0 -var total_books_failed: int = 0 - -func _init(initial_exp: int = 100000, initial_time: int = 1000000) -> void: - player_exp = initial_exp - current_time = initial_time - skills = {} - affects = {} - total_books_read = 0 - total_books_succeeded = 0 - total_books_failed = 0 - -# 注册/初始化技能 -func init_skill(skill_id: int, level: int = 20, read_count: int = 0, next_read_time: int = 0) -> void: - skills[skill_id] = { - "level": level, - "read_count": read_count, - "next_read_time": next_read_time - } - -# 获取技能档次 -func get_skill_master_type(skill_id: int) -> int: - if not skills.has(skill_id): - return SKILL_NORMAL - var lvl: int = skills[skill_id]["level"] - if lvl < 20: - return SKILL_NORMAL - elif lvl < 30: - return SKILL_MASTER - elif lvl < 40: - return SKILL_GRAND_MASTER - else: - return SKILL_PERFECT_MASTER - -# 计算升下一级所需的成功书本数 (char_skill.cpp:509 need_bookcount = level - 20) -# M1(Lv20) 升 M2: 需 1 本 (20 - 20 + 1 = 1) -# M2(Lv21) 升 M3: 需 2 本 -# ... -# M10(Lv29) 升 G1: 需 10 本 -# 累计突破至 G 需 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 = 55 本成功! -func get_need_books_for_next_level(skill_id: int) -> int: - if not skills.has(skill_id): - return 0 - var lvl: int = skills[skill_id]["level"] - if lvl < 20 or lvl >= 30: - return 0 - return lvl - 20 + 1 - -# 使用驱魔卷轴 (71001) -func use_exorcism_scroll() -> bool: - affects[EXORCISM_SCROLL_VNUM] = true - return true - -# 使用隐士之药 (71094) -func use_hermit_advice() -> bool: - affects[HERMIT_ADVICE_VNUM] = true - return true - -# 检查是否存在状态 -func has_affect(affect_vnum: int) -> bool: - return affects.get(affect_vnum, false) - -# 研读技能书核心方法 (1:1 CHARACTER::LearnSkillByBook) -# roll_override: 传入 1~100 用于单元测试确定性判定,-1 表示根据随机数判定 -func learn_skill_by_book(skill_id: int, book_socket_skill_vnum: int = -1, roll_override: int = -1) -> Dictionary: - var result := { - "success": false, - "level_up": false, - "breakthrough_g": false, - "error_code": "OK", - "read_count": 0, - "needed_books": 0, - "remaining_books": 0, - "current_level": 0, - "exp_deducted": 0, - "consumed_exorcism": false, - "consumed_hermit": false, - "message": "" - } - - # 1. 校验技能是否存在 - if not skills.has(skill_id): - result["error_code"] = "NOT_LEARNABLE" - result["message"] = "无法研读未掌握或非本流派的技能。" - return result - - # 2. 校验技能书 Socket 绑定的技能编号 (40250 char_item.cpp:2132) - if book_socket_skill_vnum != -1 and book_socket_skill_vnum != skill_id: - result["error_code"] = "SKILL_MISMATCH" - result["message"] = "该技能书并非此技能专研典籍。" - return result - - var skill_data: Dictionary = skills[skill_id] - var current_lvl: int = skill_data["level"] - result["current_level"] = current_lvl - - # 3. 校验阶档 - var master_type: int = get_skill_master_type(skill_id) - if master_type < SKILL_MASTER: - result["error_code"] = "NOT_MASTER_TIER" - result["message"] = "此技能尚未达到大师 (M) 阶档,无法研读书籍。" - return result - elif master_type >= SKILL_GRAND_MASTER: - result["error_code"] = "ALREADY_G_OR_P" - result["message"] = "此技能已达到宗师 (G) 阶或更高,书籍无法再提升造诣。" - return result - - # 4. 经验值检查与消耗 (char_skill.cpp:430: need_exp = 20000) - if player_exp < EXP_COST: - result["error_code"] = "INSUFFICIENT_EXP" - result["message"] = "角色经验值不足 20,000,精神无法专注研读。" - return result - - # 5. 冷却时间与驱魔卷轴检查 (char_skill.cpp:455) - var next_time: int = skill_data.get("next_read_time", 0) - var bypass_cooldown := false - if current_time < next_time: - if has_affect(EXORCISM_SCROLL_VNUM): - affects.erase(EXORCISM_SCROLL_VNUM) - bypass_cooldown = true - result["consumed_exorcism"] = true - else: - result["error_code"] = "COOLDOWN_ACTIVE" - result["message"] = "心智尚需沉淀,请在冷却结束后再读,或使用驱魔卷轴。" - return result - - # 扣除 20,000 EXP 并产生 24 小时冷却 (86,400 秒) - player_exp -= EXP_COST - result["exp_deducted"] = EXP_COST - skill_data["next_read_time"] = current_time + COOLDOWN_DURATION - total_books_read += 1 - - # 6. 成功率计算与隐士之药加成 - var success_chance: int = BASE_SUCCESS_RATE - if has_affect(HERMIT_ADVICE_VNUM): - affects.erase(HERMIT_ADVICE_VNUM) - success_chance = 100 # 隐士之药 100% 保底成功 (char_skill.cpp:527) - result["consumed_hermit"] = true - - var roll := roll_override if roll_override > 0 else (randi() % 100 + 1) - var is_read_success: bool = (roll <= success_chance) - - var need_books: int = get_need_books_for_next_level(skill_id) - result["needed_books"] = need_books - - if not is_read_success: - # 研读失败 (char_skill.cpp:613 READ_FAIL) - total_books_failed += 1 - result["success"] = false - result["read_count"] = skill_data["read_count"] - result["remaining_books"] = need_books - skill_data["read_count"] - result["error_code"] = "READ_FAIL" - result["message"] = "研读失败:经脉运转不顺,书中奥义未能参透。" - return result - - # 研读成功 - total_books_succeeded += 1 - result["success"] = true - var cur_count: int = skill_data["read_count"] + 1 - - if cur_count >= need_books: - # 晋级提升一阶等级!(SkillLevelUp) - skill_data["level"] += 1 - skill_data["read_count"] = 0 - result["level_up"] = true - result["current_level"] = skill_data["level"] - result["read_count"] = 0 - result["remaining_books"] = 0 - - if skill_data["level"] >= 30: - result["breakthrough_g"] = true - result["message"] = "顿悟成功!技能历经千锤百炼,成功突破至宗师境界 (G1)!" - else: - result["message"] = "顿悟成功!技能境界提升至 M%d!" % (skill_data["level"] - 19) - else: - # 累积研读进度 - skill_data["read_count"] = cur_count - result["read_count"] = cur_count - result["remaining_books"] = need_books - cur_count - result["message"] = "心领神会!还需研读 %d 本成功即可突破进阶。" % (need_books - cur_count) - - return result diff --git a/project/skill_book_reading_system.gd.uid b/project/skill_book_reading_system.gd.uid deleted file mode 100644 index da5221dd..00000000 --- a/project/skill_book_reading_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://uf2nae5qlq64 diff --git a/project/skill_book_system.gd b/project/skill_book_system.gd deleted file mode 100644 index fe801996..00000000 --- a/project/skill_book_system.gd +++ /dev/null @@ -1,203 +0,0 @@ -# skill_book_system.gd —— 40250 技能书研读与技能突破进阶系统 1:1 规则引擎 -# 严格对照: -# metin2/src/server/game/src/char_item.cpp:2128-2160 (ITEM_SKILLBOOK / 50300) -# metin2/src/server/game/src/char_skill.cpp:416-570 (CHARACTER::LearnSkillByBook) -# metin2/src/server/game/src/char_item.cpp:1657 (Soul Stone 50513 / G->P) -# ClientVS22/UserInterface/Packet.h -extends RefCounted - -const SKILL_BOOK_VNUM := 50300 -const EXORCISM_SCROLL_VNUM := 71001 # 驱魔卷轴 (AFFECT_SKILL_NO_BOOK_DELAY) -const HERMIT_ADVICE_VNUM := 71094 # 隐士教训 (AFFECT_SKILL_BOOK_BONUS) -const SOUL_STONE_VNUM := 71001 # 魂石 (50513) - -const SKILL_BOOK_EXP_COST := 20000 # 40250 char_skill.cpp:430 need_exp = 20000 -const SKILL_BOOK_DELAY := 86400 # 24 小时冷却 (LC_IsKorea 86400s) - -# 40250 MasterType 枚举 -const MASTER_NORMAL := 0 # Lv 1 - 20 -const MASTER_M := 1 # M1 - M10 (Lv 21 - 30) -const MASTER_G := 2 # G1 - G10 (Lv 31 - 40) -const MASTER_P := 3 # Perfect Master (Lv 40) - -# 错误码 -enum Result { - SUCCESS_LEVEL_UP, # 成功并升级(或突破到 G) - SUCCESS_READ_ONCE, # 成功研读一本(累积进度) - FAIL_READ, # 研读失败 - ERR_NOT_LEARNABLE, # 非本职业/未入流派技能 - ERR_NOT_MASTER_TIER, # 尚未达到 M 阶 (Lv < 20) - ERR_ALREADY_G_OR_P, # 已达到 G 阶或更高,无法再读技能书 - ERR_INSUFFICIENT_EXP, # 经验不足 20,000 - ERR_COOLDOWN, # 24小时冷却中 -} - -# 魂石善恶值消耗表 (char_skill.cpp / training_grandmaster_skill.quest) -const SOUL_STONE_KARMA_COST := { - 1: 1000, 2: 1500, 3: 2000, 4: 2500, 5: 3000, - 6: 3500, 7: 4000, 8: 4500, 9: 5000, 10: 5500 -} - -# 判定技能书升级所需本数:40250 char_skill.cpp:509 need_bookcount = GetSkillLevel(dwSkillVnum) - 20 -static func get_need_books(skill_level: int) -> int: - # M1 (21) 需 1 本,M2 (22) 需 2 本 ... M10 (30) 需 10 本 - return maxi(1, skill_level - 20) - -# 研读条件前置校验 -static func check_can_read( - skill_id: int, - skill_level: int, - master_type: int, - player_exp: int, - next_read_time: int, - current_time: int, - has_exorcism: bool -) -> int: - if skill_id <= 0: - return Result.ERR_NOT_LEARNABLE - - if master_type < MASTER_M or skill_level < 20: - return Result.ERR_NOT_MASTER_TIER - - if master_type > MASTER_M or skill_level >= 30: - return Result.ERR_ALREADY_G_OR_P - - if player_exp < SKILL_BOOK_EXP_COST: - return Result.ERR_INSUFFICIENT_EXP - - if current_time < next_read_time and not has_exorcism: - return Result.ERR_COOLDOWN - - return 0 # OK - -# 研读技能书核心判定逻辑(1:1 char_skill.cpp:488-570) -static func read_skill_book( - skill_id: int, - skill_level: int, - master_type: int, - read_count: int, - player_exp: int, - next_read_time: int, - current_time: int, - has_exorcism: bool, - has_hermit: bool, - roll_val := -1 # 测试可注入 1..100 -) -> Dictionary: - var err := check_can_read(skill_id, skill_level, master_type, player_exp, next_read_time, current_time, has_exorcism) - if err != 0: - return { - "result": err, - "skill_id": skill_id, - "level": skill_level, - "master_type": master_type, - "read_count": read_count, - "exp_consumed": 0, - "next_read_time": next_read_time, - "consumed_exorcism": false, - "consumed_hermit": false, - } - - var consumed_exorcism := false - if current_time < next_read_time and has_exorcism: - consumed_exorcism = true - - var consumed_hermit := false - var fail_percent := 65 # 40250 char_skill.cpp:525 int percent = 65; - if has_hermit: - fail_percent = 0 # 100% 成功 - consumed_hermit = true - - var dice := roll_val if roll_val > 0 else (randi() % 100 + 1) - var success := (dice > fail_percent) - - var new_level := skill_level - var new_master := master_type - var new_read_count := read_count - var new_next_time := current_time + SKILL_BOOK_DELAY - var res := Result.FAIL_READ - - if success: - var need_books := get_need_books(skill_level) - if read_count + 1 >= need_books: - # 升级! - new_level += 1 - new_read_count = 0 - if new_level >= 30: - new_master = MASTER_G # 突破到 G1 - res = Result.SUCCESS_LEVEL_UP - else: - new_read_count += 1 - res = Result.SUCCESS_READ_ONCE - - return { - "result": res, - "skill_id": skill_id, - "level": new_level, - "master_type": new_master, - "read_count": new_read_count, - "exp_consumed": SKILL_BOOK_EXP_COST, - "next_read_time": new_next_time, - "consumed_exorcism": consumed_exorcism, - "consumed_hermit": consumed_hermit, - "need_books": get_need_books(new_level), - } - -# 魂石修炼逻辑(1:1 G1..G10 -> P) -static func soul_stone_train( - skill_id: int, - skill_level: int, - master_type: int, - current_karma: int, - roll_val := -1 -) -> Dictionary: - if master_type != MASTER_G or skill_level < 30 or skill_level >= 40: - return {"success": false, "err": "not_g_tier"} - - var g_step := skill_level - 30 # 1..10 - var base_cost: int = SOUL_STONE_KARMA_COST.get(g_step, 1000) - var actual_cost: int = base_cost if current_karma >= 0 else (base_cost * 2) - - var dice := roll_val if roll_val > 0 else (randi() % 100 + 1) - # 40250 魂石成功率约 30% - var success := (dice <= 30) - - var new_level := skill_level - var new_master := master_type - if success: - new_level += 1 - if new_level >= 40: - new_master = MASTER_P - - return { - "success": success, - "skill_id": skill_id, - "level": new_level, - "master_type": new_master, - "karma_consumed": actual_cost, - } - -# 格式化官方提示文本 -static func get_result_message(res_dict: Dictionary) -> String: - match res_dict.get("result", -1): - Result.SUCCESS_LEVEL_UP: - if res_dict.get("master_type") == MASTER_G and res_dict.get("level") == 30: - return "领悟了大宗师的奥秘!技能已成功突破至大宗师(G1)!" - return "完全理解了书中内容,技能成功提升到了新的境界!" - Result.SUCCESS_READ_ONCE: - var remaining := int(res_dict.get("need_books", 1)) - int(res_dict.get("read_count", 0)) - return "成功领悟了部分技巧!还需要成功研读 %d 本书以完成突破。" % remaining - Result.FAIL_READ: - return "研读失败,未能领悟书中的奥秘。" - Result.ERR_NOT_LEARNABLE: - return "无法修炼该技能书。" - Result.ERR_NOT_MASTER_TIER: - return "这个技能还未达到大师级别(M),不能通过技能书修炼。" - Result.ERR_ALREADY_G_OR_P: - return "这个技能已达到大宗师或以上级别,无法再通过普通技能书提升。" - Result.ERR_INSUFFICIENT_EXP: - return "经验值不足(需要 20,000 EXP)。" - Result.ERR_COOLDOWN: - var remaining_s := maxi(0, int(res_dict.get("next_read_time", 0))) - return "当前处于研读冷却中,请使用驱魔卷轴消除冷却或稍后再试。" - _: - return "" diff --git a/project/skill_book_system.gd.uid b/project/skill_book_system.gd.uid deleted file mode 100644 index abbc45dd..00000000 --- a/project/skill_book_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cliup1bwf27h5 diff --git a/project/socket_glow_fx_system.gd b/project/socket_glow_fx_system.gd deleted file mode 100644 index e216d549..00000000 --- a/project/socket_glow_fx_system.gd +++ /dev/null @@ -1,123 +0,0 @@ -# socket_glow_fx_system.gd —— Metin2 40250 装备多孔镶嵌宝石发光流光粒子视觉系统 1:1 -# 对照 40250 客户端 ItemData.h, PythonItem.cpp, InstanceBaseEffect.cpp -class_name SocketGlowFxSystem -extends RefCounted - -signal glow_profile_updated(item_vnum: int, intensity: float, color: Color) - -# +4 经典灵石发光光谱 (色调与波长) -const STONE_COLORS: Dictionary = { - 28430: {"name": "战士灵石+4", "color": Color(1.0, 0.2, 0.15), "glow": 1.2}, # 炽热炽红 - 28431: {"name": "忍者灵石+4", "color": Color(0.2, 1.0, 0.35), "glow": 1.2}, # 剧毒翡翠 - 28432: {"name": "修罗灵石+4", "color": Color(0.75, 0.15, 1.0), "glow": 1.3}, # 幽冥暗紫 - 28433: {"name": "萨满灵石+4", "color": Color(1.0, 0.75, 0.1), "glow": 1.2}, # 太阳金橙 - 28435: {"name": "怪物灵石+4", "color": Color(1.0, 0.05, 0.05), "glow": 1.4}, # 嗜血深红 - 28436: {"name": "施法灵石+4", "color": Color(1.0, 0.95, 0.2), "glow": 1.5}, # 耀金雷光 - 28438: {"name": "守护灵石+4", "color": Color(0.2, 0.65, 1.0), "glow": 1.1}, # 霜华幽蓝 - 28440: {"name": "生命灵石+4", "color": Color(0.1, 0.95, 0.65), "glow": 1.1} # 生命碧绿 -} - -# +7/+8/+9 装备基础强化光效配置 -const REFINE_GLOW_TIERS: Dictionary = { - 7: {"intensity": 0.6, "particles": 12, "pulse": 1.0, "swirl": false, "trail": false}, - 8: {"intensity": 1.2, "particles": 28, "pulse": 1.6, "swirl": false, "trail": true}, - 9: {"intensity": 2.2, "particles": 55, "pulse": 2.2, "swirl": true, "trail": true} -} - -# 计算装备完整的流光与发光特效数据 -func calculate_equipment_glow(item: Dictionary) -> Dictionary: - if item.is_empty(): - return _empty_glow() - - var vnum: int = int(item.get("vnum", 0)) - var refine: int = int(item.get("refine_level", 0)) - var sockets: Array = item.get("sockets", []) - - # 1. 解析宝石镶嵌颜色 - var stone_colors: Array = [] - var stone_glow_boost: float = 0.0 - - for s in sockets: - if s != null and STONE_COLORS.has(int(s)): - var cfg = STONE_COLORS[int(s)] - stone_colors.append(cfg["color"]) - stone_glow_boost += float(cfg["glow"]) * 0.35 - - # 2. 混合计算主光色与副光色 - var primary_color := Color(1.0, 0.95, 0.85) # 默认神圣白金色 - var secondary_color := primary_color - - if not stone_colors.is_empty(): - var total_r := 0.0 - var total_g := 0.0 - var total_b := 0.0 - for c in stone_colors: - total_r += c.r - total_g += c.g - total_b += c.b - var n = float(stone_colors.size()) - primary_color = Color(total_r / n, total_g / n, total_b / n) - secondary_color = stone_colors[0] # 首颗灵石为主导特征色 - - # 3. 强化等级基础光效 - var base_intensity: float = 0.0 - var particle_rate: int = 0 - var pulse_speed: float = 0.8 - var has_trail: bool = false - var swirl_enabled: bool = false - - if REFINE_GLOW_TIERS.has(refine): - var r_cfg = REFINE_GLOW_TIERS[refine] - base_intensity = r_cfg["intensity"] - particle_rate = r_cfg["particles"] - pulse_speed = r_cfg["pulse"] - has_trail = r_cfg["trail"] - swirl_enabled = r_cfg["swirl"] - elif not stone_colors.is_empty(): - # 未到 +7 但镶嵌了灵石,产生轻微宝石微光 - base_intensity = 0.3 - particle_rate = 6 - - var final_intensity: float = base_intensity + stone_glow_boost - var is_glowing: bool = final_intensity > 0.05 - - var result: Dictionary = { - "is_glowing": is_glowing, - "refine_level": refine, - "stone_count": stone_colors.size(), - "glow_intensity": final_intensity, - "primary_color": primary_color, - "secondary_color": secondary_color, - "pulse_speed": pulse_speed, - "particle_rate": particle_rate, - "has_trail": has_trail, - "swirl_enabled": swirl_enabled - } - - glow_profile_updated.emit(vnum, final_intensity, primary_color) - return result - -# 将发光材质参数直接应用至 Godot 3D 材质 -func apply_to_material(mat: StandardMaterial3D, glow_data: Dictionary) -> void: - if not mat or not glow_data.get("is_glowing", false): - if mat: - mat.emission_enabled = false - return - - mat.emission_enabled = true - mat.emission = glow_data["primary_color"] - mat.emission_energy_multiplier = glow_data["glow_intensity"] - -func _empty_glow() -> Dictionary: - return { - "is_glowing": false, - "refine_level": 0, - "stone_count": 0, - "glow_intensity": 0.0, - "primary_color": Color.WHITE, - "secondary_color": Color.WHITE, - "pulse_speed": 1.0, - "particle_rate": 0, - "has_trail": false, - "swirl_enabled": false - } diff --git a/project/socket_glow_fx_system.gd.uid b/project/socket_glow_fx_system.gd.uid deleted file mode 100644 index 9af9c9d7..00000000 --- a/project/socket_glow_fx_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://upnn5jlab615 diff --git a/project/solo_guild_domain_system.gd b/project/solo_guild_domain_system.gd deleted file mode 100644 index 4d974fb3..00000000 --- a/project/solo_guild_domain_system.gd +++ /dev/null @@ -1,190 +0,0 @@ -# solo_guild_domain_system.gd —— Metin2 40250 离线单机个人公会领地与专属建筑建造 1:1 -# 对照 40250 服务端 guild.cpp, guild_manager.cpp, building.cpp -class_name SoloGuildDomainSystem -extends RefCounted - -signal domain_claimed(guild_name: String, cost: int) -signal building_constructed(btype: int, bname: String, level: int) -signal building_upgraded(btype: int, new_level: int) -signal guild_refine_result(success: bool, new_level: int) - -enum BuildingType { - BLACKSMITH = 1, # 公会神兵铁匠 (强化成功率 +10%) - ALCHEMIST = 2, # 公会炼金熔炉 (原矿熔炼成功率 70%) - GUARDIAN_ALTAR = 3 # 公会守护神坛 (生命 +1500, 经验 +10%) -} - -const DOMAIN_COST := 20000000 # 2000 万金币 -const MIN_GUILD_LEVEL := 10 # 公会等级门槛 - -const BUILDING_BLUEPRINTS: Dictionary = { - BuildingType.BLACKSMITH: { - "name": "公会神兵铁匠铺", - "cost": 10000000, - "wood": 100, - "stone": 100, - "max_level": 3 - }, - BuildingType.ALCHEMIST: { - "name": "公会炼金大师熔炉", - "cost": 8000000, - "wood": 80, - "stone": 80, - "max_level": 3 - }, - BuildingType.GUARDIAN_ALTAR: { - "name": "公会守护天龙神坛", - "cost": 15000000, - "wood": 150, - "stone": 150, - "max_level": 3 - } -} - -var has_domain: bool = false -var guild_name: String = "" -var guild_level: int = 1 -var constructed_buildings: Dictionary = {} # btype -> { "level": int, "name": String } - -# 开辟专属公会领地 -func claim_domain(gname: String, glevel: int, player_data: Dictionary) -> Dictionary: - if has_domain: - return {"ok": false, "reason": "ALREADY_HAS_DOMAIN", "msg": "你已拥有一片公会领地!"} - - if glevel < MIN_GUILD_LEVEL: - return {"ok": false, "reason": "GUILD_LEVEL_TOO_LOW", "msg": "开辟领地需要公会等级达到 10 级!"} - - var gold: int = int(player_data.get("gold", 0)) - if gold < DOMAIN_COST: - return {"ok": false, "reason": "NOT_ENOUGH_GOLD", "msg": "金币不足!需要 20,000,000 金币买下领地地契。"} - - player_data["gold"] = gold - DOMAIN_COST - has_domain = true - guild_name = gname - guild_level = glevel - constructed_buildings.clear() - - domain_claimed.emit(guild_name, DOMAIN_COST) - - return { - "ok": true, - "guild_name": guild_name, - "cost": DOMAIN_COST, - "remaining_gold": player_data["gold"], - "msg": "【%s】公会专属领地落成!城堡旗帜高高飘扬!" % guild_name - } - -# 建造领地建筑 -func construct_building(btype: int, player_data: Dictionary) -> Dictionary: - if not has_domain: - return {"ok": false, "reason": "NO_DOMAIN", "msg": "尚未开辟领地!"} - - if not BUILDING_BLUEPRINTS.has(btype): - return {"ok": false, "reason": "INVALID_BUILDING_TYPE"} - - if constructed_buildings.has(btype): - return {"ok": false, "reason": "ALREADY_CONSTRUCTED", "msg": "该建筑已建造完成!可选择升级。"} - - var bp = BUILDING_BLUEPRINTS[btype] - var gold: int = int(player_data.get("gold", 0)) - var cost: int = bp["cost"] - - if gold < cost: - return {"ok": false, "reason": "NOT_ENOUGH_GOLD", "msg": "建造金币不足!需要 %d 金币。" % cost} - - player_data["gold"] = gold - cost - constructed_buildings[btype] = { - "type": btype, - "name": bp["name"], - "level": 1 - } - - building_constructed.emit(btype, bp["name"], 1) - - return { - "ok": true, - "type": btype, - "name": bp["name"], - "level": 1, - "msg": "【%s】在公会领地拔地而起,正式竣工!" % bp["name"] - } - -# 升级领地建筑 -func upgrade_building(btype: int, player_data: Dictionary) -> Dictionary: - if not constructed_buildings.has(btype): - return {"ok": false, "reason": "NOT_CONSTRUCTED_YET"} - - var b = constructed_buildings[btype] - var cur_lvl: int = b["level"] - var bp = BUILDING_BLUEPRINTS[btype] - - if cur_lvl >= bp["max_level"]: - return {"ok": false, "reason": "MAX_LEVEL_REACHED", "msg": "该建筑已达最高等级 (3级)!"} - - var upgrade_cost: int = int(bp["cost"] * 0.8) - var gold: int = int(player_data.get("gold", 0)) - if gold < upgrade_cost: - return {"ok": false, "reason": "NOT_ENOUGH_GOLD", "msg": "升级金币不足!需要 %d 金币。" % upgrade_cost} - - player_data["gold"] = gold - upgrade_cost - b["level"] = cur_lvl + 1 - - building_upgraded.emit(btype, b["level"]) - - return { - "ok": true, - "type": btype, - "new_level": b["level"], - "msg": "【%s】成功升级至等级 %d!" % [b["name"], b["level"]] - } - -# 公会铁匠专属精炼 (+10% 强化成功率) -func guild_blacksmith_refine(item: Dictionary, player_data: Dictionary, force_success: bool = false) -> Dictionary: - if not constructed_buildings.has(BuildingType.BLACKSMITH): - return {"ok": false, "reason": "BLACKSMITH_NOT_BUILT", "msg": "领地尚未建造公会铁匠铺!"} - - var cur_refine: int = int(item.get("refine_level", 0)) - if cur_refine >= 9: - return {"ok": false, "reason": "ALREADY_MAX_REFINE", "msg": "装备强化等级已达 +9 极限!"} - - # 官方基准成功率 + 公会铁匠额外 +10% 成功率 (随铁匠等级提升) - var b_lvl: int = constructed_buildings[BuildingType.BLACKSMITH]["level"] - var bonus_rate: int = 10 + (b_lvl - 1) * 3 # Lv1: +10%, Lv2: +13%, Lv3: +16% - - var base_rates := [100, 90, 80, 70, 60, 50, 40, 30, 20] # +1~+9 - var rate = min(100, base_rates[cur_refine] + bonus_rate) - - var roll = randi_range(1, 100) - var is_succ = force_success or (roll <= rate) - - if is_succ: - item["refine_level"] = cur_refine + 1 - guild_refine_result.emit(true, item["refine_level"]) - return { - "ok": true, - "success": true, - "new_level": item["refine_level"], - "msg": "公会神兵铁匠挥锤如电!精炼成功,装备提升至 +%d!" % item["refine_level"] - } - else: - # 失败降 1 级 (最低 0) - item["refine_level"] = max(0, cur_refine - 1) - guild_refine_result.emit(false, item["refine_level"]) - return { - "ok": true, - "success": false, - "new_level": item["refine_level"], - "msg": "精炼失败!铁砧迸出火星,装备跌落至 +%d。" % item["refine_level"] - } - -# 公会守护神坛常驻被动光环 -func get_altar_passive_buffs() -> Dictionary: - if not constructed_buildings.has(BuildingType.GUARDIAN_ALTAR): - return {} - - var b_lvl: int = constructed_buildings[BuildingType.GUARDIAN_ALTAR]["level"] - return { - "max_hp": 1500 * b_lvl, - "exp_rate_pct": 10 * b_lvl, - "all_stats": 5 * b_lvl - } diff --git a/project/solo_guild_domain_system.gd.uid b/project/solo_guild_domain_system.gd.uid deleted file mode 100644 index cec6445f..00000000 --- a/project/solo_guild_domain_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cj03yvptn2sj0 diff --git a/project/soul_stone_grandmaster_system.gd b/project/soul_stone_grandmaster_system.gd deleted file mode 100644 index 3d2b90de..00000000 --- a/project/soul_stone_grandmaster_system.gd +++ /dev/null @@ -1,178 +0,0 @@ -# soul_stone_grandmaster_system.gd —— Metin2 40250 官方宗师绝技灵魂石悟道与善恶值走火入魔系统 1:1 -# 100% 对照 40250 服务端 training_grandmaster_skill.quest, char_skill.cpp, questlua_pc.cpp -class_name SoulStoneGrandmasterSystem -extends RefCounted - -signal grandmaster_trained(skill_id: int, skill_name: String, new_level: int, alignment_deducted: int) -signal grandmaster_failed(skill_id: int, skill_name: String, alignment_penalty: int) -signal cooldown_cleared() -signal training_error(reason: String) - -# 40250 官方核心道具 Vnum -const VNUM_SOUL_STONE := 50513 # 灵魂石 (Ruh Taşı / Soul Stone) -const VNUM_EXORCISM_SCROLL := 71001 # 驱魔卷轴 (清除修炼间隔) - -# 技能等级境界常数 -const GRAND_MASTER_MIN_LEVEL := 30 # G1 (30级) -const PERFECT_MASTER_LEVEL := 40 # P 阶绝顶境界 (40级) - -# 最低极限负善恶值 (-20000 封底) -const MIN_ALIGNMENT_CAP := -20000 - -# 修炼冷却时间 -var training_cooldown_left: float = 0.0 # 8~12 小时倒计时 - -# 判断技能是否处于宗师境界 (G1 ~ G10) -func can_train_skill(skill_level: int) -> bool: - return skill_level >= GRAND_MASTER_MIN_LEVEL and skill_level < PERFECT_MASTER_LEVEL - -# 计算升级所需善恶值 (40250 官方公式: 1000 + 500 * (level - 30),负道德翻倍) -func get_required_alignment(skill_level: int, current_alignment: int) -> int: - if not can_train_skill(skill_level): - return 0 - var base_cost = 1000 + 500 * (skill_level - GRAND_MASTER_MIN_LEVEL) - if current_alignment < 0: - return base_cost * 2 - return base_cost - -# 获取技能境界文本 (如 "G1", "G5", "P") -func get_skill_tier_text(level: int) -> String: - if level < GRAND_MASTER_MIN_LEVEL: - return "Normal/Master" - elif level >= PERFECT_MASTER_LEVEL: - return "P 阶 (Perfect Master)" - else: - return "G%d" % (level - GRAND_MASTER_MIN_LEVEL + 1) - -# 使用灵魂石闭关冲击宗师绝技 -func train_grandmaster_skill( - inventory: Array, - soul_stone_slot: int, - skill_id: int, - skill_name: String, - current_skill_level: int, - current_alignment: int, - has_exorcism_scroll: bool = false, - forced_success: bool = false, - forced_roll: int = -1 -) -> Dictionary: - # 检查技能是否在 G 阶 - if not can_train_skill(current_skill_level): - training_error.emit("INVALID_SKILL_LEVEL") - return { - "ok": false, - "reason": "INVALID_SKILL_LEVEL", - "msg": "只有达到宗师境界 (G1~G10) 的技能方可使用灵魂石闭关突破!" - } - - # 检查冷却 - if training_cooldown_left > 0.0 and not has_exorcism_scroll: - training_error.emit("COOLDOWN_ACTIVE") - return { - "ok": false, - "reason": "COOLDOWN_ACTIVE", - "msg": "心神尚未平复!需等待 %d 秒方可再次闭关,或使用【驱魔卷轴】重置心境。" % int(training_cooldown_left) - } - - # 检查灵魂石 - if soul_stone_slot < 0 or soul_stone_slot >= inventory.size() or inventory[soul_stone_slot] == null: - training_error.emit("NO_SOUL_STONE") - return {"ok": false, "reason": "NO_SOUL_STONE", "msg": "未找到灵魂石!"} - - var item = inventory[soul_stone_slot] - var vnum = int(item.get("vnum", 0)) - if vnum != VNUM_SOUL_STONE: - training_error.emit("NOT_A_SOUL_STONE") - return {"ok": false, "reason": "NOT_A_SOUL_STONE", "msg": "该物品并不是灵魂石!"} - - # 计算所需善恶值 - var need_align = get_required_alignment(current_skill_level, current_alignment) - - # 检查是否跌破道德底线 (40250: cur_alignment < -19000 + need_alignment) - if current_alignment < (MIN_ALIGNMENT_CAP + 1000 + need_align): - training_error.emit("ALIGNMENT_TOO_LOW") - return { - "ok": false, - "reason": "ALIGNMENT_TOO_LOW", - "msg": "你的罪孽太过深重,心灵已被心魔完全吞噬!必须行善恢复善恶值后方能参悟。" - } - - # 消耗灵魂石 (40250 pc.remove_item(50513)) - _consume_item_one(inventory, soul_stone_slot) - - # 设置下次领悟冷却时间 8~12 小时 (28800 ~ 43200 秒) - training_cooldown_left = randf_range(28800.0, 43200.0) - - # 判定成功概率 (40250 官方约为 30% ~ 35%) - var r = forced_roll if forced_roll > 0 else randi_range(1, 100) - var is_success = (r <= 30) or forced_success - - if is_success: - # 悟道成功 - var new_lvl = current_skill_level + 1 - var new_alignment = current_alignment - need_align - grandmaster_trained.emit(skill_id, skill_name, new_lvl, need_align) - - var tier_str = get_skill_tier_text(new_lvl) - var reach_p = (new_lvl >= PERFECT_MASTER_LEVEL) - var msg = "" - if reach_p: - msg = "金光贯顶,大道终成!你的【%s】突破桎梏,登峰造极达成了【%s】绝顶境界!" % [skill_name, tier_str] - else: - msg = "神识升华,灵智通达!你的【%s】成功精进为【%s】!消耗善恶值 %d 点。" % [skill_name, tier_str, need_align] - - return { - "ok": true, - "success": true, - "new_level": new_lvl, - "tier_text": tier_str, - "is_perfect_master": reach_p, - "alignment_cost": need_align, - "new_alignment": new_alignment, - "msg": msg - } - else: - # 走火入魔失败:扣除 1/3 ~ 1/2 的善恶值惩罚 - var penalty_ratio = randf_range(0.33, 0.50) - var penalty = max(1, int(floor(need_align * penalty_ratio))) - var new_alignment = current_alignment - penalty - grandmaster_failed.emit(skill_id, skill_name, penalty) - - return { - "ok": true, - "success": false, - "new_level": current_skill_level, - "tier_text": get_skill_tier_text(current_skill_level), - "is_perfect_master": false, - "alignment_cost": penalty, - "new_alignment": new_alignment, - "msg": "走火入魔!灵魂石在狂暴的灵气中化为碎粉,未能参悟真谛。惩罚扣除 %d 点善恶值!" % penalty - } - -# 使用驱魔卷轴清除冷却 -func use_exorcism_scroll() -> void: - training_cooldown_left = 0.0 - cooldown_cleared.emit() - -func update(delta: float) -> void: - if training_cooldown_left > 0.0: - training_cooldown_left = max(0.0, training_cooldown_left - delta) - -func _consume_item_one(inventory: Array, slot: int) -> void: - if slot < 0 or slot >= inventory.size() or inventory[slot] == null: - return - var item = inventory[slot] - var count = int(item.get("count", 1)) - if count > 1: - item["count"] = count - 1 - else: - inventory[slot] = null - -# 序列化与反序列化 -func serialize() -> Dictionary: - return { - "cooldown_left": training_cooldown_left - } - -func deserialize(data: Dictionary) -> void: - training_cooldown_left = float(data.get("cooldown_left", 0.0)) diff --git a/project/soul_stone_grandmaster_system.gd.uid b/project/soul_stone_grandmaster_system.gd.uid deleted file mode 100644 index a0c250f5..00000000 --- a/project/soul_stone_grandmaster_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b6domteea6j7t diff --git a/project/soulbound_lock_security_system.gd b/project/soulbound_lock_security_system.gd deleted file mode 100644 index 5b3b2835..00000000 --- a/project/soulbound_lock_security_system.gd +++ /dev/null @@ -1,169 +0,0 @@ -# soulbound_lock_security_system.gd —— Metin2 40250 神装灵魂绑定与防丢安全锁系统 1:1 -# 对照 40250 服务端 char_item.cpp, item.cpp, item_manager.cpp -class_name SoulboundLockSecuritySystem -extends RefCounted - -signal item_soulbound(slot: int, item_name: String) -signal unbinding_started(slot: int, item_name: String, duration_seconds: float) -signal unbinding_cancelled(slot: int, item_name: String) -signal item_unbound(slot: int, item_name: String) - -const VNUM_SOULBIND_SCROLL := 72051 # 灵魂绑定卷轴 -const VNUM_SOULUNBIND_SCROLL := 72052 # 灵魂解绑卷轴 -const DEFAULT_UNBIND_HOURS := 72.0 # 默认 72 小时解绑安全确认期 - -# 判定物品是否可在红名死亡时掉落 (灵魂绑定绝对免疫掉落) -static func can_drop_on_death(item: Dictionary) -> bool: - if item.is_empty(): - return false - if item.get("is_soulbound", false): - return false - return true - -# 判定物品是否允许丢弃至地面 -static func can_drop_to_ground(item: Dictionary) -> bool: - if item.is_empty(): - return false - if item.get("is_soulbound", false): - return false - return true - -# 判定物品是否允许出售给商店 -static func can_sell_to_shop(item: Dictionary) -> bool: - if item.is_empty(): - return false - if item.get("is_soulbound", false): - return false - return true - -# 判定物品是否允许放入摆摊货架或交易 -static func can_trade_or_stall(item: Dictionary) -> bool: - if item.is_empty(): - return false - if item.get("is_soulbound", false): - return false - return true - -# 为装备施加灵魂绑定 (72051) -func bind_item(scroll_slot: int, target_slot: int, inventory: Array) -> Dictionary: - if scroll_slot < 0 or scroll_slot >= inventory.size() or inventory[scroll_slot] == null: - return {"ok": false, "reason": "INVALID_SCROLL_SLOT"} - if target_slot < 0 or target_slot >= inventory.size() or inventory[target_slot] == null: - return {"ok": false, "reason": "INVALID_TARGET_SLOT"} - - var scroll = inventory[scroll_slot] - var target = inventory[target_slot] - - if int(scroll.get("vnum", 0)) != VNUM_SOULBIND_SCROLL: - return {"ok": false, "reason": "NOT_A_BIND_SCROLL", "msg": "此物品不是【灵魂绑定卷轴】!"} - - if target.get("is_soulbound", false): - return {"ok": false, "reason": "ALREADY_SOULBOUND", "msg": "该装备已处于灵魂绑定保护状态!"} - - # 消耗 1 个绑定卷轴 - var cnt = int(scroll.get("count", 1)) - if cnt > 1: - scroll["count"] = cnt - 1 - else: - inventory[scroll_slot] = null - - target["is_soulbound"] = true - target["unbind_remaining_time"] = 0.0 - - var iname: String = target.get("name", "神装") - item_soulbound.emit(target_slot, iname) - - return { - "ok": true, - "slot": target_slot, - "item_name": iname, - "msg": "【%s】已成功铭刻神龙灵魂烙印!永不掉落、永不遗失!" % iname - } - -# 发起解绑倒计时 (72052) -func start_unbinding(scroll_slot: int, target_slot: int, inventory: Array, duration_hours: float = DEFAULT_UNBIND_HOURS) -> Dictionary: - if scroll_slot < 0 or scroll_slot >= inventory.size() or inventory[scroll_slot] == null: - return {"ok": false, "reason": "INVALID_SCROLL_SLOT"} - if target_slot < 0 or target_slot >= inventory.size() or inventory[target_slot] == null: - return {"ok": false, "reason": "INVALID_TARGET_SLOT"} - - var scroll = inventory[scroll_slot] - var target = inventory[target_slot] - - if int(scroll.get("vnum", 0)) != VNUM_SOULUNBIND_SCROLL: - return {"ok": false, "reason": "NOT_AN_UNBIND_SCROLL", "msg": "此物品不是【灵魂解绑卷轴】!"} - - if not target.get("is_soulbound", false): - return {"ok": false, "reason": "NOT_SOULBOUND", "msg": "该装备尚未绑定,无需解绑!"} - - if float(target.get("unbind_remaining_time", 0.0)) > 0.0: - return {"ok": false, "reason": "UNBIND_ALREADY_IN_PROGRESS", "msg": "该装备已在解绑倒计时中!"} - - # 消耗解绑卷轴 - var cnt = int(scroll.get("count", 1)) - if cnt > 1: - scroll["count"] = cnt - 1 - else: - inventory[scroll_slot] = null - - var duration_seconds = duration_hours * 3600.0 - target["unbind_remaining_time"] = duration_seconds - - var iname: String = target.get("name", "神装") - unbinding_started.emit(target_slot, iname, duration_seconds) - - return { - "ok": true, - "slot": target_slot, - "item_name": iname, - "remaining_seconds": duration_seconds, - "msg": "【%s】进入解绑安全期,倒计时 %.1f 小时!在此期间随时可取消。" % [iname, duration_hours] - } - -# 取消解绑 (防误操作与防盗号保护) -func cancel_unbinding(target_slot: int, inventory: Array) -> Dictionary: - if target_slot < 0 or target_slot >= inventory.size() or inventory[target_slot] == null: - return {"ok": false, "reason": "INVALID_SLOT"} - - var target = inventory[target_slot] - if float(target.get("unbind_remaining_time", 0.0)) <= 0.0: - return {"ok": false, "reason": "NO_UNBIND_IN_PROGRESS", "msg": "该装备并未处于解绑流程中。"} - - target["unbind_remaining_time"] = 0.0 - var iname: String = target.get("name", "神装") - unbinding_cancelled.emit(target_slot, iname) - - return { - "ok": true, - "msg": "已终止【%s】的解绑流程,灵魂印记完好无损!" % iname - } - -# 倒计时结束完成解绑 -func finalize_unbinding(target_slot: int, inventory: Array) -> Dictionary: - if target_slot < 0 or target_slot >= inventory.size() or inventory[target_slot] == null: - return {"ok": false, "reason": "INVALID_SLOT"} - - var target = inventory[target_slot] - if not target.get("is_soulbound", false): - return {"ok": false, "reason": "NOT_SOULBOUND"} - - if float(target.get("unbind_remaining_time", 1.0)) > 0.0: - return {"ok": false, "reason": "COOLDOWN_NOT_FINISHED", "msg": "解绑安全冷却期尚未结束!"} - - target["is_soulbound"] = false - target["unbind_remaining_time"] = 0.0 - var iname: String = target.get("name", "神装") - item_unbound.emit(target_slot, iname) - - return { - "ok": true, - "msg": "【%s】灵魂烙印消散,已成功解绑!" % iname - } - -# 帧心跳更新解绑倒计时 -func update(delta: float, inventory: Array) -> void: - for item in inventory: - if item != null and item.get("is_soulbound", false): - var rem = float(item.get("unbind_remaining_time", 0.0)) - if rem > 0.0: - item["unbind_remaining_time"] = max(0.0, rem - delta) diff --git a/project/soulbound_lock_security_system.gd.uid b/project/soulbound_lock_security_system.gd.uid deleted file mode 100644 index 04959965..00000000 --- a/project/soulbound_lock_security_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bwqpervxrat04 diff --git a/project/spider_dungeon_key_system.gd b/project/spider_dungeon_key_system.gd deleted file mode 100644 index ddd9e7a9..00000000 --- a/project/spider_dungeon_key_system.gd +++ /dev/null @@ -1,238 +0,0 @@ -# spider_dungeon_key_system.gd —— Metin2 40250 经典深渊蜘蛛洞穴通行证与蛛后巢穴密匙系统 1:1 -# 对照 40250 服务端 spider_dungeon_3floor_boss.quest, char_item.cpp, dungeon.cpp -class_name SpiderDungeonKeySystem -extends RefCounted - -signal ticket_consumed_entry_granted(floor: int, map_name: String) -signal arachnid_key_crafted(key_count: int) -signal baroness_lair_opened(time_limit: float) -signal baroness_slain(drop_items: Array) -signal lair_closed(success: bool) - -# 40250 官方核心道具 Vnum -const VNUM_PASSAGE_TICKET := 71095 # 蛛洞通行证 (Passage Ticket) -const VNUM_ARACHNID_KEY := 30324 # 蛛网密匙 (Arachnid Key) - -# 40250 蛛网密匙合成原材料 -const VNUM_SPIDER_POISON_SACK := 30025 # 蜘蛛毒囊 -const VNUM_SPIDER_EGG_SACK := 30056 # 蜘蛛卵囊 -const VNUM_SPIDER_EYES := 30057 # 蜘蛛眼睛 -const VNUM_SPIDER_LEGS := 30058 # 蜘蛛腿 -const CRAFT_COST_GOLD := 50000 # 50,000 金币 - -# NPC Vnum -const NPC_SPIDER_GUARD := 20089 # 蛛洞入口守卫 -const NPC_SCHOLAR_URIEL:= 20011 # 学者乌列尔 (合成密匙) -const NPC_LAIR_TOMBSTONE:= 30130 # 蛛后巢穴封印石碑 - -# Boss Vnum -const MOB_SPIDER_BARONESS := 2092 # 蜘蛛女皇/蛛后 -const VNUM_CHEST_BARONESS := 50181 # 蛛后宝箱 - -# 巢穴限时 (20 分钟 / 1200 秒) -const LAIR_DURATION := 1200.0 - -var is_lair_active: bool = false -var lair_time_remaining: float = 0.0 -var baroness_alive: bool = false - -# 使用通行证进入蜘蛛洞穴 2层或3层 -func enter_spider_dungeon(floor_target: int, player_level: int, inventory: Array) -> Dictionary: - if floor_target != 2 and floor_target != 3: - return {"ok": false, "reason": "INVALID_FLOOR", "msg": "只能前往蛛洞二层(SD2)或三层(SD3)!"} - - var min_lv = 40 if floor_target == 2 else 50 - if player_level < min_lv: - return { - "ok": false, - "reason": "LEVEL_TOO_LOW", - "msg": "实力不足!进入蛛洞 %d 层需要等级达到 %d 级。" % [floor_target, min_lv] - } - - # 寻找并消耗 1 张通行证 (71095) - var ticket_slot := -1 - for i in range(inventory.size()): - if inventory[i] != null and int(inventory[i].get("vnum", 0)) == VNUM_PASSAGE_TICKET: - ticket_slot = i - break - - if ticket_slot == -1: - return {"ok": false, "reason": "NO_PASSAGE_TICKET", "msg": "守卫拦住了去路!需要出示【蛛洞通行证】(71095)。"} - - var t_item: Dictionary = inventory[ticket_slot] - var cnt := int(t_item.get("count", 1)) - if cnt > 1: - t_item["count"] = cnt - 1 - else: - inventory[ticket_slot] = null - - var target_map = "Spider_Dungeon_2" if floor_target == 2 else "Spider_Dungeon_3" - ticket_consumed_entry_granted.emit(floor_target, target_map) - - return { - "ok": true, - "floor": floor_target, - "target_map": target_map, - "msg": "检票通过!守卫为您开启了通往【蛛洞 %d 层】的隐秘石门。" % floor_target - } - -# 在学者乌列尔处精炼合成蛛网密匙 (40250 spider_dungeon_3floor_boss.quest) -func craft_arachnid_key(inventory: Array, player_gold: Dictionary) -> Dictionary: - var current_gold := int(player_gold.get("gold", 0)) - if current_gold < CRAFT_COST_GOLD: - return {"ok": false, "reason": "NOT_ENOUGH_GOLD", "msg": "精炼费用不足!需要 %d 金币。" % CRAFT_COST_GOLD} - - # 检查 4 大蜘蛛残肢素材 - var req_vnums = [VNUM_SPIDER_POISON_SACK, VNUM_SPIDER_EGG_SACK, VNUM_SPIDER_EYES, VNUM_SPIDER_LEGS] - var slots_to_consume: Array = [] - - for req_vnum in req_vnums: - var found_slot := -1 - for i in range(inventory.size()): - if inventory[i] != null and int(inventory[i].get("vnum", 0)) == req_vnum: - found_slot = i - break - if found_slot == -1: - return {"ok": false, "reason": "MISSING_MATERIALS", "msg": "合成材料不足!缺少蜘蛛残肢核心原料。"} - slots_to_consume.append(found_slot) - - # 寻找放置生成密匙的空位 - var free_slot := -1 - for i in range(inventory.size()): - if inventory[i] == null: - free_slot = i - break - - # 如果没有全新空位,但材料消耗后会腾出空位也是可行的 - if free_slot == -1: - for s in slots_to_consume: - if int(inventory[s].get("count", 1)) == 1: - free_slot = s # 可以复用该格 - break - - if free_slot == -1: - return {"ok": false, "reason": "INVENTORY_FULL", "msg": "背包已满,无法容纳新炼制的密匙!"} - - # 消耗材料与金币 - player_gold["gold"] = current_gold - CRAFT_COST_GOLD - for s in slots_to_consume: - var it: Dictionary = inventory[s] - var c := int(it.get("count", 1)) - if c > 1: - it["count"] = c - 1 - else: - inventory[s] = null - - # 产出蛛网密匙 (30324) - inventory[free_slot] = { - "vnum": VNUM_ARACHNID_KEY, - "name": "蛛网密匙", - "count": 1 - } - - arachnid_key_crafted.emit(1) - - return { - "ok": true, - "key_vnum": VNUM_ARACHNID_KEY, - "slot": free_slot, - "msg": "学者乌列尔成功为您炼制了【蛛网密匙】(30324)!" - } - -# 封印石碑检票开启蛛后巢穴 (40250 NPC 30130) -func open_baroness_lair(player_level: int, inventory: Array) -> Dictionary: - if is_lair_active: - return {"ok": false, "reason": "LAIR_ALREADY_ACTIVE", "msg": "蛛后巢穴已在挑战中!"} - - if player_level < 50: - return {"ok": false, "reason": "LEVEL_TOO_LOW", "msg": "挑战蛛后需要角色等级达到 50 级!"} - - # 寻找并消耗蛛网密匙 (30324) - var key_slot := -1 - for i in range(inventory.size()): - if inventory[i] != null and int(inventory[i].get("vnum", 0)) == VNUM_ARACHNID_KEY: - key_slot = i - break - - if key_slot == -1: - return {"ok": false, "reason": "NO_ARACHNID_KEY", "msg": "封印石碑毫无反应!需要镶嵌【蛛网密匙】(30324)。"} - - # 消耗钥匙 - var k_item: Dictionary = inventory[key_slot] - var cnt := int(k_item.get("count", 1)) - if cnt > 1: - k_item["count"] = cnt - 1 - else: - inventory[key_slot] = null - - is_lair_active = true - lair_time_remaining = LAIR_DURATION - baroness_alive = true - - baroness_lair_opened.emit(LAIR_DURATION) - - return { - "ok": true, - "duration": LAIR_DURATION, - "boss_vnum": MOB_SPIDER_BARONESS, - "msg": "石碑封印破除!蛛网密室大门洞开,限时 20 分钟讨伐蜘蛛女皇!" - } - -# 击败蜘蛛女皇 -func slay_baroness(inventory: Array) -> Dictionary: - if not is_lair_active or not baroness_alive: - return {"ok": false, "reason": "BARONESS_NOT_ALIVE"} - - baroness_alive = false - - var drops: Array = [ - {"vnum": VNUM_CHEST_BARONESS, "name": "蛛后宝箱", "count": 1}, - {"vnum": 25040, "name": "祝福卷轴", "count": 2} - ] - - # 放入背包 - for drop in drops: - for i in range(inventory.size()): - if inventory[i] == null: - inventory[i] = drop.duplicate(true) - break - - baroness_slain.emit(drops) - - # 胜利结束 - close_lair(true) - - return { - "ok": true, - "drops": drops, - "msg": "诛灭深渊蛛后!成功夺得【蛛后宝箱】!" - } - -# 关闭巢穴副本 -func close_lair(success: bool) -> void: - is_lair_active = false - lair_time_remaining = 0.0 - baroness_alive = false - lair_closed.emit(success) - -# 时钟心跳推进 -func update(delta: float) -> void: - if not is_lair_active: - return - lair_time_remaining -= delta - if lair_time_remaining <= 0.0: - close_lair(false) # 超时失败关闭 - -# 序列化 -func serialize() -> Dictionary: - return { - "is_lair_active": is_lair_active, - "lair_time_remaining": lair_time_remaining, - "baroness_alive": baroness_alive - } - -# 反序列化 -func deserialize(data: Dictionary) -> void: - is_lair_active = bool(data.get("is_lair_active", false)) - lair_time_remaining = float(data.get("lair_time_remaining", 0.0)) - baroness_alive = bool(data.get("baroness_alive", false)) diff --git a/project/spider_dungeon_key_system.gd.uid b/project/spider_dungeon_key_system.gd.uid deleted file mode 100644 index 476c476b..00000000 --- a/project/spider_dungeon_key_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://chv8kl8y62arl diff --git a/project/spider_dungeon_system.gd b/project/spider_dungeon_system.gd deleted file mode 100644 index 822dfaaa..00000000 --- a/project/spider_dungeon_system.gd +++ /dev/null @@ -1,161 +0,0 @@ -# spider_dungeon_system.gd —— Metin2 40250 蜘蛛洞穴三层与男爵夫人巢穴 1:1 -# 对照 40250 服务端 spider_dungeon_3.quest, dungeon.cpp, Boss 2094 & Mob 2095 -class_name SpiderDungeonSystem -extends RefCounted - -signal dungeon_started(duration: float) -signal egg_destroyed(eggs_left: int, current_atk: int, current_def: int) -signal minions_summoned() -signal dungeon_cleared(drops: Array[Dictionary]) -signal dungeon_failed(reason: String) - -const NPC_GRAVESTONE := 20371 # 守墓人碑文 -const ITEM_ARACHNID_KEY := 30327 # 蜘蛛之钥 -const BOSS_SPIDER_BARONESS := 2094 # 蜘蛛男爵夫人 -const MOB_SPIDER_EGG := 2095 # 蜘蛛卵 -const ITEM_BARONESS_CHEST := 50157 # 男爵宝箱 -const ITEM_TITAN_SHIELD := 13129 # 泰坦盾牌+9 - -const REQUIRED_LEVEL := 50 -const DUNGEON_TIME_LIMIT := 1200.0 # 20 分钟 (秒) - -const BASE_BARONESS_MAX_HP := 250000 -const BASE_BARONESS_DEF := 400 -const BASE_BARONESS_ATK := 800 - -var is_active: bool = false -var remaining_time: float = 0.0 -var eggs_remaining: int = 4 -var baroness_hp: int = 0 -var baroness_max_hp: int = BASE_BARONESS_MAX_HP -var baroness_def: int = BASE_BARONESS_DEF -var baroness_atk: int = BASE_BARONESS_ATK -var minions_spawned: bool = false -var dungeon_completed: bool = false - -# 开启地牢密室 (spider_dungeon_3.quest) -func enter_dungeon(player_level: int, inventory: Array) -> Dictionary: - if is_active: - return {"ok": false, "reason": "ALREADY_ACTIVE", "msg": "地牢密室已经在进行中!"} - if player_level < REQUIRED_LEVEL: - return {"ok": false, "reason": "LEVEL_TOO_LOW", "msg": "等级未满 50 级,无法承受蜘蛛巢穴的剧毒!"} - - # 查找并扣除蜘蛛之钥 (30327) - var key_slot := -1 - for i in range(inventory.size()): - var it = inventory[i] - if it != null and int(it.get("vnum", 0)) == ITEM_ARACHNID_KEY: - key_slot = i - break - - if key_slot == -1: - return {"ok": false, "reason": "MISSING_KEY", "msg": "进入蜘蛛男爵夫人巢穴需要消耗【蜘蛛钥匙】!"} - - inventory[key_slot] = null - - # 初始化地牢状态 - is_active = true - remaining_time = DUNGEON_TIME_LIMIT - eggs_remaining = 4 - baroness_max_hp = BASE_BARONESS_MAX_HP - baroness_hp = baroness_max_hp - baroness_def = BASE_BARONESS_DEF - baroness_atk = BASE_BARONESS_ATK - minions_spawned = false - dungeon_completed = false - - dungeon_started.emit(remaining_time) - - return { - "ok": true, - "time_limit": remaining_time, - "eggs_count": eggs_remaining, - "baroness_hp": baroness_hp, - "baroness_atk": baroness_atk, - "baroness_def": baroness_def, - "msg": "密室封印已破除!蜘蛛男爵夫人苏醒,你有 20 分钟时间击杀它!" - } - -# 击碎一枚蜘蛛卵 (狂暴激怒机制: 防御 -15%, 攻击 +20%) -func destroy_egg() -> Dictionary: - if not is_active: - return {"ok": false, "reason": "DUNGEON_NOT_ACTIVE"} - if eggs_remaining <= 0: - return {"ok": false, "reason": "NO_EGGS_LEFT", "msg": "所有蜘蛛卵均已被击碎!"} - - eggs_remaining -= 1 - var eggs_broken := 4 - eggs_remaining - - # 每碎 1 卵:防御降低 15% (最高 -60%), 攻击提升 20% (最高 +80%) - var def_reduction_pct: float = float(eggs_broken) * 0.15 - var atk_bonus_pct: float = float(eggs_broken) * 0.20 - - baroness_def = int(round(float(BASE_BARONESS_DEF) * (1.0 - def_reduction_pct))) - baroness_atk = int(round(float(BASE_BARONESS_ATK) * (1.0 + atk_bonus_pct))) - - egg_destroyed.emit(eggs_remaining, baroness_atk, baroness_def) - - return { - "ok": true, - "eggs_left": eggs_remaining, - "eggs_broken": eggs_broken, - "baroness_def": baroness_def, - "baroness_atk": baroness_atk, - "msg": "蜘蛛卵被击碎!男爵夫人防御大幅削弱,但陷入了极度狂暴!" - } - -# 攻击男爵夫人 -func damage_baroness(raw_damage: int) -> Dictionary: - if not is_active: - return {"ok": false, "reason": "DUNGEON_NOT_ACTIVE"} - if dungeon_completed: - return {"ok": false, "reason": "ALREADY_COMPLETED"} - - var eff_dmg: int = maxi(1, raw_damage - baroness_def) - baroness_hp = maxi(0, baroness_hp - eff_dmg) - - # 50% 血量召唤蜘蛛群 - var triggered_minions := false - if not minions_spawned and baroness_hp <= int(baroness_max_hp * 0.5): - minions_spawned = true - triggered_minions = true - minions_summoned.emit() - - if baroness_hp <= 0: - dungeon_completed = true - is_active = false - var drops: Array[Dictionary] = [ - {"vnum": ITEM_BARONESS_CHEST, "count": 1, "name": "男爵宝箱"}, - {"vnum": ITEM_TITAN_SHIELD, "count": 1, "name": "泰坦盾牌+9"} - ] - dungeon_cleared.emit(drops) - return { - "ok": true, - "dead": true, - "baroness_hp": 0, - "damage_dealt": eff_dmg, - "drops": drops, - "msg": "蜘蛛男爵夫人轰然倒地!巢穴通关!" - } - - return { - "ok": true, - "dead": false, - "baroness_hp": baroness_hp, - "damage_dealt": eff_dmg, - "minions_summoned": triggered_minions - } - -# 逐帧更新倒计时 -func update(dt: float) -> Dictionary: - if not is_active: - return {} - - remaining_time -= dt - if remaining_time <= 0.0: - remaining_time = 0.0 - is_active = false - dungeon_failed.emit("TIME_OUT") - return {"failed": true, "reason": "TIME_OUT", "msg": "挑战超时!你已被移出蜘蛛男爵夫人巢穴!"} - - return {"failed": false, "remaining_time": remaining_time} diff --git a/project/spider_dungeon_system.gd.uid b/project/spider_dungeon_system.gd.uid deleted file mode 100644 index 752ed3bf..00000000 --- a/project/spider_dungeon_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bi2m3ypw6w5ls diff --git a/project/status_skill_reset_system.gd b/project/status_skill_reset_system.gd deleted file mode 100644 index 1cdd117f..00000000 --- a/project/status_skill_reset_system.gd +++ /dev/null @@ -1,183 +0,0 @@ -# status_skill_reset_system.gd —— Metin2 40250 官方属性重修卷轴与忘却之书洗点系统 1:1 -# 对照 40250 服务端 char_item.cpp, char.cpp, root/uipointreset.py -class_name StatusSkillResetSystem -extends RefCounted - -signal status_reset(stat_name: String, refunded_points: int) -signal all_status_reset(refunded_points: int) -signal skill_point_reset(skill_id: int, new_level: int) -signal all_skills_reset(refunded_points: int) - -# 40250 官方重修道具 Vnum -const VNUM_SKILL_RESET_SCROLL := 71001 # 技能重置卷轴 (全部技能洗点) -const VNUM_STATUS_REDIST_SCROLL := 71002 # 属性重修卷轴 (单项四维属性洗点) -const VNUM_ALL_STATUS_RESET_SCROLL := 71003 # 全属性重置卷轴 (血统重塑卷轴) - -# 40250 四维职业初始基础属性标准表 (Lv 1 Base) -const JOB_BASE_STATS: Dictionary = { - "warrior": {"vit": 16, "int": 3, "str": 15, "dex": 5}, - "assassin": {"vit": 14, "int": 3, "str": 8, "dex": 16}, - "sura": {"vit": 15, "int": 16, "str": 8, "dex": 3}, - "shaman": {"vit": 15, "int": 16, "str": 5, "dex": 6} -} - -# 单项属性重修 (71002 属性重修卷轴) -func reset_single_status(stat_name: String, player_data: Dictionary, inventory: Array, item_slot: int) -> Dictionary: - var valid_stats = ["vit", "int", "str", "dex"] - if not valid_stats.has(stat_name): - return {"ok": false, "reason": "INVALID_STAT_NAME", "msg": "无效的属性名称!"} - - if item_slot < 0 or item_slot >= inventory.size() or inventory[item_slot] == null: - return {"ok": false, "reason": "INVALID_SLOT"} - - var item: Dictionary = inventory[item_slot] - if int(item.get("vnum", 0)) != VNUM_STATUS_REDIST_SCROLL: - return {"ok": false, "reason": "NOT_A_STATUS_REDIST_SCROLL", "msg": "需要使用【属性重修卷轴】(71002)!"} - - var job: String = str(player_data.get("job", "warrior")) - var base_stat_table: Dictionary = JOB_BASE_STATS.get(job, JOB_BASE_STATS["warrior"]) - var base_val: int = int(base_stat_table.get(stat_name, 10)) - var cur_val: int = int(player_data.get(stat_name, base_val)) - - if cur_val <= base_val: - return {"ok": false, "reason": "STAT_ALREADY_AT_BASE", "msg": "该属性尚未分配任何点数,无需重置!"} - - var refunded: int = cur_val - base_val - - # 消耗 1 卷 - var cnt := int(item.get("count", 1)) - if cnt > 1: - item["count"] = cnt - 1 - else: - inventory[item_slot] = null - - player_data[stat_name] = base_val - player_data["unused_status_points"] = int(player_data.get("unused_status_points", 0)) + refunded - - status_reset.emit(stat_name, refunded) - - return { - "ok": true, - "stat_name": stat_name, - "refunded_points": refunded, - "new_val": base_val, - "total_unused": player_data["unused_status_points"], - "msg": "成功重置【%s】,全额退还 %d 点属性点!" % [stat_name.to_upper(), refunded] - } - -# 全属性重置 (71003 全属性重置卷轴) -func reset_all_status(player_data: Dictionary, inventory: Array, item_slot: int) -> Dictionary: - if item_slot < 0 or item_slot >= inventory.size() or inventory[item_slot] == null: - return {"ok": false, "reason": "INVALID_SLOT"} - - var item: Dictionary = inventory[item_slot] - if int(item.get("vnum", 0)) != VNUM_ALL_STATUS_RESET_SCROLL: - return {"ok": false, "reason": "NOT_AN_ALL_STATUS_RESET_SCROLL", "msg": "需要使用【全属性重置卷轴】(71003)!"} - - var job: String = str(player_data.get("job", "warrior")) - var base_stat_table: Dictionary = JOB_BASE_STATS.get(job, JOB_BASE_STATS["warrior"]) - - var total_refunded := 0 - for stat_name in ["vit", "int", "str", "dex"]: - var base_val: int = int(base_stat_table.get(stat_name, 10)) - var cur_val: int = int(player_data.get(stat_name, base_val)) - if cur_val > base_val: - total_refunded += (cur_val - base_val) - player_data[stat_name] = base_val - - # 消耗 1 卷 - var cnt := int(item.get("count", 1)) - if cnt > 1: - item["count"] = cnt - 1 - else: - inventory[item_slot] = null - - player_data["unused_status_points"] = int(player_data.get("unused_status_points", 0)) + total_refunded - - all_status_reset.emit(total_refunded) - - return { - "ok": true, - "refunded_points": total_refunded, - "total_unused": player_data["unused_status_points"], - "msg": "血统重塑完成!四维属性恢复初始基础值,共退还 %d 点属性点。" % total_refunded - } - -# 忘却之书重置单项技能 1 点 (50311~50320 Book of Oblivion) -func reset_single_skill_point(skill_id: int, player_skills: Dictionary, player_data: Dictionary, inventory: Array, item_slot: int) -> Dictionary: - if item_slot < 0 or item_slot >= inventory.size() or inventory[item_slot] == null: - return {"ok": false, "reason": "INVALID_SLOT"} - - var book: Dictionary = inventory[item_slot] - var book_vnum := int(book.get("vnum", 0)) - var book_target_skill := int(book.get("target_skill", 0)) - - # 检查是否为忘却之书 (50311~50330) - if book_vnum < 50311 or book_vnum > 50330: - return {"ok": false, "reason": "NOT_A_BOOK_OF_OBLIVION", "msg": "需要使用对应的【忘却之书】!"} - - if book_target_skill != 0 and book_target_skill != skill_id: - return {"ok": false, "reason": "SKILL_ID_MISMATCH", "msg": "此忘却之书与目标技能不符!"} - - var cur_skill_lv := int(player_skills.get(skill_id, 0)) - if cur_skill_lv <= 0: - return {"ok": false, "reason": "SKILL_LEVEL_ZERO", "msg": "该技能尚未习得,无法忘却!"} - - # 40250 官方规则:技能达到 Master (>= 20) 后无法使用普通忘却之书降级 - if cur_skill_lv >= 20: - return {"ok": false, "reason": "SKILL_IS_MASTER", "msg": "已达到大师境界(Master)的技能无法通过普通忘却之书降级!"} - - # 消耗 1 本忘却之书 - var cnt := int(book.get("count", 1)) - if cnt > 1: - book["count"] = cnt - 1 - else: - inventory[item_slot] = null - - player_skills[skill_id] = cur_skill_lv - 1 - player_data["unused_skill_points"] = int(player_data.get("unused_skill_points", 0)) + 1 - - skill_point_reset.emit(skill_id, player_skills[skill_id]) - - return { - "ok": true, - "skill_id": skill_id, - "new_level": player_skills[skill_id], - "refunded_points": 1, - "total_unused": player_data["unused_skill_points"], - "msg": "领悟忘却之书!技能等级降低 1 级,返还 1 点可用技能点。" - } - -# 技能重置卷轴全技能洗点 (71001) -func reset_all_skills(player_skills: Dictionary, player_data: Dictionary, inventory: Array, item_slot: int) -> Dictionary: - if item_slot < 0 or item_slot >= inventory.size() or inventory[item_slot] == null: - return {"ok": false, "reason": "INVALID_SLOT"} - - var item: Dictionary = inventory[item_slot] - if int(item.get("vnum", 0)) != VNUM_SKILL_RESET_SCROLL: - return {"ok": false, "reason": "NOT_A_SKILL_RESET_SCROLL", "msg": "需要使用【技能重置卷轴】(71001)!"} - - var total_refunded := 0 - for sk_id in player_skills.keys(): - var lv := int(player_skills[sk_id]) - if lv > 0: - total_refunded += lv - player_skills[sk_id] = 0 - - # 消耗 1 卷 - var cnt := int(item.get("count", 1)) - if cnt > 1: - item["count"] = cnt - 1 - else: - inventory[item_slot] = null - - player_data["unused_skill_points"] = int(player_data.get("unused_skill_points", 0)) + total_refunded - - all_skills_reset.emit(total_refunded) - - return { - "ok": true, - "refunded_points": total_refunded, - "total_unused": player_data["unused_skill_points"], - "msg": "技能重置完成!所有技能点已洗净,全额返还 %d 点可用技能点。" % total_refunded - } diff --git a/project/status_skill_reset_system.gd.uid b/project/status_skill_reset_system.gd.uid deleted file mode 100644 index 1758c258..00000000 --- a/project/status_skill_reset_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dynnpji6kpo1j diff --git a/project/system_setting_system.gd b/project/system_setting_system.gd deleted file mode 100644 index 078bedd7..00000000 --- a/project/system_setting_system.gd +++ /dev/null @@ -1,147 +0,0 @@ -# system_setting_system.gd —— Metin2 40250 音频视听与战斗选项系统 1:1 -# 对照 40250 服务端与客户端 PythonSystem.cpp, uisystemoption.py, uigameoption.py -class_name SystemSettingSystem -extends RefCounted - -signal setting_changed(setting_name: String, new_val: Variant) -signal pk_mode_changed(new_mode: int, mode_name: String) - -enum PkMode { - PEACE = 0, # 和平 - REVENGE = 1, # 善恶 (仅可攻击负善恶红名) - GUILD = 2, # 阵营 (公会战保护) - FREE = 3 # 自由 (无限制) -} - -# 音频设置 -var bgm_volume: float = 0.8: - set(v): - bgm_volume = clampf(v, 0.0, 1.0) - setting_changed.emit("bgm_volume", bgm_volume) - -var sfx_volume: float = 0.8: - set(v): - sfx_volume = clampf(v, 0.0, 1.0) - setting_changed.emit("sfx_volume", sfx_volume) - -var is_muted: bool = false: - set(v): - is_muted = v - setting_changed.emit("is_muted", is_muted) - -# 视距与画质设置 -var camera_distance: float = 25.0: - set(v): - camera_distance = clampf(v, 15.0, 40.0) - setting_changed.emit("camera_distance", camera_distance) - -var fog_level: int = 1: # 0: 无, 1: 近, 2: 远 - set(v): - fog_level = clampi(v, 0, 2) - setting_changed.emit("fog_level", fog_level) - -var shadow_quality: int = 1: # 0: 关, 1: 地面, 2: 全部 - set(v): - shadow_quality = clampi(v, 0, 2) - setting_changed.emit("shadow_quality", shadow_quality) - -# 战斗飘字与标签显示 -var show_damage_dealt: bool = true -var show_damage_received: bool = true -var show_critical_hit: bool = true -var show_monster_names: bool = true -var show_player_names: bool = true - -# PK 模式 -var current_pk_mode: int = PkMode.PEACE - -# 设置 PK 模式 -func set_pk_mode(mode: int) -> Dictionary: - if mode < PkMode.PEACE or mode > PkMode.FREE: - return {"ok": false, "reason": "INVALID_PK_MODE", "msg": "无效的 PK 模式!"} - - current_pk_mode = mode - var mname := get_pk_mode_name(mode) - pk_mode_changed.emit(mode, mname) - - return { - "ok": true, - "mode": mode, - "mode_name": mname, - "msg": "PK 模式已切换为【%s】!" % mname - } - -# 获取模式中文名称 -func get_pk_mode_name(mode: int) -> String: - match mode: - PkMode.PEACE: return "和平模式" - PkMode.REVENGE: return "善恶反击模式" - PkMode.GUILD: return "阵营公会模式" - PkMode.FREE: return "自由对决模式" - _: return "未知模式" - -# 判定是否可以对指定目标发起攻击 (PK 判定状态机) -func can_attack_target( - target_is_player: bool, - target_alignment: int = 0, - target_guild_id: int = 0, - my_guild_id: int = 0 -) -> bool: - # 怪物/NPC 永远可以攻击 - if not target_is_player: - return true - - # 玩家间攻击依据 PK 模式判定 - match current_pk_mode: - PkMode.PEACE: - # 和平模式:严禁攻击任何玩家 - return false - - PkMode.REVENGE: - # 善恶模式:只可攻击红名玩家 (善恶值 < 0) - return target_alignment < 0 - - PkMode.GUILD: - # 阵营模式:同公会玩家受到保护,禁止攻击 - if my_guild_id > 0 and target_guild_id > 0 and my_guild_id == target_guild_id: - return false - return true - - PkMode.FREE: - # 自由模式:可攻击任何玩家 - return true - - _: - return false - -# 导出配置项 -func export_settings() -> Dictionary: - return { - "bgm_volume": bgm_volume, - "sfx_volume": sfx_volume, - "is_muted": is_muted, - "camera_distance": camera_distance, - "fog_level": fog_level, - "shadow_quality": shadow_quality, - "show_damage_dealt": show_damage_dealt, - "show_damage_received": show_damage_received, - "show_critical_hit": show_critical_hit, - "show_monster_names": show_monster_names, - "show_player_names": show_player_names, - "current_pk_mode": current_pk_mode - } - -# 导入配置项 -func import_settings(cfg: Dictionary) -> void: - if cfg.has("bgm_volume"): bgm_volume = float(cfg["bgm_volume"]) - if cfg.has("sfx_volume"): sfx_volume = float(cfg["sfx_volume"]) - if cfg.has("is_muted"): is_muted = bool(cfg["is_muted"]) - if cfg.has("camera_distance"): camera_distance = float(cfg["camera_distance"]) - if cfg.has("fog_level"): fog_level = int(cfg["fog_level"]) - if cfg.has("shadow_quality"): shadow_quality = int(cfg["shadow_quality"]) - if cfg.has("show_damage_dealt"): show_damage_dealt = bool(cfg["show_damage_dealt"]) - if cfg.has("show_damage_received"): show_damage_received = bool(cfg["show_damage_received"]) - if cfg.has("show_critical_hit"): show_critical_hit = bool(cfg["show_critical_hit"]) - if cfg.has("show_monster_names"): show_monster_names = bool(cfg["show_monster_names"]) - if cfg.has("show_player_names"): show_player_names = bool(cfg["show_player_names"]) - if cfg.has("current_pk_mode"): set_pk_mode(int(cfg["current_pk_mode"])) diff --git a/project/system_setting_system.gd.uid b/project/system_setting_system.gd.uid deleted file mode 100644 index a25df907..00000000 --- a/project/system_setting_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dycl43xkqqoa3 diff --git a/project/test_alignment_parity.gd b/project/test_alignment_parity.gd deleted file mode 100644 index ab3f6be3..00000000 --- a/project/test_alignment_parity.gd +++ /dev/null @@ -1,102 +0,0 @@ -# test_alignment_parity.gd —— 验证方向 2:人物善恶道德与红名系统 40250 1:1 对齐 -extends SceneTree - -const AlignmentSystem = preload("res://alignment_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_alignment_parity (Direction 2: Karma & Alignment 1:1) ===") - test_alignment_grades_and_thresholds() - test_title_colors_and_names() - test_natural_time_recovery() - test_kill_monster_karma() - test_zen_bean_karma_clearing() - - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed > 0: - printerr("TEST FAILED!") - quit(1) - else: - print("ALL TESTS PASSED!") - quit(0) - -func test_alignment_grades_and_thresholds() -> void: - print("\n--- Test 1: 9 Alignment Grades & Numerical Thresholds ---") - _assert(AlignmentSystem.get_alignment_grade(15000) == AlignmentSystem.TITLE_HEROIC, "15000 is Heroic (>= 12000)") - _assert(AlignmentSystem.get_alignment_grade(12000) == AlignmentSystem.TITLE_HEROIC, "12000 is Heroic (boundary)") - _assert(AlignmentSystem.get_alignment_grade(9500) == AlignmentSystem.TITLE_NOBLE, "9500 is Noble (8000..11999)") - _assert(AlignmentSystem.get_alignment_grade(5000) == AlignmentSystem.TITLE_GOOD, "5000 is Good (4000..7999)") - _assert(AlignmentSystem.get_alignment_grade(2000) == AlignmentSystem.TITLE_FRIENDLY, "2000 is Friendly (1000..3999)") - _assert(AlignmentSystem.get_alignment_grade(500) == AlignmentSystem.TITLE_NEUTRAL, "500 is Neutral (0..999)") - _assert(AlignmentSystem.get_alignment_grade(0) == AlignmentSystem.TITLE_NEUTRAL, "0 is Neutral (boundary)") - _assert(AlignmentSystem.get_alignment_grade(-2000) == AlignmentSystem.TITLE_AGGRESSIVE, "-2000 is Aggressive (-1..-3999)") - _assert(AlignmentSystem.get_alignment_grade(-5000) == AlignmentSystem.TITLE_FRAUDULENT, "-5000 is Fraudulent (-4000..-7999)") - _assert(AlignmentSystem.get_alignment_grade(-10000) == AlignmentSystem.TITLE_MALICIOUS, "-10000 is Malicious (-8000..-11999)") - _assert(AlignmentSystem.get_alignment_grade(-15000) == AlignmentSystem.TITLE_CRUEL, "-15000 is Cruel (<= -12000)") - -func test_title_colors_and_names() -> void: - print("\n--- Test 2: Title Names and 40250 RGB Colors ---") - _assert(AlignmentSystem.get_title_name(AlignmentSystem.TITLE_HEROIC) == "英雄", "Grade 0 is 英雄") - _assert(AlignmentSystem.get_title_name(AlignmentSystem.TITLE_CRUEL) == "魔头", "Grade 8 is 魔头") - - var c_heroic := AlignmentSystem.get_title_color(AlignmentSystem.TITLE_HEROIC) - _assert(c_heroic == Color8(0, 204, 255), "Heroic color matches 40250 Color8(0, 204, 255) cyan") - - var c_cruel := AlignmentSystem.get_title_color(AlignmentSystem.TITLE_CRUEL) - _assert(c_cruel == Color8(255, 0, 0), "Cruel color matches 40250 Color8(255, 0, 0) red") - - _assert(AlignmentSystem.is_red_name(-100), "-100 is red name (evil)") - _assert(not AlignmentSystem.is_red_name(0), "0 is not red name") - _assert(AlignmentSystem.is_blue_name(1500), "1500 is blue name (just)") - -func test_natural_time_recovery() -> void: - print("\n--- Test 3: 40250 Online Natural Time Recovery (char.cpp:1197) ---") - # 1. Evil player (-1000) after 10 minutes -> +60 * 10 = +600 -> -400 - var evil_rec := AlignmentSystem.calculate_time_recovery(-1000, 10, false) - _assert(evil_rec == -400, "Evil karma recovered from -1000 to -400 (+60/min)") - - # 2. Evil player with fast item (120/min) after 5 minutes -> +120 * 5 = +600 -> -400 - var fast_rec := AlignmentSystem.calculate_time_recovery(-1000, 5, true) - _assert(fast_rec == -400, "Evil karma with fast item recovered +120/min") - - # 3. Evil natural recovery clamps at 0 (does not automatically become positive just by AFK) - var clamp_rec := AlignmentSystem.calculate_time_recovery(-100, 10, false) - _assert(clamp_rec == 0, "Evil time recovery clamps at 0") - - # 4. Good player (1000) after 10 minutes -> +5 * 10 = +50 -> 1050 - var good_rec := AlignmentSystem.calculate_time_recovery(1000, 10, false) - _assert(good_rec == 1050, "Good karma increases +5/min") - -func test_kill_monster_karma() -> void: - print("\n--- Test 4: Monster Kill Karma Growth ---") - var normal_kill := AlignmentSystem.on_kill_monster(500, 30, 20) # 30 - 10 = 20 - _assert(normal_kill == 520, "Monster kill added 20 alignment") - - # Evil players get double recovery rate when killing monsters - var evil_kill := AlignmentSystem.on_kill_monster(-1000, 30, 20) # 20 * 2 = 40 - _assert(evil_kill == -960, "Evil player gets 2x karma on monster kill") - -func test_zen_bean_karma_clearing() -> void: - print("\n--- Test 5: Zen Bean (70102) Karma Clearing ---") - # Zen bean on evil player (-800) clears 500 karma -> -300 - var res_bean := AlignmentSystem.use_zen_bean(-800) - _assert(res_bean.ok and res_bean.new_alignment == -300, "Zen bean reduced evil karma from -800 to -300") - - # Zen bean on slightly evil player (-200) clamps at 0 - var res_bean_clamp := AlignmentSystem.use_zen_bean(-200) - _assert(res_bean_clamp.ok and res_bean_clamp.new_alignment == 0, "Zen bean clamps at 0") - - # Zen bean on neutral or good player is rejected - var res_bean_good := AlignmentSystem.use_zen_bean(500) - _assert(not res_bean_good.ok and res_bean_good.code == "CANNOT_USE_NOT_EVIL", - "Zen bean rejected when player is not evil") diff --git a/project/test_alignment_parity.gd.uid b/project/test_alignment_parity.gd.uid deleted file mode 100644 index 24ff546b..00000000 --- a/project/test_alignment_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bfls441vq8ubu diff --git a/project/test_alignment_pk_parity.gd b/project/test_alignment_pk_parity.gd deleted file mode 100644 index 68837c49..00000000 --- a/project/test_alignment_pk_parity.gd +++ /dev/null @@ -1,149 +0,0 @@ -# test_alignment_pk_parity.gd —— 40250 善恶值与名望头衔奖惩系统 1:1 回归测试套件 -extends SceneTree - -const AlignmentPkSystem = preload("res://alignment_pk_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_alignment_pk_parity (Direction 3: Alignment PK & Titles 1:1) ===") - _test_title_tiers_and_thresholds() - _test_zen_bean_clearing() - _test_safe_zone_time_recovery() - _test_red_name_death_drop_penalty() - _test_serialization() - _finish() - -func _test_title_tiers_and_thresholds() -> void: - print("\n--- Test 1: Title Tiers & Alignment Thresholds (40250 1:1) ---") - var sys := AlignmentPkSystem.new() - - _assert(AlignmentPkSystem.get_title_name(15000) == "崇高骑士", ">= 12000 is Chivalric (崇高骑士)") - _assert(AlignmentPkSystem.get_title_name(9000) == "尊贵贵族", ">= 8000 is Noble (尊贵贵族)") - _assert(AlignmentPkSystem.get_title_name(5000) == "正义良民", ">= 4000 is Good (正义良民)") - _assert(AlignmentPkSystem.get_title_name(1500) == "友好伙伴", ">= 1000 is Friendly (友好伙伴)") - _assert(AlignmentPkSystem.get_title_name(500) == "中立行者", "0 ~ 999 is Neutral (中立行者)") - _assert(AlignmentPkSystem.get_title_name(-1000) == "好斗狂徒", "-1 ~ -3999 is Aggressive (好斗狂徒)") - _assert(AlignmentPkSystem.get_title_name(-5000) == "狡诈恶徒", "-4000 ~ -7999 is Fraudulent (狡诈恶徒)") - _assert(AlignmentPkSystem.get_title_name(-9000) == "险恶凶手", "-8000 ~ -11999 is Malicious (险恶凶手)") - _assert(AlignmentPkSystem.get_title_name(-15000)== "残忍魔头", "<= -12000 is Cruel (残忍魔头)") - - # Test clamping bounds (-200000 to +200000 real alignment) - sys.update_real_alignment(500000) - _assert(sys.real_alignment == 200000, "Clamped at MAX_REAL_ALIGNMENT (+200000)") - _assert(sys.get_alignment() == 20000, "Display alignment clamped at +20000") - - sys.update_real_alignment(-900000) - _assert(sys.real_alignment == -200000, "Clamped at MIN_REAL_ALIGNMENT (-200000)") - _assert(sys.get_alignment() == -20000, "Display alignment clamped at -20000") - -func _test_zen_bean_clearing() -> void: - print("\n--- Test 2: Zen Bean (70102) Sin Clearing ---") - var sys := AlignmentPkSystem.new() - var inv: Array = [ - {"vnum": AlignmentPkSystem.VNUM_ZEN_BEAN, "name": "禅豆", "count": 3} - ] - - # Set alignment to positive - sys.real_alignment = 5000 # +500 - var res_pos = sys.use_zen_bean(inv, 0) - _assert(res_pos["ok"] == false, "Zen bean rejected when alignment is positive") - _assert(res_pos["reason"] == "ALIGNMENT_ALREADY_GOOD", "Reason is ALREADY_GOOD") - _assert(inv[0]["count"] == 3, "Zen bean not consumed") - - # Set alignment to -1200 (-12000 real) - sys.real_alignment = -12000 - var bean_sig := {"fired": false, "cleared": 0, "align": 0} - sys.zen_bean_consumed.connect(func(c: int, a: int): - bean_sig["fired"] = true - bean_sig["cleared"] = c - bean_sig["align"] = a - ) - - var res_neg = sys.use_zen_bean(inv, 0) - _assert(res_neg["ok"] == true, "Zen bean successfully used") - _assert(res_neg["cleared"] == 500, "Cleared 500 display points") - _assert(sys.get_alignment() == -700, "New alignment is -700 (-1200 + 500)") - _assert(inv[0]["count"] == 2, "Zen bean count decremented to 2") - _assert(bean_sig["fired"] == true, "zen_bean_consumed signal emitted") - - # Test capping clear to 0 when remaining evil < 500 (e.g. -200) - sys.real_alignment = -2000 # -200 display - inv[0]["count"] = 1 - var res_cap = sys.use_zen_bean(inv, 0) - _assert(res_cap["ok"] == true, "Zen bean used") - _assert(res_cap["cleared"] == 200, "Only cleared remaining 200 points") - _assert(sys.get_alignment() == 0, "Alignment restored to 0 (Neutral)") - _assert(inv[0] == null, "Last Zen bean consumed completely") - -func _test_safe_zone_time_recovery() -> void: - print("\n--- Test 3: Safe Zone Time-Based Recovery (40250 char.cpp) ---") - var sys := AlignmentPkSystem.new() - - # Evil player: recovers 60 pts/min without item - sys.real_alignment = -10000 # -1000 display - sys.recover_alignment_by_time(5, false) # +300 pts - _assert(sys.get_alignment() == -700, "Recovered 300 points over 5 minutes (60/min)") - - # Evil player: recovers 120 pts/min with item - sys.recover_alignment_by_time(5, true) # +600 pts - _assert(sys.get_alignment() == -100, "Recovered 600 points over 5 minutes with item (120/min)") - - # Good player: recovers 5 pts/min - sys.real_alignment = 10000 # +1000 display - sys.recover_alignment_by_time(10, false) # +50 pts - _assert(sys.get_alignment() == 1050, "Recovered 50 points over 10 minutes (5/min)") - -func _test_red_name_death_drop_penalty() -> void: - print("\n--- Test 4: Red Name Death Drop Penalty & Soulbound Exemption ---") - var sys := AlignmentPkSystem.new() - - var inv: Array = [ - {"vnum": 189, "name": "毒剑+9", "is_soulbound": false}, - {"vnum": 11299, "name": "黑铁甲+9", "is_soulbound": true}, # Soulbound protected! - {"vnum": 50300, "name": "技能书", "is_soulbound": false} - ] - - # 1. White name (Alignment >= 0): 0 drops - sys.real_alignment = 50000 - var drops_good = sys.handle_death_drop_penalty(inv) - _assert(drops_good.is_empty(), "Good player never drops items on death") - _assert(inv[0] != null and inv[1] != null and inv[2] != null, "Inventory remains intact") - - # 2. Cruel Red name (Alignment <= -12000): guaranteed drop with 80% chance - sys.real_alignment = -150000 # -15000 (Cruel) - # Seed random for deterministic test - seed(42) - var drops_evil = sys.handle_death_drop_penalty(inv) - _assert(not drops_evil.is_empty(), "Cruel red name dropped items on death") - _assert(inv[1] != null, "Soulbound Black Steel Armor was NOT dropped (immune)") - -func _test_serialization() -> void: - print("\n--- Test 5: Serialization & Restoration ---") - var sys1 := AlignmentPkSystem.new() - sys1.real_alignment = -85400 # -8540 display (Malicious) - - var data = sys1.serialize() - var sys2 := AlignmentPkSystem.new() - sys2.deserialize(data) - _assert(sys2.real_alignment == -85400, "Restored real alignment") - _assert(sys2.get_alignment() == -8540, "Restored display alignment") - _assert(sys2.get_title_name(sys2.get_alignment()) == "险恶凶手", "Restored Malicious title") - -func _finish() -> void: - print("\n=======================================================") - print("Alignment PK System Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_alignment_pk_parity.gd.uid b/project/test_alignment_pk_parity.gd.uid deleted file mode 100644 index 9020d5ad..00000000 --- a/project/test_alignment_pk_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dbu8gisfruo3p diff --git a/project/test_atlas_navigation_parity.gd b/project/test_atlas_navigation_parity.gd deleted file mode 100644 index 6f5a0105..00000000 --- a/project/test_atlas_navigation_parity.gd +++ /dev/null @@ -1,119 +0,0 @@ -# test_atlas_navigation_parity.gd —— 40250 全景大地图与寻路导航回归测试 -extends SceneTree - -const SystemClass = preload("res://atlas_navigation_system.gd") - -var _passed: int = 0 -var _failed: int = 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_atlas_navigation_parity (Batch 28 Direction 1) ===") - _test_atlas_toggle_and_map_registry() - _test_zoom_and_pan_controls() - _test_npc_directory_indexing() - _test_signal_point_and_navigation_vector() - _test_coordinate_projection() - - print("\n=== Result: %d Passed, %d Failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - else: - printerr("SOME TESTS FAILED!\n") - quit(0 if _failed == 0 else 1) - -func _test_atlas_toggle_and_map_registry() -> void: - print("\n--- Test 1: M Key Toggle & Map Registry ---") - var nav = SystemClass.new("metin2_map_a1") - - _assert(nav.is_atlas_visible == false, "Atlas initially hidden") - - # Toggle M - var state1 = nav.toggle_atlas() - _assert(state1 == true and nav.is_atlas_visible == true, "Atlas toggled open on M key") - - var state2 = nav.toggle_atlas() - _assert(state2 == false and nav.is_atlas_visible == false, "Atlas toggled closed on second M key") - - # Map switching - var sw_ok = nav.set_map("metin2_map_milgyo") - _assert(sw_ok == true, "Switched map to metin2_map_milgyo") - var info = nav.get_current_map_info() - _assert(info["name"] == "幽冥神殿 (Dark Temple)", "Map registry metadata correctly fetched") - - var sw_fail = nav.set_map("invalid_map_id") - _assert(sw_fail == false, "Reject invalid map id") - -func _test_zoom_and_pan_controls() -> void: - print("\n--- Test 2: Zoom and Pan Clamping ---") - var nav = SystemClass.new() - - # Zoom clamping (0.5 to 3.0) - nav.set_zoom(0.2) - _assert(nav.zoom_level == 0.5, "Zoom clamped at lower bound 0.5x") - - nav.set_zoom(5.0) - _assert(nav.zoom_level == 3.0, "Zoom clamped at upper bound 3.0x") - - nav.set_zoom(1.5) - _assert(nav.zoom_level == 1.5, "Zoom set to 1.5x") - - # Pan accumulation - nav.apply_pan(Vector2(20, -10)) - _assert(nav.pan_offset == Vector2(20, -10), "Pan offset accumulated") - -func _test_npc_directory_indexing() -> void: - print("\n--- Test 3: NPC Directory Indexing & Filtering ---") - var nav = SystemClass.new() - - var all_npcs = nav.get_npcs() - _assert(all_npcs.size() >= 10, "Default town contains at least 10 key NPCs") - - var smiths = nav.get_npcs("Smith") - _assert(smiths.size() == 1 and smiths[0]["vid"] == 20016, "Filtered single blacksmith (VID 20016)") - - var shops = nav.get_npcs("Shop") - _assert(shops.size() == 3, "Filtered 3 shops (Weapon, Armor, General)") - - var bio = nav.find_npc_by_vid(20084) - _assert(bio.get("name").contains("生物学家"), "Found Biologist by VID 20084") - -func _test_signal_point_and_navigation_vector() -> void: - print("\n--- Test 4: Signal Point Waypoint & Compass Bearing ---") - var nav = SystemClass.new() - - # Add signal waypoint - var sig = nav.add_signal_point(600, 600) - _assert(nav.signal_points.size() == 1, "Signal waypoint added") - _assert(sig["x"] == 600 and sig["y"] == 600, "Signal waypoint coordinates match (600, 600)") - - # Calculate compass bearing: Player at (500, 600) -> Target at (600, 600) - var nav_vec = nav.calculate_navigation_vector(Vector2(500, 600), Vector2(600, 600)) - _assert(nav_vec["distance"] == 100.0, "Distance is exactly 100") - _assert(is_equal_approx(nav_vec["angle_deg"], 0.0), "Heading is exactly 0 degrees (East)") - _assert(nav_vec["arrived"] == false, "Not arrived (dist 100 > 30)") - - # Arrival test: Player at (590, 600) -> Target at (600, 600) - var nav_arrived = nav.calculate_navigation_vector(Vector2(590, 600), Vector2(600, 600)) - _assert(nav_arrived["distance"] == 10.0, "Distance is 10") - _assert(nav_arrived["arrived"] == true, "Arrival detected when dist <= 30") - - # Clear all signals - nav.clear_all_signals() - _assert(nav.signal_points.is_empty(), "All signal waypoints cleared") - -func _test_coordinate_projection() -> void: - print("\n--- Test 5: World to Atlas UI Coordinate Projection ---") - var nav = SystemClass.new("metin2_map_a1") # 4000x4000 world -> 256x256 atlas - nav.set_zoom(1.0) - - # World center (2000, 2000) -> Atlas center (128, 128) - var projected = nav.world_to_atlas_coord(Vector2(2000, 2000)) - _assert(is_equal_approx(projected.x, 128.0) and is_equal_approx(projected.y, 128.0), "Projected world center (2000, 2000) to atlas (128, 128)") diff --git a/project/test_atlas_navigation_parity.gd.uid b/project/test_atlas_navigation_parity.gd.uid deleted file mode 100644 index 760abca8..00000000 --- a/project/test_atlas_navigation_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://carj6kfhf47rv diff --git a/project/test_attach_metin_parity.gd b/project/test_attach_metin_parity.gd deleted file mode 100644 index 6376f0a5..00000000 --- a/project/test_attach_metin_parity.gd +++ /dev/null @@ -1,92 +0,0 @@ -# test_attach_metin_parity.gd -# 检验 40250 官方装备灵魂石镶嵌与双重属性预览系统 -extends SceneTree - -const AttachMetinDialogSystem = preload("res://attach_metin_dialog_system.gd") - -func _init() -> void: - print("--- 开始测试 40250 装备灵魂石镶嵌系统 (test_attach_metin_parity) ---") - var system = AttachMetinDialogSystem.new() - var fails: int = 0 - - # 1. 槽位类型合法性 CanAttachMetin 测试 - # METIN_NORMAL 应该能嵌入 SILVER(1) 和 GOLD(2),但不能嵌入 NONE(0) 或已经有石头的槽位(28430) - if not system.can_attach_metin(AttachMetinDialogSystem.METIN_SOCKET_TYPE_SILVER, AttachMetinDialogSystem.METIN_NORMAL): - printerr("FAIL: 普通灵魂石无法嵌入银孔!") - fails += 1 - if not system.can_attach_metin(AttachMetinDialogSystem.METIN_SOCKET_TYPE_GOLD, AttachMetinDialogSystem.METIN_NORMAL): - printerr("FAIL: 普通灵魂石无法嵌入金孔!") - fails += 1 - if system.can_attach_metin(AttachMetinDialogSystem.METIN_SOCKET_TYPE_NONE, AttachMetinDialogSystem.METIN_NORMAL): - printerr("FAIL: 未开孔槽位不应能嵌入灵魂石!") - fails += 1 - - # METIN_GOLD 只能嵌入 GOLD(2) - if system.can_attach_metin(AttachMetinDialogSystem.METIN_SOCKET_TYPE_SILVER, AttachMetinDialogSystem.METIN_GOLD): - printerr("FAIL: 黄金幽魂石不应能嵌入普通银孔!") - fails += 1 - if not system.can_attach_metin(AttachMetinDialogSystem.METIN_SOCKET_TYPE_GOLD, AttachMetinDialogSystem.METIN_GOLD): - printerr("FAIL: 黄金幽魂石应当能嵌入金孔!") - fails += 1 - - # 2. 对话框打开与预览槽位替换测试 - # 假设目标武器当前槽位: [1 (开孔), 1 (开孔), 0 (未开孔)] - # 镶嵌 致命之石+4 (28435, METIN_NORMAL) - var opened = system.open_dialog(10, 20, 28435, AttachMetinDialogSystem.METIN_NORMAL, 10, [1, 1, 0]) - if not opened or not system.is_open: - printerr("FAIL: 开启镶嵌对话框失败!") - fails += 1 - if system.target_socket_idx != 0: - printerr("FAIL: 首个开放槽位索引应为 0,实际为: ", system.target_socket_idx) - fails += 1 - if system.preview_sockets[0] != 28435 or system.preview_sockets[1] != 1 or system.preview_sockets[2] != 0: - printerr("FAIL: 预览槽位状态不符,实际: ", system.preview_sockets) - fails += 1 - - # 3. 接受镶嵌并检查数据包与音效 - var accepted = system.accept() - if not accepted: - printerr("FAIL: 确认镶嵌失败!") - fails += 1 - if system.last_packet_sent.get("header") != "HEADER_CG_ITEM_USE_TO_ITEM": - printerr("FAIL: 数据包 Header 错误: ", system.last_packet_sent) - fails += 1 - if system.last_sound_played != "sound/ui/metinstone_insert.wav": - printerr("FAIL: 镶嵌音效路径不符: ", system.last_sound_played) - fails += 1 - - # 4. 服务端结算模拟: 成功嵌入 vs 失败破碎 (28960) - var sockets_after_success = system.simulate_server_process([1, 1, 0], 0, 28435, true) - if sockets_after_success[0] != 28435: - printerr("FAIL: 成功镶嵌后槽位未记录石头 VNUM: ", sockets_after_success) - fails += 1 - - var sockets_after_fail = system.simulate_server_process([1, 1, 0], 0, 28435, false) - if sockets_after_fail[0] != AttachMetinDialogSystem.ITEM_BROKEN_METIN_VNUM: - printerr("FAIL: 镶嵌失败未生成破碎石块 28960: ", sockets_after_fail) - fails += 1 - - # 5. 清理碎石卷轴测试 - var cleaned = system.clean_broken_socket(sockets_after_fail, 0) - if cleaned[0] != AttachMetinDialogSystem.METIN_SOCKET_TYPE_SILVER: - printerr("FAIL: 清理破碎石块后槽位未恢复银孔状态: ", cleaned) - fails += 1 - - # 6. 无可用槽位时拒绝开启 - var no_slot_opened = system.open_dialog(11, 21, 28435, AttachMetinDialogSystem.METIN_NORMAL, 10, [0, 0, 0]) - if no_slot_opened or system.is_open: - printerr("FAIL: 无可用孔位时不应成功开启镶嵌对话框!") - fails += 1 - - # 7. 石头数据查询 - var info = system.get_metin_info(28434) - if info.bonus_type != "POINT_PENETRATE_PCT" or info.bonus_value != 8: - printerr("FAIL: 穿透之石+4 属性解析错误: ", info) - fails += 1 - - if fails == 0: - print("PASS: test_attach_metin_parity (7/7 全部通过)") - quit(0) - else: - printerr("FAILED: test_attach_metin_parity (", fails, " 项失败)") - quit(1) diff --git a/project/test_attach_metin_parity.gd.uid b/project/test_attach_metin_parity.gd.uid deleted file mode 100644 index a12076a7..00000000 --- a/project/test_attach_metin_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cuk0clbocgasy diff --git a/project/test_auto_combat_bot_parity.gd b/project/test_auto_combat_bot_parity.gd deleted file mode 100644 index 0255d246..00000000 --- a/project/test_auto_combat_bot_parity.gd +++ /dev/null @@ -1,168 +0,0 @@ -# test_auto_combat_bot_parity.gd —— 40250 自动战斗猎杀挂机助手 1:1 回归测试套件 -extends SceneTree - -const AutoCombatBotSystem = preload("res://auto_combat_bot_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_auto_combat_bot_parity (Direction 2: Auto Combat Bot 1:1) ===") - _test_bot_toggle() - _test_auto_potion() - _test_auto_target_and_combat() - _test_auto_looting() - _finish() - -func _test_bot_toggle() -> void: - print("\n--- Test 1: Bot Toggle State ---") - var bot := AutoCombatBotSystem.new() - - var toggle_sig := {"fired": false, "enabled": false} - bot.bot_toggled.connect(func(en: bool): - toggle_sig["fired"] = true - toggle_sig["enabled"] = en - ) - - var res1 = bot.toggle_bot(true) - _assert(res1["enabled"] == true and bot.is_enabled == true, "Bot enabled") - _assert(toggle_sig["fired"] == true and toggle_sig["enabled"] == true, "bot_toggled emitted true") - - bot.toggle_bot(false) - _assert(bot.is_enabled == false, "Bot disabled") - -func _test_auto_potion() -> void: - print("\n--- Test 2: Smart Auto Potion (HP/SP Threshold) ---") - var bot := AutoCombatBotSystem.new() - bot.toggle_bot(true) - - var player := { - "hp": 400, - "max_hp": 1000, # 40% < 50% threshold - "sp": 800, - "max_sp": 1000, # 80% > 40% threshold (no SP potion) - "pos": Vector3.ZERO - } - var inv: Array = [ - {"vnum": 27003, "name": "红色药水(特大)", "count": 2}, - {"vnum": 27006, "name": "蓝色药水(特大)", "count": 2} - ] - - var pot_events := [] - bot.potion_used.connect(func(v: int, pt: String): - pot_events.append({"vnum": v, "type": pt}) - ) - - # Update tick should trigger HP potion only - bot.update_bot(0.1, player, inv, [], []) - _assert(player["hp"] == 1000, "HP restored (capped at max 1000)") - _assert(inv[0]["count"] == 1, "Red potion consumed (2 -> 1)") - _assert(pot_events.size() == 1 and pot_events[0]["type"] == "hp", "potion_used emitted for HP") - - # Now drop SP to 20% (< 40%) and reset potion cooldown - bot.potion_cooldown = 0.0 - player["sp"] = 200 - bot.update_bot(0.1, player, inv, [], []) - _assert(player["sp"] == 1000, "SP restored to max 1000") - _assert(inv[1]["count"] == 1, "Blue potion consumed (2 -> 1)") - _assert(pot_events.size() == 2 and pot_events[1]["type"] == "sp", "potion_used emitted for SP") - -func _test_auto_target_and_combat() -> void: - print("\n--- Test 3: Auto Target, Move & Skill/Attack Execution ---") - var bot := AutoCombatBotSystem.new() - bot.toggle_bot(true) - bot.assigned_skills = [1] # Assign Triple Slash - - var player := { - "atk": 500, - "hp": 1000, - "max_hp": 1000, - "sp": 1000, - "max_sp": 1000, - "pos": Vector3.ZERO - } - var mob := { - "id": "mob_99", - "hp": 2000, - "alive": true, - "pos": Vector3(8.0, 0.0, 0.0) # 8m away (> 3.0m attack range) - } - - var lock_sig := {"fired": false, "id": ""} - bot.target_locked.connect(func(tid: String): - lock_sig["fired"] = true - lock_sig["id"] = tid - ) - - # 1. Step: Locks target and moves closer - var step1 = bot.update_bot(0.5, player, [], [mob], []) - _assert(lock_sig["fired"] == true and lock_sig["id"] == "mob_99", "target_locked emitted") - _assert(step1["status"] == "moving_to_target", "Bot moving toward target") - _assert(player["pos"].x > 0.0, "Player moved along X axis") - - # Move player close (2.0m <= 3.0m attack range) - player["pos"] = Vector3(6.5, 0.0, 0.0) # 1.5m distance - var sk_sig := {"fired": false} - bot.auto_skill_fired.connect(func(_sk: int, _t: String): sk_sig["fired"] = true) - - # 2. Step: Fires skill (Triple Slash, 2.0x dmg = 1000) - var step2 = bot.update_bot(0.1, player, [], [mob], []) - _assert(step2["status"] == "skill_attack", "Executed skill attack") - _assert(step2["damage"] == 1000, "Dealt 1000 skill damage (500 * 2)") - _assert(mob["hp"] == 1000, "Mob HP reduced 2000 -> 1000") - _assert(sk_sig["fired"] == true, "auto_skill_fired emitted") - - # 3. Step: Skill is now on cooldown, performs basic attack (500 dmg) - var step3 = bot.update_bot(0.1, player, [], [mob], []) - _assert(step3["status"] == "basic_attack", "Executed basic attack on cooldown") - _assert(mob["hp"] == 500, "Mob HP reduced 1000 -> 500") - -func _test_auto_looting() -> void: - print("\n--- Test 4: Auto Looting Within 8.0m ---") - var bot := AutoCombatBotSystem.new() - bot.toggle_bot(true) - - var player := { - "hp": 1000, - "max_hp": 1000, - "sp": 1000, - "max_sp": 1000, - "pos": Vector3.ZERO - } - var inv: Array = [null, null] - var drops: Array = [ - {"id": 1, "vnum": 71003, "name": "祝福卷轴", "count": 1, "pos": Vector3(3.0, 0.0, 0.0), "looted": false}, - {"id": 2, "vnum": 50513, "name": "灵魂之石", "count": 1, "pos": Vector3(20.0, 0.0, 0.0), "looted": false} # > 8m - ] - - var loot_sig := {"fired": false, "name": ""} - bot.auto_loot_collected.connect(func(n: String, _s: int): - loot_sig["fired"] = true - loot_sig["name"] = n - ) - - bot.update_bot(0.1, player, inv, [], drops) - _assert(drops[0]["looted"] == true, "Drop 1 (3m) was looted") - _assert(drops[1]["looted"] == false, "Drop 2 (20m) out of range, not looted") - _assert(inv[0]["vnum"] == 71003, "Blessing Scroll placed into slot 0") - _assert(loot_sig["fired"] == true and loot_sig["name"] == "祝福卷轴", "auto_loot_collected emitted") - -func _finish() -> void: - print("\n==========================================") - print("Auto Combat Bot Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Auto Combat Bot parity tests passed!") - quit(0) diff --git a/project/test_auto_combat_bot_parity.gd.uid b/project/test_auto_combat_bot_parity.gd.uid deleted file mode 100644 index 3fb2dc9f..00000000 --- a/project/test_auto_combat_bot_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dwysm8q3qqvbu diff --git a/project/test_battle_arena_duel_parity.gd b/project/test_battle_arena_duel_parity.gd deleted file mode 100644 index 2e49132c..00000000 --- a/project/test_battle_arena_duel_parity.gd +++ /dev/null @@ -1,149 +0,0 @@ -# test_battle_arena_duel_parity.gd —— 40250 官方武斗竞技场与裁判规则 1:1 回归测试套件 -extends SceneTree - -const BattleArenaDuelSystem = preload("res://battle_arena_duel_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_battle_arena_duel_parity (Direction 2: Battle Arena Duel 1:1) ===") - _test_duel_start_and_countdown() - _test_rules_interception() - _test_best_of_three_match_progression() - _test_straight_sweep_victory() - _finish() - -func _test_duel_start_and_countdown() -> void: - print("\n--- Test 1: Arena Duel Initialization & Countdown ---") - var sys := BattleArenaDuelSystem.new() - var duel_sig := {"fired": false, "opp": "", "sets": 0} - sys.duel_started.connect(func(opp: String, s: int, _t: float): - duel_sig["fired"] = true - duel_sig["opp"] = opp - duel_sig["sets"] = s - ) - - var start_res = sys.start_duel("warrior", 3, 300.0) - _assert(start_res["ok"] == true, "Duel started successfully") - _assert(start_res["sets_to_win"] == 2, "Sets to win is 2 (Best of 3)") - _assert(sys.match_state == BattleArenaDuelSystem.STATE_COUNTDOWN, "Match in countdown state") - _assert(duel_sig["fired"] == true, "duel_started signal fired") - _assert(duel_sig["opp"] == "战神武者教头", "Opponent name matches warrior master") - - var round_sig := {"fired": false, "round": 0} - sys.round_started.connect(func(r: int): - round_sig["fired"] = true - round_sig["round"] = r - ) - - # Update countdown by 3.0s - sys.update(3.0) - _assert(sys.match_state == BattleArenaDuelSystem.STATE_FIGHTING, "Match entered FIGHTING state after countdown") - _assert(round_sig["fired"] == true, "round_started signal fired") - _assert(round_sig["round"] == 1, "First round started") - -func _test_rules_interception() -> void: - print("\n--- Test 2: Potion, Mount, & Boundary Rules Interception ---") - var sys := BattleArenaDuelSystem.new() - sys.start_duel("assassin", 3, 300.0) - sys.update(3.0) # In fighting state - - var violation_sig := {"fired": false, "reason": ""} - sys.rule_violation_intercepted.connect(func(r: String): - violation_sig["fired"] = true - violation_sig["reason"] = r - ) - - # 1. Potion test (Red Potion 27001) - var potion_allowed = sys.use_potion_check(27001) - _assert(potion_allowed == false, "Red potion prohibited in arena") - _assert(violation_sig["reason"] == "POTION_FORBIDDEN_IN_ARENA", "Reported potion violation") - - # 2. Mount test - violation_sig["fired"] = false - var mount_allowed = sys.mount_check() - _assert(mount_allowed == false, "Mount prohibited in arena") - _assert(violation_sig["reason"] == "MOUNT_FORBIDDEN_IN_ARENA", "Reported mount violation") - - # 3. Arena Bounds test (Legal point inside (100, 100)) - var inside_ok = sys.check_boundary(Vector2(100.0, 100.0)) - _assert(inside_ok == true, "Position (100, 100) within arena bounds") - - # Out of bounds point (500, 500) - violation_sig["fired"] = false - var out_ok = sys.check_boundary(Vector2(500.0, 500.0)) - _assert(out_ok == false, "Position (500, 500) outside arena bounds") - _assert(violation_sig["reason"] == "OUT_OF_ARENA_BOUNDS", "Reported boundary violation") - -func _test_best_of_three_match_progression() -> void: - print("\n--- Test 3: Best-of-3 Match Progression (1-0 -> 1-1 -> 2-1) ---") - var sys := BattleArenaDuelSystem.new() - sys.start_duel("sura", 3, 300.0) - sys.update(3.0) # Round 1 fighting - - var duel_fin_sig := {"fired": false, "winner": "", "reward": {}} - sys.duel_finished.connect(func(w: String, _sa: int, _sb: int, rew: Dictionary): - duel_fin_sig["fired"] = true - duel_fin_sig["winner"] = w - duel_fin_sig["reward"] = rew - ) - - # Round 1: Player beats Sura - var r1 = sys.record_defeat("B") - _assert(r1["match_finished"] == false, "Match not finished after Round 1") - _assert(r1["score_a"] == 1 and r1["score_b"] == 0, "Score is 1 - 0") - _assert(r1["current_round"] == 2, "Round 2 is next") - - # Advance countdown for Round 2 - sys.update(3.0) - - # Round 2: Sura beats Player - var r2 = sys.record_defeat("A") - _assert(r2["match_finished"] == false, "Match not finished after Round 2") - _assert(r2["score_a"] == 1 and r2["score_b"] == 1, "Score is 1 - 1 tied") - _assert(r2["current_round"] == 3, "Round 3 is decider") - - # Advance countdown for Round 3 - sys.update(3.0) - - # Round 3: Player beats Sura - var r3 = sys.record_defeat("B") - _assert(r3["match_finished"] == true, "Match finished after Round 3") - _assert(r3["winner"] == "A", "Grand winner is Player (Side A)") - _assert(duel_fin_sig["fired"] == true, "duel_finished signal fired") - _assert(duel_fin_sig["reward"]["arena_points"] == 200, "Received 200 arena points") - _assert(sys.match_state == BattleArenaDuelSystem.STATE_FINISHED, "State is FINISHED") - -func _test_straight_sweep_victory() -> void: - print("\n--- Test 4: Straight Sweep (2-0) Clean Finish ---") - var sys := BattleArenaDuelSystem.new() - sys.start_duel("shaman", 3, 300.0) - sys.update(3.0) - - # Round 1 win - sys.record_defeat("B") - sys.update(3.0) - - # Round 2 win -> reaches 2 sets_to_win - var r2 = sys.record_defeat("B") - _assert(r2["match_finished"] == true, "Match ends immediately on 2-0 sweep") - _assert(r2["winner"] == "A", "Player won 2-0") - _assert(sys.score_a == 2 and sys.score_b == 0, "Final score is 2 - 0") - -func _finish() -> void: - print("\n=======================================================") - print("Battle Arena Duel Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_battle_arena_duel_parity.gd.uid b/project/test_battle_arena_duel_parity.gd.uid deleted file mode 100644 index e767dab7..00000000 --- a/project/test_battle_arena_duel_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cvws4wvlb12uk diff --git a/project/test_belt_system_parity.gd b/project/test_belt_system_parity.gd deleted file mode 100644 index babe57ee..00000000 --- a/project/test_belt_system_parity.gd +++ /dev/null @@ -1,113 +0,0 @@ -# test_belt_system_parity.gd —— 40250 腰带系统与专属药水背包 1:1 纯净回归测试套件 -extends SceneTree - -const BeltSystem = preload("res://belt_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_belt_system_parity (Direction 2: Belt System 1:1) ===") - _test_slot_unlock_by_refine_level() - _test_potion_only_restriction() - _test_capacity_boundary() - _test_unequip_and_inventory_transfer() - _finish() - -func _test_slot_unlock_by_refine_level() -> void: - print("\n--- Test 1: Refine-Level Based Slot Unlocking (4 ~ 16 Slots) ---") - _assert(BeltSystem.calculate_unlocked_slots(0) == 4, "+0 belt unlocks 4 slots") - _assert(BeltSystem.calculate_unlocked_slots(3) == 8, "+3 belt unlocks 8 slots") - _assert(BeltSystem.calculate_unlocked_slots(7) == 12, "+7 belt unlocks 12 slots") - _assert(BeltSystem.calculate_unlocked_slots(9) == 16, "+9 belt unlocks 16 slots (4x4)") - - var bs := BeltSystem.new() - _assert(bs.get_unlocked_slot_count() == 0, "0 slots when no belt equipped") - - bs.equip_belt({"vnum": 18000, "name": "亚麻腰带+0", "refine_level": 0}) - _assert(bs.get_unlocked_slot_count() == 4, "+0 Linen Belt unlocks 4 slots") - - bs.equip_belt({"vnum": 18039, "name": "王者腰带+9", "refine_level": 9}) - _assert(bs.get_unlocked_slot_count() == 16, "+9 King Belt unlocks 16 slots") - -func _test_potion_only_restriction() -> void: - print("\n--- Test 2: Potion-Only Storage Restriction ---") - var bs := BeltSystem.new() - bs.equip_belt({"vnum": 18000, "name": "皮腰带+3", "refine_level": 3}) # 8 slots - - # 1. Weapon rejected - var r_sword = bs.store_item_in_belt(0, {"vnum": 10, "name": "木剑+0", "item_type": "weapon"}) - _assert(r_sword["ok"] == false and r_sword["reason"] == "NOT_A_POTION", "Weapon rejected from belt") - - # 2. Armor rejected - var r_armor = bs.store_item_in_belt(0, {"vnum": 11299, "name": "黑钢甲+9", "item_type": "armor"}) - _assert(r_armor["ok"] == false and r_armor["reason"] == "NOT_A_POTION", "Armor rejected from belt") - - # 3. Red Potion accepted - var r_pot = bs.store_item_in_belt(0, {"vnum": 27003, "name": "红药水(大)", "item_type": "potion", "count": 200}) - _assert(r_pot["ok"] == true, "Red Potion accepted in belt") - _assert(bs.belt_inventory[0]["vnum"] == 27003, "Red potion stored at belt slot 0") - - # 4. Sacred Dew accepted - var r_dew = bs.store_item_in_belt(1, {"vnum": 50821, "name": "绿露水", "item_type": "dew", "count": 10}) - _assert(r_dew["ok"] == true, "Dew accepted in belt") - - # 5. Sun Elixir accepted - var r_elixir = bs.store_item_in_belt(2, {"vnum": 72723, "name": "太阳之灵药", "item_type": "elixir", "count": 1}) - _assert(r_elixir["ok"] == true, "Elixir accepted in belt") - -func _test_capacity_boundary() -> void: - print("\n--- Test 3: Capacity Boundaries on Locked Slots ---") - var bs := BeltSystem.new() - bs.equip_belt({"vnum": 18000, "name": "亚麻腰带+0", "refine_level": 0}) # only slots 0..3 open - - var potion := {"vnum": 27003, "name": "红药水", "item_type": "potion", "count": 10} - - # Slot 3 (4th slot) is allowed - var r_ok = bs.store_item_in_belt(3, potion) - _assert(r_ok["ok"] == true, "Slot 3 allowed on +0 belt") - - # Slot 4 (5th slot) is locked - var r_locked = bs.store_item_in_belt(4, potion) - _assert(r_locked["ok"] == false and r_locked["reason"] == "SLOT_LOCKED_OR_INVALID", "Slot 4 locked on +0 belt") - -func _test_unequip_and_inventory_transfer() -> void: - print("\n--- Test 4: Belt Unequip & Potion Eviction to Main Inventory ---") - var bs := BeltSystem.new() - bs.equip_belt({"vnum": 18000, "name": "腰带+0", "refine_level": 0}) - - # Store 2 potions in belt - bs.store_item_in_belt(0, {"vnum": 27003, "name": "红药水", "item_type": "potion", "count": 50}) - bs.store_item_in_belt(1, {"vnum": 27006, "name": "蓝药水", "item_type": "potion", "count": 50}) - - # 1. Main inventory completely full -> unequip blocked - var full_inv: Array = [{"vnum": 1}, {"vnum": 2}] # no nulls - var r_block = bs.unequip_belt(full_inv) - _assert(r_block["ok"] == false and r_block["reason"] == "BELT_NOT_EMPTY", "Unequip blocked when main inventory full") - _assert(not bs.equipped_belt.is_empty(), "Belt remains equipped") - - # 2. Main inventory has space -> potions transferred and belt unequipped - var open_inv: Array = [null, null, null] - var r_ok = bs.unequip_belt(open_inv) - _assert(r_ok["ok"] == true, "Unequip succeeds with open inventory") - _assert(bs.equipped_belt.is_empty(), "Belt unequipped") - _assert(open_inv[0] != null and open_inv[0]["vnum"] == 27003, "Red potion transferred to main inventory slot 0") - _assert(open_inv[1] != null and open_inv[1]["vnum"] == 27006, "Blue potion transferred to main inventory slot 1") - _assert(bs.belt_inventory[0] == null and bs.belt_inventory[1] == null, "Belt inventory cleared") - -func _finish() -> void: - print("\n==========================================") - print("Belt System Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_belt_system_parity.gd.uid b/project/test_belt_system_parity.gd.uid deleted file mode 100644 index fd77303f..00000000 --- a/project/test_belt_system_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bqjdo1qgl77yv diff --git a/project/test_bgm_jukebox_parity.gd b/project/test_bgm_jukebox_parity.gd deleted file mode 100644 index dcd98b74..00000000 --- a/project/test_bgm_jukebox_parity.gd +++ /dev/null @@ -1,92 +0,0 @@ -# test_bgm_jukebox_parity.gd —— 40250 客户端 BGM 点唱机与地图音乐配置回归测试 -extends SceneTree - -const SystemClass = preload("res://bgm_jukebox_system.gd") - -var _passed: int = 0 -var _failed: int = 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_bgm_jukebox_parity (Batch 28 Direction 4) ===") - _test_playback_controls() - _test_volume_clamping() - _test_map_default_transitions() - _test_custom_jukebox_overrides() - - print("\n=== Result: %d Passed, %d Failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - else: - printerr("SOME TESTS FAILED!\n") - quit(0 if _failed == 0 else 1) - -func _test_playback_controls() -> void: - print("\n--- Test 1: BGM Playback Controls & Track Registry ---") - var juke = SystemClass.new() - - # 1. Play canonical track - var p_ok = juke.play_bgm("open_the_gate.mp3", true) - _assert(p_ok == true, "Play canonical track open_the_gate.mp3") - _assert(juke.is_playing == true, "Player is playing") - _assert(juke.current_track == "open_the_gate.mp3", "Current track matches") - _assert(juke.loop_enabled == true, "Loop is enabled") - - # 2. Reject non-canonical track - var p_fail = juke.play_bgm("unknown_external_audio.mp3") - _assert(p_fail == false, "Reject unregistered track") - - # 3. Stop playback - juke.stop_bgm() - _assert(juke.is_playing == false, "Playback stopped") - _assert(juke.current_track == "", "Current track cleared") - -func _test_volume_clamping() -> void: - print("\n--- Test 2: Volume Range Clamping (0.0 to 1.0) ---") - var juke = SystemClass.new() - - juke.set_volume(-0.5) - _assert(juke.volume == 0.0, "Volume clamped at 0.0") - - juke.set_volume(1.8) - _assert(juke.volume == 1.0, "Volume clamped at 1.0") - - juke.set_volume(0.65) - _assert(is_equal_approx(juke.volume, 0.65), "Volume set to 0.65 (65%)") - -func _test_map_default_transitions() -> void: - print("\n--- Test 3: Map Default BGM Transitions ---") - var juke = SystemClass.new() - - # Enter Login Screen -> enter_the_east.mp3 - var t_login = juke.on_enter_map("intro_login") - _assert(t_login == "enter_the_east.mp3", "Login screen plays enter_the_east.mp3") - _assert(juke.current_track == "enter_the_east.mp3", "Current track updated to login theme") - - # Enter Yongbi Desert -> desert.mp3 - var t_desert = juke.on_enter_map("metin2_map_n_desert_01") - _assert(t_desert == "desert.mp3", "Desert map plays desert.mp3") - _assert(juke.current_track == "desert.mp3", "Current track updated to desert") - -func _test_custom_jukebox_overrides() -> void: - print("\n--- Test 4: Jukebox Custom Map Overrides & Restoration ---") - var juke = SystemClass.new() - - # Default for a1 is open_the_gate.mp3 - _assert(juke.get_effective_map_bgm("metin2_map_a1") == "open_the_gate.mp3", "Default a1 track is open_the_gate.mp3") - - # Custom override: change a1 to save_the_world.mp3 - var o_ok = juke.set_custom_map_bgm("metin2_map_a1", "save_the_world.mp3") - _assert(o_ok == true, "Overrode a1 BGM to save_the_world.mp3") - _assert(juke.get_effective_map_bgm("metin2_map_a1") == "save_the_world.mp3", "Effective BGM is now custom track") - - # Reset override - juke.reset_custom_map_bgm("metin2_map_a1") - _assert(juke.get_effective_map_bgm("metin2_map_a1") == "open_the_gate.mp3", "Restored to default track on reset") diff --git a/project/test_bgm_jukebox_parity.gd.uid b/project/test_bgm_jukebox_parity.gd.uid deleted file mode 100644 index 226506d6..00000000 --- a/project/test_bgm_jukebox_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://blr5x7ej58d4w diff --git a/project/test_biologist_chaegirab_research_parity.gd b/project/test_biologist_chaegirab_research_parity.gd deleted file mode 100644 index 233bf3f0..00000000 --- a/project/test_biologist_chaegirab_research_parity.gd +++ /dev/null @@ -1,162 +0,0 @@ -# test_biologist_chaegirab_research_parity.gd —— 40250 生物学者车齿木研究链 1:1 回归测试套件 -extends SceneTree - -const BiologistChaegirabResearchSystem = preload("res://biologist_chaegirab_research_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_biologist_chaegirab_research_parity (Direction 1: Biologist Research 1:1) ===") - _test_stage_progression_and_sample_validation() - _test_sample_completion_and_soul_stone_capstone() - _test_subsequent_stages_and_stat_stacking() - _test_serialization() - _finish() - -func _test_stage_progression_and_sample_validation() -> void: - print("\n--- Test 1: Stage Requirements & Sample Item Validations ---") - var sys := BiologistChaegirabResearchSystem.new() - var inv: Array = [ - {"vnum": 10, "name": "木剑", "count": 1}, - {"vnum": 30006, "name": "兽人牙齿", "count": 5} - ] - - # 1. Level too low (< 30) - var res_low = sys.submit_sample(inv, 1, 25) - _assert(res_low["ok"] == false, "Rejected: level 25 < 30") - _assert(res_low["reason"] == "PLAYER_LEVEL_TOO_LOW", "Reason is PLAYER_LEVEL_TOO_LOW") - - # 2. Wrong sample item - var res_wrong = sys.submit_sample(inv, 0, 35) - _assert(res_wrong["ok"] == false, "Rejected: wrong item (Wood Sword)") - _assert(res_wrong["reason"] == "WRONG_SAMPLE_ITEM", "Reason is WRONG_SAMPLE_ITEM") - - # 3. Valid sample submission (force success) - var sub_sig := {"fired": false, "cur": 0} - sys.sample_submitted.connect(func(_n: String, _s: bool, c: int, _t: int): - sub_sig["fired"] = true - sub_sig["cur"] = c - ) - - var res_succ = sys.submit_sample(inv, 1, 35, true) - _assert(res_succ["ok"] == true, "Orc Tooth submitted successfully") - _assert(res_succ["current"] == 1, "Progress is 1/10") - _assert(inv[1]["count"] == 4, "Orc tooth count decremented 5 -> 4") - _assert(sub_sig["fired"] == true, "sample_submitted signal emitted") - -func _test_sample_completion_and_soul_stone_capstone() -> void: - print("\n--- Test 2: Sample Completion & Capstone Soul Stone Delivery ---") - var sys := BiologistChaegirabResearchSystem.new() - var player_stats := {"move_speed": 100} - var inv: Array = [ - {"vnum": 30006, "name": "兽人牙齿", "count": 10}, - {"vnum": 30220, "name": "兽人灵魂石", "count": 1} - ] - - # Submit remaining 9 teeth to hit 10/10 - for i in range(10): - sys.submit_sample(inv, 0, 35, true) - - _assert(sys.current_samples == 10, "Collected all 10 Orc Teeth") - _assert(sys.waiting_for_soul_stone == true, "Now waiting for soul stone") - - # Cannot submit more teeth - inv[0] = {"vnum": 30006, "name": "兽人牙齿", "count": 1} - var res_excess = sys.submit_sample(inv, 0, 35, true) - _assert(res_excess["ok"] == false, "Excess sample submission blocked") - _assert(res_excess["reason"] == "WAITING_FOR_SOUL_STONE", "Reason is WAITING_FOR_SOUL_STONE") - - var cap_sig := {"fired": false, "stone": ""} - sys.capstone_submitted.connect(func(sn: String, _st: String): - cap_sig["fired"] = true - cap_sig["stone"] = sn - ) - - # Submit wrong soul stone - inv[0] = {"vnum": 30221, "name": "诅咒灵魂石", "count": 1} - var res_wrong_stone = sys.submit_soul_stone(inv, 0, player_stats) - _assert(res_wrong_stone["ok"] == false, "Wrong soul stone rejected") - _assert(res_wrong_stone["reason"] == "WRONG_SOUL_STONE", "Reason is WRONG_SOUL_STONE") - - # Submit valid Orc Soul Stone (slot 1) - var res_stone_succ = sys.submit_soul_stone(inv, 1, player_stats) - _assert(res_stone_succ["ok"] == true, "Orc Soul Stone submitted") - _assert(player_stats["move_speed"] == 110, "Permanent move speed +10 applied (100 -> 110)") - _assert(sys.permanent_bonuses["move_speed"] == 10, "Permanent bonus recorded") - _assert(sys.current_stage_index == 1, "Advanced to Stage 2 (Curse Book)") - _assert(sys.current_samples == 0, "Current samples reset to 0") - _assert(sys.waiting_for_soul_stone == false, "No longer waiting for soul stone") - _assert(cap_sig["fired"] == true, "capstone_submitted signal emitted") - -func _test_subsequent_stages_and_stat_stacking() -> void: - print("\n--- Test 3: Subsequent Stages (Curse Book & Demon Keepsake) Stacking ---") - var sys := BiologistChaegirabResearchSystem.new() - var player_stats := {"move_speed": 100, "cast_speed_pct": 0, "defense": 200} - - # Stage 1: Complete Orc Tooth (+10 move speed) - var inv1: Array = [ - {"vnum": 30006, "name": "兽人牙齿", "count": 10}, - {"vnum": 30220, "name": "兽人灵魂石", "count": 1} - ] - for i in range(10): - sys.submit_sample(inv1, 0, 30, true) - sys.submit_soul_stone(inv1, 1, player_stats) - - # Stage 2: Complete Curse Book (+5% cast speed) - var inv2: Array = [ - {"vnum": 30047, "name": "诅咒之书", "count": 15}, - {"vnum": 30221, "name": "诅咒灵魂石", "count": 1} - ] - for i in range(15): - sys.submit_sample(inv2, 0, 40, true) - sys.submit_soul_stone(inv2, 1, player_stats) - - # Stage 3: Complete Demon Keepsake (+60 def) - var inv3: Array = [ - {"vnum": 30015, "name": "恶魔纪念物", "count": 15}, - {"vnum": 30222, "name": "恶魔灵魂石", "count": 1} - ] - for i in range(15): - sys.submit_sample(inv3, 0, 50, true) - sys.submit_soul_stone(inv3, 1, player_stats) - - _assert(player_stats["move_speed"] == 110, "Stage 1 bonus active (+10)") - _assert(player_stats["cast_speed_pct"] == 5, "Stage 2 bonus active (+5%)") - _assert(player_stats["defense"] == 260, "Stage 3 bonus active (+60)") - _assert(sys.completed_stages.size() == 3, "3 stages completed in total") - -func _test_serialization() -> void: - print("\n--- Test 4: Serialization & Restoration ---") - var sys1 := BiologistChaegirabResearchSystem.new() - sys1.current_stage_index = 4 # Stage 5: Zelkova Branch - sys1.current_samples = 18 - sys1.waiting_for_soul_stone = false - sys1.completed_stages = [1, 2, 3, 4] - sys1.permanent_bonuses = {"move_speed": 10, "cast_speed_pct": 5, "defense": 60, "attack_power": 50} - - var data = sys1.serialize() - var sys2 := BiologistChaegirabResearchSystem.new() - sys2.deserialize(data) - - _assert(sys2.current_stage_index == 4, "Restored stage index 4") - _assert(sys2.current_samples == 18, "Restored samples 18") - _assert(sys2.completed_stages.size() == 4, "Restored 4 completed stages") - _assert(sys2.permanent_bonuses["attack_power"] == 50, "Restored attack bonus 50") - -func _finish() -> void: - print("\n=======================================================") - print("Biologist Research Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_biologist_chaegirab_research_parity.gd.uid b/project/test_biologist_chaegirab_research_parity.gd.uid deleted file mode 100644 index 728ebd08..00000000 --- a/project/test_biologist_chaegirab_research_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://vuvnbdspji2u diff --git a/project/test_biologist_quest_parity.gd b/project/test_biologist_quest_parity.gd deleted file mode 100644 index 03c7c5e6..00000000 --- a/project/test_biologist_quest_parity.gd +++ /dev/null @@ -1,140 +0,0 @@ -# test_biologist_quest_parity.gd —— 40250 生物学研究家任务链 1:1 纯净回归测试套件 -extends SceneTree - -const BiologistQuestSystem = preload("res://biologist_quest_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_biologist_quest_parity (Direction 1: Biologist Quests 1:1) ===") - _test_level_unlock() - _test_item_submission_and_potion() - _test_soul_stone_completion_and_bonuses() - _test_full_chain_progression() - _finish() - -func _test_level_unlock() -> void: - print("\n--- Test 1: Level Requirement & Stage Unlock ---") - var bio := BiologistQuestSystem.new() - bio.init_for_player(25) # Level 25 - _assert(bio.current_stage_idx == -1, "Level 25 cannot start Lv30 Orc Tooth quest") - - bio.init_for_player(30) # Level 30 - _assert(bio.current_stage_idx == 0, "Level 30 unlocks Stage 0 (Orc Tooth)") - var stage: Dictionary = bio.get_current_stage() - _assert(stage["collect_vnum"] == 30006, "Stage 0 requires Orc Tooth (30006)") - _assert(stage["collect_count"] == 10, "Stage 0 requires 10 teeth") - -func _test_item_submission_and_potion() -> void: - print("\n--- Test 2: Item Submission & Spiral Potion (71035) ---") - var bio := BiologistQuestSystem.new() - bio.init_for_player(30) - - var inv: Array = [ - {"vnum": 30006}, # slot 0: Orc Tooth - {"vnum": 71035}, # slot 1: Spiral Potion - null - ] - - # Test missing item error - var empty_inv: Array = [null, null] - var res = bio.submit_item(empty_inv) - _assert(res["ok"] == false and res["reason"] == "MISSING_ITEM", "Fails when missing tooth in inventory") - - # Test successful submission with Spiral Potion - res = bio.submit_item(inv, true, true) - _assert(res["ok"] == true and res["success"] == true, "Submission succeeds with potion") - _assert(bio.delivered_count == 1, "Delivered count incremented to 1") - _assert(inv[0] == null, "Tooth consumed from slot 0") - _assert(inv[1] == null, "Spiral Potion consumed from slot 1") - _assert(bio.cooldown_remaining == 0.0, "Cooldown is 0 with potion") - -func _test_soul_stone_completion_and_bonuses() -> void: - print("\n--- Test 3: Soul Stone Phase & Permanent Stat Bonus ---") - var bio := BiologistQuestSystem.new() - bio.init_for_player(30) - bio.delivered_count = 10 - bio.soul_stone_phase = true - - var inv: Array = [ - {"vnum": 30220}, # Jinunggyi's Soul Stone - null - ] - - # Test without soul stone - var res = bio.submit_soul_stone([null]) - _assert(res["ok"] == false and res["reason"] == "MISSING_SOUL_STONE", "Fails without soul stone") - - # Submit soul stone - res = bio.submit_soul_stone(inv) - _assert(res["ok"] == true, "Soul stone submitted successfully") - _assert(res["completed_stage"] == 0, "Stage 0 completed") - _assert(bio.permanent_bonuses["move_speed"] == 10, "Permanent move speed +10 granted") - _assert(inv[0] == null, "Soul stone consumed from inventory") - -func _test_full_chain_progression() -> void: - print("\n--- Test 4: Full Quest Progression Lv30 -> Lv80 ---") - var bio := BiologistQuestSystem.new() - bio.init_for_player(85) # High level to test through all stages - - # Stage 0: Orc Tooth (Lv30) - _assert(bio.current_stage_idx == 0, "Current stage is 0 (Lv30)") - bio.delivered_count = 10 - bio.soul_stone_phase = true - bio.submit_soul_stone([{"vnum": 30220}]) - _assert(bio.permanent_bonuses["move_speed"] == 10, "Lv30 Bonus: Move speed +10") - - # Stage 1: Curse Book (Lv40) - _assert(bio.current_stage_idx == 1, "Advanced to stage 1 (Lv40 Curse Book)") - bio.delivered_count = 15 - bio.soul_stone_phase = true - bio.submit_soul_stone([{"vnum": 30221}]) - _assert(bio.permanent_bonuses["att_speed"] == 5, "Lv40 Bonus: Attack speed +5") - - # Stage 2: Demon Keepsake (Lv50) - _assert(bio.current_stage_idx == 2, "Advanced to stage 2 (Lv50 Demon Keepsake)") - bio.delivered_count = 15 - bio.soul_stone_phase = true - bio.submit_soul_stone([{"vnum": 30222}]) - _assert(bio.permanent_bonuses["def_grade"] == 60, "Lv50 Bonus: Defense +60") - - # Stage 3: Ice Marble (Lv60) - _assert(bio.current_stage_idx == 3, "Advanced to stage 3 (Lv60 Ice Marble)") - bio.delivered_count = 20 - bio.soul_stone_phase = true - bio.submit_soul_stone([{"vnum": 30223}]) - _assert(bio.permanent_bonuses["att_grade"] == 50, "Lv60 Bonus: Attack power +50") - - # Stage 4: Zelkova Branch (Lv70) - _assert(bio.current_stage_idx == 4, "Advanced to stage 4 (Lv70 Zelkova Branch)") - bio.delivered_count = 25 - bio.soul_stone_phase = true - bio.submit_soul_stone([{"vnum": 30224}]) - _assert(bio.permanent_bonuses["damage_reduction"] == 10, "Lv70 Bonus: Damage reduction 10%") - - # Stage 5: Tugyi's Tablet (Lv80) - _assert(bio.current_stage_idx == 5, "Advanced to stage 5 (Lv80 Tugyi Tablet)") - bio.delivered_count = 30 - bio.soul_stone_phase = true - bio.submit_soul_stone([{"vnum": 30225}]) - _assert(bio.permanent_bonuses["att_grade_pct"] == 10, "Lv80 Bonus: Attack power +10%") - - _assert(bio.completed_stages.size() == 6, "All 6 biologist quest stages completed") - -func _finish() -> void: - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - quit(0) - else: - printerr("SOME TESTS FAILED!\n") - quit(1) diff --git a/project/test_biologist_quest_parity.gd.uid b/project/test_biologist_quest_parity.gd.uid deleted file mode 100644 index 59506baa..00000000 --- a/project/test_biologist_quest_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dijmfku8ud36f diff --git a/project/test_boss_summon_scroll_parity.gd b/project/test_boss_summon_scroll_parity.gd deleted file mode 100644 index 62bbbc1b..00000000 --- a/project/test_boss_summon_scroll_parity.gd +++ /dev/null @@ -1,200 +0,0 @@ -# test_boss_summon_scroll_parity.gd —— Metin2 40250 首领与魔石召唤卷轴系统 1:1 校验套件 -extends SceneTree - -const BossSummonScrollSystem = preload("res://boss_summon_scroll_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - print(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_boss_summon_scroll_parity (Direction 4: Boss & Metin Summon Scrolls 1:1) ===") - - test_boss_scrolls_spawning() - test_metin_scrolls_spawning() - test_ore_vein_scrolls_spawning() - test_safe_zone_restriction() - test_serialization_and_management() - - print("\n=======================================================") - print("Boss & Metin Summon Scroll Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - - if _failed > 0: - quit(1) - else: - quit(0) - -func test_boss_scrolls_spawning() -> void: - print("\n--- Test 1: Boss Summon Scrolls (40250 1:1) ---") - var system = BossSummonScrollSystem.new() - - var boss_emitted := [] - system.boss_summoned.connect(func(vnum, name, pos, aggressive): - boss_emitted.append({"vnum": vnum, "name": name, "pos": pos, "aggressive": aggressive}) - ) - - # 背包中有 8 种 Boss 召唤卷 - var inv: Array = [ - {"vnum": 71036, "count": 2}, # 兽人首领 - {"vnum": 71037, "count": 1}, # 密教首领 - {"vnum": 71038, "count": 1}, # 蜘蛛女王 - {"vnum": 71039, "count": 1}, # 沙漠巨龟 - {"vnum": 71040, "count": 1}, # 烈焰魔王 - {"vnum": 71041, "count": 1}, # 九尾狐 - {"vnum": 71042, "count": 1}, # 恶魔之王 - {"vnum": 71043, "count": 1} # 黄虎幽灵 - ] - - var center = Vector2(1000.0, 2000.0) - - # 测试 71036 堆叠为 2,消耗后余 1 - var res = system.use_summon_scroll(inv, 0, center, false) - _assert(res["ok"] == true, "Summon Orc Chief succeeded") - _assert(inv[0] != null and inv[0]["count"] == 1, "Count decremented from 2 to 1") - _assert(res["entity"]["vnum"] == 691, "Spawned mob vnum is 691 (Orc Chief)") - _assert(res["entity"]["is_aggressive"] == true, "Boss is aggressive") - _assert(res["entity"]["level"] == 50, "Orc Chief level is 50") - _assert(boss_emitted.size() == 1, "boss_summoned signal emitted") - _assert(boss_emitted[0]["vnum"] == 691, "Signal reports mob vnum 691") - - # 测试 71040 烈焰魔王 (2206, Lv 73) - res = system.use_summon_scroll(inv, 4, center, false) - _assert(res["ok"] == true, "Summon Flame King succeeded") - _assert(inv[4] == null, "Single count scroll consumed to null") - _assert(res["entity"]["vnum"] == 2206, "Spawned Flame King vnum 2206") - _assert(res["entity"]["level"] == 73, "Flame King level 73") - - # 测试 71041 九尾狐 (1901, Lv 72) - res = system.use_summon_scroll(inv, 5, center, false) - _assert(res["ok"] == true, "Summon Nine Tails succeeded") - _assert(res["entity"]["vnum"] == 1901, "Spawned Nine Tails vnum 1901") - -func test_metin_scrolls_spawning() -> void: - print("\n--- Test 2: Metin Stone Summon Scrolls (40250 1:1) ---") - var system = BossSummonScrollSystem.new() - - var metin_emitted := [] - system.metin_summoned.connect(func(vnum, name, pos, level): - metin_emitted.append({"vnum": vnum, "name": name, "pos": pos, "level": level}) - ) - - var inv: Array = [ - {"vnum": 71080, "count": 1}, # 初级魔石 - {"vnum": 71081, "count": 1}, # 中级魔石 - {"vnum": 71082, "count": 1} # 高级魔石 - ] - var center = Vector2(500.0, 500.0) - - # 71080 初级魔石 (强制选取第4个 8004 贪婪之石 Lv 20) - var res = system.use_summon_scroll(inv, 0, center, false, 3) - _assert(res["ok"] == true, "Low Metin summon succeeded") - _assert(res["entity"]["vnum"] == 8004, "Metin of Greed spawned (vnum 8004)") - _assert(res["entity"]["level"] == 20, "Level is 20") - _assert(res["entity"]["rank"] == 5, "Rank is 5 (STONE)") - _assert(res["entity"]["is_aggressive"] == false, "Metin is stationary/non-aggressive") - - # 71081 中级魔石 (强制选取第3个 8008 灵魂之石 Lv 40) - res = system.use_summon_scroll(inv, 1, center, false, 2) - _assert(res["ok"] == true, "Medium Metin summon succeeded") - _assert(res["entity"]["vnum"] == 8008, "Metin of Soul spawned (vnum 8008)") - _assert(res["entity"]["level"] == 40, "Level is 40") - - # 71082 高级魔石 (强制选取第7个 8027 绝望之石 Lv 90) - res = system.use_summon_scroll(inv, 2, center, false, 7) - _assert(res["ok"] == true, "High Metin summon succeeded") - _assert(res["entity"]["vnum"] == 8027, "Metin of Jeon-Un spawned (vnum 8027)") - _assert(res["entity"]["level"] == 90, "Level is 90") - _assert(metin_emitted.size() == 3, "All 3 metin_summoned signals fired") - -func test_ore_vein_scrolls_spawning() -> void: - print("\n--- Test 3: Ore Vein Summon Scrolls (40250 1:1) ---") - var system = BossSummonScrollSystem.new() - - var vein_emitted := [] - system.ore_vein_summoned.connect(func(vnum, name, pos): - vein_emitted.append({"vnum": vnum, "name": name, "pos": pos}) - ) - - var inv: Array = [ - {"vnum": 71057, "count": 1}, # 钻石矿脉 (20049) - {"vnum": 71064, "count": 1} # 黑檀木矿脉 (20056) - ] - var center = Vector2(300.0, 300.0) - - var res = system.use_summon_scroll(inv, 0, center, false) - _assert(res["ok"] == true, "Diamond vein summon succeeded") - _assert(res["entity"]["vnum"] == 20049, "Vein vnum 20049") - _assert(vein_emitted.size() == 1, "ore_vein_summoned emitted") - - res = system.use_summon_scroll(inv, 1, center, false) - _assert(res["ok"] == true, "Ebony vein summon succeeded") - _assert(res["entity"]["vnum"] == 20056, "Vein vnum 20056") - -func test_safe_zone_restriction() -> void: - print("\n--- Test 4: Safe Zone Summon Restrictions (40250 1:1) ---") - var system = BossSummonScrollSystem.new() - - var inv: Array = [ - {"vnum": 71036, "count": 1}, # 兽人首领 - {"vnum": 71081, "count": 1}, # 中级魔石 - {"vnum": 71060, "count": 1} # 铜矿脉 - ] - - # 在安全区内尝试召唤首领 - var res = system.use_summon_scroll(inv, 0, Vector2.ZERO, true) - _assert(res["ok"] == false, "Boss summon rejected in safe zone") - _assert(res["reason"] == "CANNOT_SPAWN_IN_SAFEZONE", "Reason is CANNOT_SPAWN_IN_SAFEZONE") - _assert(inv[0]["count"] == 1, "Item not consumed on rejection") - - # 在安全区内尝试召唤魔石 - res = system.use_summon_scroll(inv, 1, Vector2.ZERO, true) - _assert(res["ok"] == false, "Metin summon rejected in safe zone") - _assert(inv[1]["count"] == 1, "Metin scroll not consumed") - - # 在安全区召唤矿脉是允许的 (矿脉无攻击性) - res = system.use_summon_scroll(inv, 2, Vector2.ZERO, true) - _assert(res["ok"] == true, "Ore vein allowed in safe zone") - _assert(inv[2] == null, "Ore vein scroll consumed") - -func test_serialization_and_management() -> void: - print("\n--- Test 5: Registry Management & Serialization ---") - var system = BossSummonScrollSystem.new() - - var inv: Array = [ - {"vnum": 71038, "count": 1}, # 蜘蛛女王 - {"vnum": 71082, "count": 1} # 高级魔石 - ] - system.use_summon_scroll(inv, 0, Vector2(100.0, 100.0), false) - system.use_summon_scroll(inv, 1, Vector2(200.0, 200.0), false, 0) - - var list = system.get_active_summoned_entities() - _assert(list.size() == 2, "Active entities count is 2") - _assert(list[0]["vnum"] == 2091, "First entity is Spider Queen (2091)") - _assert(list[1]["vnum"] == 8011, "Second entity is Metin of Devil (8011)") - - # 序列化 - var data = system.serialize() - _assert(data.has("active_entities") and data["active_entities"].size() == 2, "Serialized 2 entities") - - # 新系统反序列化 - var restored_system = BossSummonScrollSystem.new() - restored_system.deserialize(data) - var restored_list = restored_system.get_active_summoned_entities() - _assert(restored_list.size() == 2, "Restored 2 entities") - _assert(restored_list[0]["vnum"] == 2091, "Restored Spider Queen vnum") - _assert(restored_list[1]["level"] == 55, "Restored Metin level 55") - - # 移除与清空 - var removed = system.remove_summoned_entity(0) - _assert(removed == true, "Removed first entity") - _assert(system.get_active_summoned_entities().size() == 1, "1 entity remaining") - system.clear_summoned_entities() - _assert(system.get_active_summoned_entities().is_empty(), "All entities cleared") diff --git a/project/test_boss_summon_scroll_parity.gd.uid b/project/test_boss_summon_scroll_parity.gd.uid deleted file mode 100644 index 628dc6e3..00000000 --- a/project/test_boss_summon_scroll_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://7j2lta1cyakb diff --git a/project/test_chat_hyperlink_dice_parity.gd b/project/test_chat_hyperlink_dice_parity.gd deleted file mode 100644 index 127ed978..00000000 --- a/project/test_chat_hyperlink_dice_parity.gd +++ /dev/null @@ -1,103 +0,0 @@ -# test_chat_hyperlink_dice_parity.gd —— 40250 聊天物品超链接与 /dice 掷骰系统回归测试 -extends SceneTree - -const SystemClass = preload("res://chat_hyperlink_dice_system.gd") - -var _passed: int = 0 -var _failed: int = 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_chat_hyperlink_dice_parity (Batch 27 Direction 3) ===") - _test_item_hyperlink_encode_decode() - _test_tooltip_generation() - _test_broken_hyperlink_handling() - _test_dice_command_variations() - - print("\n=== Result: %d Passed, %d Failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - else: - printerr("SOME TESTS FAILED!\n") - quit(0 if _failed == 0 else 1) - -func _test_item_hyperlink_encode_decode() -> void: - print("\n--- Test 1: Item Hyperlink Encode & Decode Round-trip ---") - var original_item = { - "vnum": 189, - "name": "Poison Sword+9", - "flags": 1, - "sockets": [28430, 28431, 28432], - "attributes": [ - { "type": 72, "value": 45 }, # 平均伤害 45% - { "type": 71, "value": -12 }, # 技能伤害 -12% - { "type": 17, "value": 10 } # 半人加成 10% - ] - } - - # 1. Encode - var link_str = SystemClass.encode_item_hyperlink(original_item) - _assert(link_str.begins_with("|cffffc700|Hitem:"), "Link starts with official color and header") - _assert(link_str.ends_with("[Poison Sword+9]|h|r"), "Link ends with brackets and reset tag") - - # 2. Decode - var decoded = SystemClass.decode_item_hyperlink(link_str) - _assert(decoded["valid"] == true, "Decoded successfully") - _assert(decoded["vnum"] == 189, "Vnum matches 189") - _assert(decoded["name"] == "Poison Sword+9", "Name matches Poison Sword+9") - _assert(decoded["sockets"][0] == 28430, "Socket 0 matches 28430") - _assert(decoded["sockets"][1] == 28431, "Socket 1 matches 28431") - _assert(decoded["sockets"][2] == 28432, "Socket 2 matches 28432") - _assert(decoded["attributes"].size() == 3, "Decoded 3 attributes") - _assert(decoded["attributes"][0]["type"] == 72 and decoded["attributes"][0]["value"] == 45, "Attr 0 is Type 72 Val 45") - -func _test_tooltip_generation() -> void: - print("\n--- Test 2: Tooltip Generation on Link Hover ---") - var decoded = { - "valid": true, - "vnum": 11299, - "name": "Black Steel Armor+9", - "sockets": [28440, 28441, 0], - "attributes": [ - { "type": 1, "value": 2000 } # 最大生命值 +2000 - ] - } - - var tooltip = SystemClass.build_tooltip_text(decoded) - _assert(tooltip.contains("Black Steel Armor+9"), "Tooltip contains item name") - _assert(tooltip.contains("Vnum: 11299"), "Tooltip contains item vnum") - _assert(tooltip.contains("镶嵌宝石 [Vnum 28440]"), "Tooltip contains socket 1 gem") - _assert(tooltip.contains("附加属性: 类型 1 +2000"), "Tooltip contains max HP bonus") - -func _test_broken_hyperlink_handling() -> void: - print("\n--- Test 3: Broken Hyperlink Text Safety ---") - var r1 = SystemClass.decode_item_hyperlink("Just normal chat text") - _assert(r1["valid"] == false, "Normal text produces invalid hyperlink result") - - var r2 = SystemClass.decode_item_hyperlink("|cffffc700|Hitem:corrupted_data|h") - _assert(r2["valid"] == false, "Corrupted hyperlink handled safely without crash") - -func _test_dice_command_variations() -> void: - print("\n--- Test 4: 40250 /dice Command Logic (1:1 do_dice) ---") - # 1. Default /dice (1..100) - var d1 = SystemClass.execute_dice("Player1", [], 77) - _assert(d1["roll"] == 77, "Default dice rolled 77") - _assert(d1["start"] == 1 and d1["end"] == 100, "Default range is 1-100") - _assert(d1["message"].contains("77 (1-100)"), "Message correctly formatted") - - # 2. Single arg /dice 20 (1..20) - var d2 = SystemClass.execute_dice("Player2", ["20"], 15) - _assert(d2["roll"] == 15, "Dice rolled 15 in range 1-20") - _assert(d2["start"] == 1 and d2["end"] == 20, "Single arg range is 1-20") - - # 3. Two args reversed: /dice 80 20 -> auto min/max to (20..80) - var d3 = SystemClass.execute_dice("Player3", ["80", "20"], 50) - _assert(d3["start"] == 20 and d3["end"] == 80, "Auto swaps reversed range to 20-80") - _assert(d3["roll"] == 50, "Rolled 50 within normalized range") diff --git a/project/test_chat_hyperlink_dice_parity.gd.uid b/project/test_chat_hyperlink_dice_parity.gd.uid deleted file mode 100644 index ac7ca1f0..00000000 --- a/project/test_chat_hyperlink_dice_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bnwasl2v04le4 diff --git a/project/test_class_specialization_parity.gd b/project/test_class_specialization_parity.gd deleted file mode 100644 index 9f91b067..00000000 --- a/project/test_class_specialization_parity.gd +++ /dev/null @@ -1,144 +0,0 @@ -# test_class_specialization_parity.gd —— 40250 角色 30 级转职与无损洗髓 1:1 回归测试套件 -extends SceneTree - -const ClassSpecializationSystem = preload("res://class_specialization_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_class_specialization_parity (Direction 3: Class Specialization 1:1) ===") - _test_spec_tree_configuration() - _test_level_30_requirement() - _test_choosing_specialization() - _test_skill_point_allocation() - _test_reset_specialization_lossless() - _finish() - -func _test_spec_tree_configuration() -> void: - print("\n--- Test 1: 4 Classes x 2 Specializations Tree ---") - var sys := ClassSpecializationSystem.new() - for j in [0, 1, 2, 3]: - var specs = sys.get_available_specializations(j) - _assert(specs.size() == 2, "Job %d has exactly 2 specialization branches" % j) - _assert(specs.has(1) and specs.has(2), "Branches are 1 and 2") - _assert(specs[1]["skills"].size() == 5, "Branch 1 has 5 skills") - _assert(specs[2]["skills"].size() == 5, "Branch 2 has 5 skills") - -func _test_level_30_requirement() -> void: - print("\n--- Test 2: Level 30 Requirement ---") - var sys := ClassSpecializationSystem.new() - var low_lvl_player := {"job": 0, "level": 25, "skill_group": 0} - - var fail_res = sys.choose_specialization(1, low_lvl_player) - _assert(fail_res["ok"] == false and fail_res["reason"] == "LEVEL_TOO_LOW", "Level 25 rejected") - -func _test_choosing_specialization() -> void: - print("\n--- Test 3: Choosing Specialization (Warrior Body) ---") - var sys := ClassSpecializationSystem.new() - var player := {"job": 0, "level": 35, "skill_group": 0, "skill_points": 35} - - var spec_sig := {"fired": false, "job": -1, "spec_id": 0} - sys.specialization_chosen.connect(func(j: int, sid: int, _name: String): - spec_sig["fired"] = true - spec_sig["job"] = j - spec_sig["spec_id"] = sid - ) - - # Choose Arahan (Body Warrior) - var res = sys.choose_specialization(1, player) - _assert(res["ok"] == true, "Specialization chosen") - _assert(player["skill_group"] == 1, "skill_group set to 1") - _assert(player["skills"].size() == 5, "5 skills initialized") - _assert(player["skills"].has(4) and player["skills"][4]["name"] == "剑气", "Has Aura of Sword (4)") - _assert(spec_sig["fired"] == true and spec_sig["spec_id"] == 1, "specialization_chosen signal emitted") - - # Duplicate choice without reset must fail - var fail_dup = sys.choose_specialization(2, player) - _assert(fail_dup["ok"] == false and fail_dup["reason"] == "ALREADY_SPECIALIZED", "Cannot re-choose without reset") - -func _test_skill_point_allocation() -> void: - print("\n--- Test 4: Skill Point Allocation ---") - var sys := ClassSpecializationSystem.new() - var player := {"job": 0, "level": 35, "skill_group": 1, "skill_points": 5, "skills": { - 4: {"name": "剑气", "level": 0} - }} - - var alloc_sig := {"fired": false, "vnum": 0, "lvl": 0} - sys.skill_point_allocated.connect(func(v: int, l: int, _r: int): - alloc_sig["fired"] = true - alloc_sig["vnum"] = v - alloc_sig["lvl"] = l - ) - - # Allocate 3 points into Aura of Sword - sys.allocate_skill_point(4, player) - sys.allocate_skill_point(4, player) - sys.allocate_skill_point(4, player) - _assert(player["skills"][4]["level"] == 3, "Aura of Sword leveled to 3") - _assert(player["skill_points"] == 2, "Remaining points 5 - 3 = 2") - _assert(alloc_sig["fired"] == true and alloc_sig["lvl"] == 3, "skill_point_allocated emitted") - - # Cap test: level 20 is max normal level - player["skills"][4]["level"] = 20 - var fail_cap = sys.allocate_skill_point(4, player) - _assert(fail_cap["ok"] == false and fail_cap["reason"] == "MAX_NORMAL_LEVEL", "Cannot allocate past level 20") - -func _test_reset_specialization_lossless() -> void: - print("\n--- Test 5: Lossless Reset with Scroll (71100) & Branch Switch ---") - var sys := ClassSpecializationSystem.new() - var player := { - "job": 0, - "level": 40, - "skill_group": 1, # Body - "skill_points": 5, - "skills": { - 1: {"name": "三连斩", "level": 10}, - 4: {"name": "剑气", "level": 20} - } - } - # Total points invested: 10 + 20 = 30. Remaining: 5. After refund: 35. - - var inv: Array = [ - {"vnum": ClassSpecializationSystem.VNUM_RESET_SCROLL, "name": "洗髓卷轴", "count": 1} - ] - - var reset_sig := {"fired": false, "pts": 0} - sys.specialization_reset.connect(func(pts: int): - reset_sig["fired"] = true - reset_sig["pts"] = pts - ) - - var res = sys.reset_specialization(inv, player, false) - _assert(res["ok"] == true, "Reset succeeded") - _assert(inv[0] == null, "Reset scroll consumed") - _assert(player["skill_group"] == 0, "skill_group reset to 0") - _assert(player["skills"].is_empty(), "Old skills cleared") - _assert(player["skill_points"] == 35, "All 30 points refunded (5 + 30 = 35)") - _assert(reset_sig["fired"] == true and reset_sig["pts"] == 30, "specialization_reset emitted with 30 pts") - - # Now player can choose the other branch: Partisan (Mental Warrior) - var switch_res = sys.choose_specialization(2, player) - _assert(switch_res["ok"] == true, "Successfully switched to Mental Warrior (Partisan)") - _assert(player["skill_group"] == 2, "skill_group is now 2") - _assert(player["skills"].has(16) and player["skills"][16]["name"] == "强身术", "Has Strong Body (16)") - -func _finish() -> void: - print("\n==========================================") - print("Class Specialization Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Class Specialization parity tests passed!") - quit(0) diff --git a/project/test_class_specialization_parity.gd.uid b/project/test_class_specialization_parity.gd.uid deleted file mode 100644 index 3a1d1963..00000000 --- a/project/test_class_specialization_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://wq3ob7yiiyjt diff --git a/project/test_classic_pet_parity.gd b/project/test_classic_pet_parity.gd deleted file mode 100644 index 2f1fad89..00000000 --- a/project/test_classic_pet_parity.gd +++ /dev/null @@ -1,146 +0,0 @@ -# test_classic_pet_parity.gd —— 40250 经典封印宠物系统 1:1 回归测试套件 -extends SceneTree - -const ClassicPetSystem = preload("res://classic_pet_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_classic_pet_parity (Direction 1: Classic Pet System 1:1) ===") - _test_summon_and_buff_application() - _test_auto_unsummon_previous_pet() - _test_dungeon_only_buff_condition() - _test_follow_and_teleport_ai() - _test_serialization() - _finish() - -func _test_summon_and_buff_application() -> void: - print("\n--- Test 1: Summon & Buff Injection/Removal ---") - var sys := ClassicPetSystem.new() - var player := {"max_hp": 5000, "attack_pct": 0, "defense_pct": 0} - var seal_firedrake := {"vnum": ClassicPetSystem.SEAL_BABY_FIREDRAKE} - - var summon_sig := {"fired": false, "vnum": 0, "name": ""} - sys.pet_summoned.connect(func(v: int, n: String, _b: Dictionary): - summon_sig["fired"] = true - summon_sig["vnum"] = v - summon_sig["name"] = n - ) - - var unsummon_sig := {"fired": false, "vnum": 0} - sys.pet_unsummoned.connect(func(v: int, _n: String): - unsummon_sig["fired"] = true - unsummon_sig["vnum"] = v - ) - - # Summon Baby Firedrake - var res = sys.summon_pet(seal_firedrake, player, false, Vector2.ZERO) - _assert(res["ok"] == true, "Summon succeeded") - _assert(sys.is_summoned == true, "System reports pet is summoned") - _assert(sys.active_mob_vnum == ClassicPetSystem.MOB_BABY_FIREDRAKE, "Mob vnum is 34005") - _assert(player["max_hp"] == 6500, "HP increased by 1500 (5000 -> 6500)") - _assert(player["attack_pct"] == 15, "Attack pct increased by 15%") - _assert(summon_sig["fired"] == true, "pet_summoned signal emitted") - - # Unsummon - var un_res = sys.unsummon_pet(player) - _assert(un_res["ok"] == true, "Unsummon succeeded") - _assert(sys.is_summoned == false, "System reports pet unsummoned") - _assert(player["max_hp"] == 5000, "HP reversed back to 5000") - _assert(player["attack_pct"] == 0, "Attack pct reversed back to 0%") - _assert(unsummon_sig["fired"] == true, "pet_unsummoned signal emitted") - -func _test_auto_unsummon_previous_pet() -> void: - print("\n--- Test 2: Auto Unsummon Previous Pet When Summoning New ---") - var sys := ClassicPetSystem.new() - var player := {"monster_damage_pct": 0, "defense_pct": 0, "max_hp": 10000} - - # 1. Summon Boar (+15% monster damage) - sys.summon_pet({"vnum": ClassicPetSystem.SEAL_BABY_BOAR}, player, false) - _assert(player["monster_damage_pct"] == 15, "Boar buff applied (+15% monster dmg)") - - # 2. Directly summon Ice Phoenix (+15% def, +1500 HP) - sys.summon_pet({"vnum": ClassicPetSystem.SEAL_ICE_PHOENIX}, player, false) - _assert(sys.active_mob_vnum == ClassicPetSystem.MOB_ICE_PHOENIX, "Now Ice Phoenix is active") - _assert(player["monster_damage_pct"] == 0, "Previous Boar buff cleared") - _assert(player["defense_pct"] == 15, "Phoenix defense buff applied (+15%)") - _assert(player["max_hp"] == 11500, "Phoenix HP buff applied (+1500)") - -func _test_dungeon_only_buff_condition() -> void: - print("\n--- Test 3: 40250 Dungeon-Only Buff Validation (Baby Azrael) ---") - var sys := ClassicPetSystem.new() - var seal_azrael := {"vnum": ClassicPetSystem.SEAL_BABY_AZRAEL} - - # In normal field map (not in dungeon) - var player1 := {"max_hp": 5000, "dungeon_attack_pct": 0} - sys.summon_pet(seal_azrael, player1, false) - _assert(player1["max_hp"] == 6000, "Azrael standard HP (+1000) applied in field") - _assert(player1["dungeon_attack_pct"] == 0, "Dungeon attack buff not applied outside dungeon") - sys.unsummon_pet(player1) - - # Inside dungeon - var player2 := {"max_hp": 5000, "dungeon_attack_pct": 0} - sys.summon_pet(seal_azrael, player2, true) - _assert(player2["max_hp"] == 6000, "Azrael standard HP (+1000) applied in dungeon") - _assert(player2["dungeon_attack_pct"] == 20, "Dungeon attack buff (+20%) successfully applied in dungeon") - sys.unsummon_pet(player2) - _assert(player2["dungeon_attack_pct"] == 0, "Dungeon buff cleared after unsummon") - -func _test_follow_and_teleport_ai() -> void: - print("\n--- Test 4: Follow AI (Idle, Follow, Teleport) ---") - var sys := ClassicPetSystem.new() - var dummy_stats := {} - sys.summon_pet({"vnum": ClassicPetSystem.SEAL_BABY_BOAR}, dummy_stats, false, Vector2(0, 0)) - - # Owner is at (1, 0) -> dist is 1.5 - 1.0 = 0.5 (< 2.0 MIN_FOLLOW_DIST) - var ai_idle = sys.update_follow_ai(Vector2(1.0, 0.0), 0.1) - _assert(ai_idle["action"] == "idle", "AI stays idle when within 2.0m") - - # Owner walks away to (8, 0) -> dist > 2.0m - var ai_follow = sys.update_follow_ai(Vector2(8.0, 0.0), 0.5) - _assert(ai_follow["action"] == "follow", "AI follows owner when distance > 2.0m") - - # Owner warps/runs far away to (100, 100) -> dist > 15.0m - var move_sig := {"fired": false, "teleport": false} - sys.pet_moved.connect(func(_pos: Vector2, is_tele: bool): - move_sig["fired"] = true - move_sig["teleport"] = is_tele - ) - var ai_tele = sys.update_follow_ai(Vector2(100.0, 100.0), 0.1) - _assert(ai_tele["action"] == "teleport", "AI teleports when distance > 15.0m") - _assert(move_sig["teleport"] == true, "pet_moved signal reported teleport") - -func _test_serialization() -> void: - print("\n--- Test 5: State Serialization & Restoration ---") - var sys1 := ClassicPetSystem.new() - var dummy_stats := {"max_hp": 5000} - sys1.summon_pet({"vnum": ClassicPetSystem.SEAL_BABY_FIREDRAKE}, dummy_stats, false, Vector2(10, 20)) - - var data = sys1.serialize() - _assert(data["is_summoned"] == true, "Serialized is_summoned") - _assert(data["active_mob_vnum"] == ClassicPetSystem.MOB_BABY_FIREDRAKE, "Serialized mob vnum") - _assert(data["active_seal_vnum"] == ClassicPetSystem.SEAL_BABY_FIREDRAKE, "Serialized seal vnum") - - var sys2 := ClassicPetSystem.new() - sys2.deserialize(data) - _assert(sys2.is_summoned == true, "Restored is_summoned") - _assert(sys2.active_mob_vnum == ClassicPetSystem.MOB_BABY_FIREDRAKE, "Restored mob vnum") - _assert(sys2.active_pet_name == "幼火龙", "Restored pet name") - -func _finish() -> void: - print("\n=======================================================") - print("Classic Pet System Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_classic_pet_parity.gd.uid b/project/test_classic_pet_parity.gd.uid deleted file mode 100644 index 603b5f76..00000000 --- a/project/test_classic_pet_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://chiax3jol1idq diff --git a/project/test_client_phase_integration_parity.gd b/project/test_client_phase_integration_parity.gd deleted file mode 100644 index f46f12eb..00000000 --- a/project/test_client_phase_integration_parity.gd +++ /dev/null @@ -1,123 +0,0 @@ -# test_client_phase_integration_parity.gd —— 40250 客户端全状态机一体化回归测试 -extends SceneTree - -const SystemClass = preload("res://client_phase_integration_system.gd") - -var _passed: int = 0 -var _failed: int = 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_client_phase_integration_parity (Batch 30 Direction 3) ===") - _test_fresh_account_full_lifecycle_flow() - _test_existing_account_direct_select_flow() - _test_in_game_hotkey_and_focus_stack() - _test_logout_transitions() - - print("\n=== Result: %d Passed, %d Failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - else: - printerr("SOME TESTS FAILED!\n") - quit(0 if _failed == 0 else 1) - -func _test_fresh_account_full_lifecycle_flow() -> void: - print("\n--- Test 1: Fresh Account Full Lifecycle (Login -> Empire -> Create -> Select -> Loading -> Game) ---") - var client = SystemClass.new() - _assert(client.current_phase == SystemClass.ClientPhase.PHASE_LOGIN, "Starts in PHASE_LOGIN") - - # Register temporary fresh account - client.login_phase.registered_accounts["newbie"] = { "password": "123", "has_characters": false } - - # 1. Login - var r_log = client.process_login("newbie", "123") - _assert(r_log["success"] == true, "Login succeeded") - _assert(client.current_phase == SystemClass.ClientPhase.PHASE_EMPIRE, "Transitions to PHASE_EMPIRE for new account") - - # 2. Select Empire (Jinno = 3) - var r_emp = client.process_empire_select(3) - _assert(r_emp["success"] == true, "Empire selection confirmed") - _assert(client.current_phase == SystemClass.ClientPhase.PHASE_CREATE, "Transitions to PHASE_CREATE") - - # 3. Create Character - var r_cr = client.process_character_create("NewKnight", 3) - _assert(r_cr["success"] == true, "Created NewKnight successfully") - _assert(client.current_phase == SystemClass.ClientPhase.PHASE_SELECT, "Transitions to PHASE_SELECT") - - # 4. Start Game - var r_st = client.process_character_start(0) - _assert(r_st["success"] == true, "Started game from slot 0") - _assert(client.current_phase == SystemClass.ClientPhase.PHASE_GAME, "Transitions through Loading to PHASE_GAME") - _assert(client.active_character["name"] == "NewKnight", "Active character is NewKnight") - _assert(client.jukebox.is_playing == true, "BGM jukebox actively playing in-game music") - -func _test_existing_account_direct_select_flow() -> void: - print("\n--- Test 2: Existing Account Direct to Character Select ---") - var client = SystemClass.new() - - # Login with existing account (admin has characters) - var r_log = client.process_login("admin", "password") - _assert(r_log["success"] == true, "Admin logged in") - _assert(client.current_phase == SystemClass.ClientPhase.PHASE_SELECT, "Existing account transitions directly to PHASE_SELECT") - -func _test_in_game_hotkey_and_focus_stack() -> void: - print("\n--- Test 3: In-Game Hotkey Management & Escape Stack ---") - var client = SystemClass.new() - client.login_phase.registered_accounts["hero"] = { "password": "123", "has_characters": false } - client.process_login("hero", "123") - client.process_empire_select(1) - client.process_character_create("HeroPlayer", 1) - client.process_character_start(0) - _assert(client.current_phase == SystemClass.ClientPhase.PHASE_GAME, "In PHASE_GAME") - - # 1. Alt+M opens Messenger - var h_msg = client.handle_input_hotkey("Alt+M") - _assert(h_msg["action"] == "OPEN_MESSENGER", "Alt+M opens messenger") - _assert(client.dialog_focus_stack.has("MESSENGER"), "MESSENGER pushed to stack") - - # 2. M opens Atlas - var h_atlas = client.handle_input_hotkey("M") - _assert(h_atlas["action"] == "OPEN_ATLAS", "M opens atlas") - _assert(client.atlas.is_atlas_visible == true, "Atlas is visible") - _assert(client.dialog_focus_stack[client.dialog_focus_stack.size() - 1] == "ATLAS", "ATLAS on top of stack") - - # 3. First Escape closes top dialog (ATLAS) - var esc1 = client.handle_input_hotkey("Escape") - _assert(esc1["action"] == "CLOSED_TOP_DIALOG" and esc1["dialog"] == "ATLAS", "Escape popped ATLAS") - _assert(client.atlas.is_atlas_visible == false, "Atlas is hidden") - - # 4. Second Escape closes MESSENGER - var esc2 = client.handle_input_hotkey("Escape") - _assert(esc2["action"] == "CLOSED_TOP_DIALOG" and esc2["dialog"] == "MESSENGER", "Escape popped MESSENGER") - - # 5. Third Escape on empty stack opens System Menu - var esc3 = client.handle_input_hotkey("Escape") - _assert(esc3["action"] == "OPEN_SYSTEM_MENU", "Escape opens System Menu when stack is empty") - -func _test_logout_transitions() -> void: - print("\n--- Test 4: Logout Transitions ---") - var client = SystemClass.new() - client.login_phase.registered_accounts["user"] = { "password": "123", "has_characters": false } - client.process_login("user", "123") - client.process_empire_select(1) - client.process_character_create("UserChar", 1) - client.process_character_start(0) - - # Logout to Select - client.logout_to_select() - _assert(client.current_phase == SystemClass.ClientPhase.PHASE_SELECT, "Logged out to PHASE_SELECT") - _assert(client.active_character.is_empty(), "Active character cleared") - _assert(client.jukebox.current_track == "characterselect.mp3", "Switched BGM to character select theme") - - # Logout to Login - client.logout_to_login() - _assert(client.current_phase == SystemClass.ClientPhase.PHASE_LOGIN, "Logged out to PHASE_LOGIN") - _assert(client.active_account.is_empty(), "Active account cleared") - _assert(client.jukebox.current_track == "enter_the_east.mp3", "Switched BGM to login theme") diff --git a/project/test_client_phase_integration_parity.gd.uid b/project/test_client_phase_integration_parity.gd.uid deleted file mode 100644 index 6f4463b2..00000000 --- a/project/test_client_phase_integration_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bd15xmtc2ol1e diff --git a/project/test_combat_replay_snapshot_parity.gd b/project/test_combat_replay_snapshot_parity.gd deleted file mode 100644 index 29bb884c..00000000 --- a/project/test_combat_replay_snapshot_parity.gd +++ /dev/null @@ -1,140 +0,0 @@ -# test_combat_replay_snapshot_parity.gd —— 40250 战斗高光战报与首领击杀快照 1:1 回归测试套件 -extends SceneTree - -const CombatReplaySnapshotSystem = preload("res://combat_replay_snapshot_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_combat_replay_snapshot_parity (Direction 4: Combat Highlights 1:1) ===") - _test_combat_session_tracking() - _test_highlight_card_generation_and_rating() - _test_personal_bests_and_records() - _test_history_buffer_cap() - _finish() - -func _test_combat_session_tracking() -> void: - print("\n--- Test 1: Session Tracking & Hit Metrics ---") - var cr := CombatReplaySnapshotSystem.new() - - var start_sig := {"fired": false, "name": ""} - cr.combat_session_started.connect(func(n: String): - start_sig["fired"] = true - start_sig["name"] = n - ) - - var s_res = cr.start_combat_session(1901, "九尾妖狐", 280000) - _assert(s_res["ok"] == true, "Session started") - _assert(cr.is_in_session == true, "is_in_session is true") - _assert(start_sig["fired"] == true and start_sig["name"] == "九尾妖狐", "combat_session_started emitted") - - # Record attacks - cr.record_hit(1500, false) - cr.record_hit(3000, true) # Crit - cr.record_hit(8500, true, "三连斩") # Skill crit, highest hit so far - cr.record_damage_taken(650) - - _assert(cr.total_damage_dealt == 13000, "Total damage dealt is 13000 (1500 + 3000 + 8500)") - _assert(cr.total_attacks == 3, "Total attacks is 3") - _assert(cr.critical_hits_count == 2, "2 critical hits") - _assert(cr.total_skills_used == 1, "1 skill used") - _assert(cr.max_single_hit == 8500, "Highest single hit tracked as 8500") - _assert(cr.total_damage_taken == 650, "Damage taken tracked as 650") - -func _test_highlight_card_generation_and_rating() -> void: - print("\n--- Test 2: Highlight Card Generation & S-Rank Rating ---") - var cr := CombatReplaySnapshotSystem.new() - cr.start_combat_session(1304, "黄虎领主", 320000) - - var card_sig := {"fired": false, "rank": ""} - cr.combat_highlight_generated.connect(func(c: Dictionary): - card_sig["fired"] = true - card_sig["rank"] = c["rank"] - ) - - # Simulate 10 seconds of combat with 50,000 total damage (DPS = 5000 -> S Rank) - cr.update(10.0) - cr.record_hit(20000, true, "旋风斩") - cr.record_hit(30000, true, "剑气斩") - - var drops: Array = [{"vnum": 50513, "name": "灵魂之石", "count": 1}] - var card = cr.end_combat_session(true, drops) - - _assert(cr.is_in_session == false, "Session ended") - _assert(card["target_name"] == "黄虎领主", "Card target name matches") - _assert(card["total_damage"] == 50000, "Card total damage is 50000") - _assert(card["dps"] == 5000, "DPS is 5000 (50000 / 10s)") - _assert(card["rank"] == "S", "Awarded S Rank (DPS >= 4000)") - _assert(card["drops"].size() == 1, "Drops included in highlight card") - _assert(card_sig["fired"] == true and card_sig["rank"] == "S", "combat_highlight_generated signal fired") - -func _test_personal_bests_and_records() -> void: - print("\n--- Test 3: Personal Best Records & Signals ---") - var cr := CombatReplaySnapshotSystem.new() - - var record_sig := {"fired": false, "type": "", "val": 0} - cr.new_personal_record.connect(func(_t: String, rtype: String, val: int): - record_sig["fired"] = true - record_sig["type"] = rtype - record_sig["val"] = val - ) - - # 1st Kill: Nine Tails, 10s, 30000 dmg, DPS = 3000, max hit = 10000 - cr.start_combat_session(1901, "九尾妖狐", 280000) - cr.update(10.0) - cr.record_hit(10000, true) - cr.record_hit(20000, true) - cr.end_combat_session(true) - - _assert(record_sig["fired"] == true and record_sig["type"] == "FIRST_KILL", "First kill record emitted") - var pb1 = cr.get_personal_best("九尾妖狐") - _assert(pb1["best_dps"] == 3000, "Recorded best DPS 3000") - _assert(pb1["highest_hit"] == 20000, "Recorded highest hit 20000") - - # 2nd Kill: Nine Tails, faster kill (5s), DPS = 8000, max hit = 35000 - record_sig["fired"] = false - cr.start_combat_session(1901, "九尾妖狐", 280000) - cr.update(5.0) - cr.record_hit(35000, true, "大绝招") - cr.record_hit(5000, false) - cr.end_combat_session(true) - - _assert(record_sig["fired"] == true, "New personal record emitted") - var pb2 = cr.get_personal_best("九尾妖狐") - _assert(pb2["best_dps"] == 8000, "Best DPS updated to 8000") - _assert(pb2["highest_hit"] == 35000, "Highest hit updated to 35000") - _assert(pb2["fastest_time"] == 5.0, "Fastest time updated to 5.0s") - -func _test_history_buffer_cap() -> void: - print("\n--- Test 4: History Buffer Cap (Max 20 Cards) ---") - var cr := CombatReplaySnapshotSystem.new() - for i in range(25): - cr.start_combat_session(1, "Mob_%d" % i, 100) - cr.update(1.0) - cr.record_hit(100, false) - cr.end_combat_session(true) - - var recents = cr.get_recent_highlights() - _assert(recents.size() == CombatReplaySnapshotSystem.MAX_STORED_CARDS, "History capped at max 20 cards") - _assert(recents[0]["target_name"] == "Mob_24", "Most recent card is at front") - -func _finish() -> void: - print("\n==========================================") - print("Combat Replay Snapshot Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Combat Replay Snapshot parity tests passed!") - quit(0) diff --git a/project/test_combat_replay_snapshot_parity.gd.uid b/project/test_combat_replay_snapshot_parity.gd.uid deleted file mode 100644 index b9a99e14..00000000 --- a/project/test_combat_replay_snapshot_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c10cjpkxxmlcn diff --git a/project/test_combat_status_effect_parity.gd b/project/test_combat_status_effect_parity.gd deleted file mode 100644 index ee2740b1..00000000 --- a/project/test_combat_status_effect_parity.gd +++ /dev/null @@ -1,133 +0,0 @@ -# test_combat_status_effect_parity.gd —— Metin2 40250 战斗状态异常与负面减益 1:1 测试套件 -extends SceneTree - -const CombatStatusEffectSystem = preload("res://combat_status_effect_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_combat_status_effect_parity (Direction 2: Combat Status Effects 1:1) ===") - - test_stun_and_immunity() - test_slow_and_immunity() - test_poison_mechanics_and_regen_block() - test_fire_damage_ticks() - test_cure_and_serialization() - - print("\n=======================================================") - print("Combat Status Effect Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - - if _failed > 0: - quit(1) - else: - quit(0) - -func test_stun_and_immunity() -> void: - print("\n--- Test 1: Stun & IMMUNE_STUN (40250 1:1) ---") - var system = CombatStatusEffectSystem.new() - - var attacker = {"id": 1, "is_pc": true, "stun_pct": 20} - var victim_normal = {"id": 101, "is_pc": false, "immune_stun": false} - var victim_shielded = {"id": 102, "is_pc": false, "immune_stun": true} - - # 1. 普通怪物被击中眩晕 (PvE 4秒) - var res = system.try_apply_normal_attack_effects(attacker, victim_normal, {"stun_pct_roll": 10}) - _assert(res["applied"].has(CombatStatusEffectSystem.EFFECT_STUN), "Applied stun to normal monster") - _assert(system.is_stunned(101) == true, "Target 101 is stunned") - - # 2. 携带免疫眩晕盾牌的目标 (IMMUNE_STUN) -> 100% 抵抗 - res = system.try_apply_normal_attack_effects(attacker, victim_shielded, {"stun_pct_roll": 5}) - _assert(not res["applied"].has(CombatStatusEffectSystem.EFFECT_STUN), "Stun avoided") - _assert(res["resisted"].has("immune_stun"), "Resisted via immune_stun") - _assert(system.is_stunned(102) == false, "Target 102 is not stunned") - -func test_slow_and_immunity() -> void: - print("\n--- Test 2: Slow & IMMUNE_SLOW (40250 1:1) ---") - var system = CombatStatusEffectSystem.new() - - var attacker = {"id": 1, "slow_pct": 15} - var victim_normal = {"id": 201, "immune_slow": false} - var victim_immune = {"id": 202, "immune_slow": true} - - # 1. 成功施加减速 - var res = system.try_apply_normal_attack_effects(attacker, victim_normal, {"slow_pct_roll": 10}) - _assert(res["applied"].has(CombatStatusEffectSystem.EFFECT_SLOW), "Applied slow") - _assert(system.get_slow_reduction(201) == 30, "Speed reduced by 30 points") - - # 2. 免疫减速抵抗 - res = system.try_apply_normal_attack_effects(attacker, victim_immune, {"slow_pct_roll": 5}) - _assert(res["resisted"].has("immune_slow"), "Resisted via immune_slow") - _assert(system.get_slow_reduction(202) == 0, "No speed reduction") - -func test_poison_mechanics_and_regen_block() -> void: - print("\n--- Test 3: Poison Damage, HP Regen Block & Monster Single Poison (40250 1:1) ---") - var system = CombatStatusEffectSystem.new() - - var attacker = {"id": 1, "is_pc": true, "level": 50, "poison_pct": 100} - var monster = {"id": 301, "is_pc": false, "level": 50, "max_hp": 2000, "hp": 2000, "has_been_poisoned": false} - - # 1. 成功施加中毒 - var res = system.try_apply_normal_attack_effects(attacker, monster, {"poison_pct_roll": 50}) - _assert(res["applied"].has(CombatStatusEffectSystem.EFFECT_POISON), "Applied poison") - _assert(system.has_effect(301, CombatStatusEffectSystem.EFFECT_POISON) == true, "Monster has poison effect") - _assert(system.is_natural_hp_regen_blocked(301) == true, "Poison blocks natural HP regen") - _assert(monster["has_been_poisoned"] == true, "Monster recorded has_been_poisoned") - - # 2. 心跳更新 3 秒触发跳毒 (5% of 2000 = 100 伤害) - var targets_map = {301: monster} - system.update(3.0, targets_map) - _assert(monster["hp"] == 1900, "HP reduced from 2000 to 1900 after first poison tick") - - # 3. 怪物在解除中毒后不再被第二次中毒 (40250 char_resist.cpp: m_bHasPoisoned && !IsPC()) - system.clear_effect(301, CombatStatusEffectSystem.EFFECT_POISON) - res = system.try_apply_normal_attack_effects(attacker, monster, {"poison_pct_roll": 10}) - _assert(not res["applied"].has(CombatStatusEffectSystem.EFFECT_POISON), "Monster rejected second poison") - _assert(res["resisted"].has("poison_already_poisoned_once"), "Resisted as monster poisoned once") - -func test_fire_damage_ticks() -> void: - print("\n--- Test 4: Fire Burn Damage Ticks (40250 1:1) ---") - var system = CombatStatusEffectSystem.new() - - var victim = {"id": 401, "hp": 500} - var targets_map = {401: victim} - - # 施加烈焰灼烧: 3 次跳伤,每次 60 点 - system.apply_fire(401, 3, 60) - _assert(system.has_effect(401, CombatStatusEffectSystem.EFFECT_FIRE) == true, "Has fire effect") - - # 经过 3 秒 - system.update(3.0, targets_map) - _assert(victim["hp"] == 440, "HP dropped by 60 to 440") - - # 经过 6 秒 (跳完剩余 2 次,共 180 伤害,生命为 500 - 180 = 320) - system.update(6.0, targets_map) - _assert(victim["hp"] == 320, "HP dropped to 320") - _assert(system.has_effect(401, CombatStatusEffectSystem.EFFECT_FIRE) == false, "Fire effect expired") - -func test_cure_and_serialization() -> void: - print("\n--- Test 5: Curing & State Serialization ---") - var system = CombatStatusEffectSystem.new() - system.apply_stun(501, 4.0) - var victim = {"id": 502, "max_hp": 1000, "hp": 1000} - system.apply_poison(victim) - - _assert(system.has_effect(502, CombatStatusEffectSystem.EFFECT_POISON) == true, "Poison active") - # 使用解毒药剂清除中毒 - var cleared = system.clear_effect(502, CombatStatusEffectSystem.EFFECT_POISON) - _assert(cleared == true, "Poison cleared via antidote") - _assert(system.has_effect(502, CombatStatusEffectSystem.EFFECT_POISON) == false, "No longer poisoned") - - var data = system.serialize() - var restored = CombatStatusEffectSystem.new() - restored.deserialize(data) - _assert(restored.is_stunned(501) == true, "Restored stun effect") diff --git a/project/test_combat_status_effect_parity.gd.uid b/project/test_combat_status_effect_parity.gd.uid deleted file mode 100644 index 57e689e7..00000000 --- a/project/test_combat_status_effect_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dq2028ngo6up5 diff --git a/project/test_compass_treasure_hunt_parity.gd b/project/test_compass_treasure_hunt_parity.gd deleted file mode 100644 index b68f4184..00000000 --- a/project/test_compass_treasure_hunt_parity.gd +++ /dev/null @@ -1,145 +0,0 @@ -# test_compass_treasure_hunt_parity.gd —— 40250 魔石地脉探测罗盘与寻宝系统 1:1 回归测试套件 -extends SceneTree - -const CompassTreasureHuntSystem = preload("res://compass_treasure_hunt_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_compass_treasure_hunt_parity (Direction 4: Compass Treasure Hunt 1:1) ===") - _test_compass_scanning_and_charges() - _test_excavation_at_dowsing_node() - _test_key_opening() - _finish() - -func _test_compass_scanning_and_charges() -> void: - print("\n--- Test 1: Compass Scanning & 6-Charge Depletion ---") - var sys := CompassTreasureHuntSystem.new() - var inv: Array = [ - {"vnum": CompassTreasureHuntSystem.VNUM_COMPASS_OF_METIN, "name": "魔石探测罗盘", "charges": 6} - ] - var player_pos := Vector2(0, 0) - var metins: Array = [ - {"name": "战斗魔石", "pos": Vector2(300, 300)}, - {"name": "嫉妒魔石", "pos": Vector2(10, 10)}, # dist ~14.14m (VERY_CLOSE) - {"name": "阴影魔石", "pos": Vector2(150, 20)} - ] - - var scan_sig := {"fired": false, "target": "", "dist": 0.0, "prox": "", "charges": 0} - sys.compass_scanned.connect(func(t: String, d: float, _a: float, p: String, c: int): - scan_sig["fired"] = true - scan_sig["target"] = t - scan_sig["dist"] = d - scan_sig["prox"] = p - scan_sig["charges"] = c - ) - - # Scan 1 - var res1 = sys.scan_nearest_metin(player_pos, metins, inv, 0) - _assert(res1["ok"] == true, "Compass scan 1 succeeded") - _assert(res1["target_name"] == "嫉妒魔石", "Nearest metin is 嫉妒魔石") - _assert(res1["proximity"] == CompassTreasureHuntSystem.PROXIMITY_VERY_CLOSE, "Proximity is VERY_CLOSE (< 20m)") - _assert(res1["charges_left"] == 5, "Charges left is 5") - _assert(scan_sig["fired"] == true, "compass_scanned signal emitted") - - # Deplete remaining 5 charges - for i in range(4): - sys.scan_nearest_metin(player_pos, metins, inv, 0) - - _assert(inv[0]["charges"] == 1, "1 charge remaining before last use") - - # 6th scan consumes the compass completely - var res6 = sys.scan_nearest_metin(player_pos, metins, inv, 0) - _assert(res6["ok"] == true, "6th scan succeeded") - _assert(res6["charges_left"] == 0, "0 charges left") - _assert(inv[0] == null, "Compass destroyed after 6 charges (slot 0 is null)") - -func _test_excavation_at_dowsing_node() -> void: - print("\n--- Test 2: Treasure Excavation Nearby (< 5m) ---") - var sys := CompassTreasureHuntSystem.new() - var inv: Array = [null, null, null] - var nodes: Array = [ - {"pos": Vector2(50, 50), "is_gold": true}, - {"pos": Vector2(200, 200), "is_gold": false} - ] - - # 1. Player is far away at (0, 0) - var res_far = sys.excavate_treasure(Vector2(0, 0), nodes, inv) - _assert(res_far["ok"] == false, "Excavation rejected when > 5m") - _assert(res_far["reason"] == "NO_TREASURE_NEARBY", "Reason is NO_TREASURE_NEARBY") - - var exc_sig := {"fired": false, "vnum": 0, "name": ""} - sys.treasure_excavated.connect(func(v: int, n: String): - exc_sig["fired"] = true - exc_sig["vnum"] = v - exc_sig["name"] = n - ) - - # 2. Player approaches to (51, 50) -> dist is 1m <= 5m - var res_succ = sys.excavate_treasure(Vector2(51, 50), nodes, inv) - _assert(res_succ["ok"] == true, "Excavation succeeded") - _assert(inv[0] != null and inv[0]["vnum"] == CompassTreasureHuntSystem.VNUM_GOLD_CHEST, "Golden Chest placed into slot 0") - _assert(exc_sig["fired"] == true, "treasure_excavated signal emitted") - _assert(nodes.size() == 1, "Excavated node removed from map") - -func _test_key_opening() -> void: - print("\n--- Test 3: Golden/Silver Key Chest Opening ---") - var sys := CompassTreasureHuntSystem.new() - var inv: Array = [ - {"vnum": CompassTreasureHuntSystem.VNUM_GOLD_CHEST, "name": "黄金宝箱"}, - {"vnum": CompassTreasureHuntSystem.VNUM_SILVER_KEY, "name": "白银钥匙", "count": 1}, - {"vnum": CompassTreasureHuntSystem.VNUM_GOLD_KEY, "name": "黄金钥匙", "count": 1}, - null, null, null - ] - - # 1. Open Golden Chest with Silver Key -> mismatch - var res_mismatch = sys.open_treasure_chest(0, 1, inv) - _assert(res_mismatch["ok"] == false, "Key mismatch rejected") - _assert(res_mismatch["reason"] == "KEY_MISMATCH", "Reason is KEY_MISMATCH") - - var open_sig := {"fired": false, "rewards": []} - sys.treasure_opened.connect(func(_n: String, rew: Array): - open_sig["fired"] = true - open_sig["rewards"] = rew - ) - - # 2. Open Golden Chest with Golden Key (slot 0 & slot 2) - var res_succ = sys.open_treasure_chest(0, 2, inv) - var chest_remains = false - var key_remains = false - for it in inv: - if it != null: - if int(it.get("vnum", 0)) == CompassTreasureHuntSystem.VNUM_GOLD_CHEST: - chest_remains = true - elif int(it.get("vnum", 0)) == CompassTreasureHuntSystem.VNUM_GOLD_KEY: - key_remains = true - _assert(not chest_remains, "Chest consumed") - _assert(not key_remains, "Key consumed") - _assert(open_sig["fired"] == true, "treasure_opened signal emitted") - _assert(open_sig["rewards"].size() == 3, "Received 3 rare rewards") - - # Check blessing scroll in inventory - var scroll_found := false - for it in inv: - if it != null and int(it.get("vnum", 0)) == 25040: - scroll_found = true - break - _assert(scroll_found == true, "Blessing Scroll found in rewards") - -func _finish() -> void: - print("\n=======================================================") - print("Compass & Treasure Hunt Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_compass_treasure_hunt_parity.gd.uid b/project/test_compass_treasure_hunt_parity.gd.uid deleted file mode 100644 index 34c5d8f8..00000000 --- a/project/test_compass_treasure_hunt_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://k8bgm61rlj2t diff --git a/project/test_costume_attr_transfer_parity.gd b/project/test_costume_attr_transfer_parity.gd deleted file mode 100644 index 5578e389..00000000 --- a/project/test_costume_attr_transfer_parity.gd +++ /dev/null @@ -1,136 +0,0 @@ -# test_costume_attr_transfer_parity.gd —— Metin2 40250 时装属性转移与增益附魔 1:1 测试套件 -extends SceneTree - -const CostumeAttrTransferSystem = preload("res://costume_attr_transfer_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_costume_attr_transfer_parity (Direction 3: Costume Attr Transfer 1:1) ===") - - test_costume_transform_70063() - test_costume_enchant_70064() - test_costume_attribute_transfer() - test_rejections_and_safety() - - print("\n=======================================================") - print("Costume Attr Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - - if _failed > 0: - quit(1) - else: - quit(0) - -func test_costume_transform_70063() -> void: - print("\n--- Test 1: Costume Transform 70063 (40250 1:1) ---") - var system = CostumeAttrTransferSystem.new() - - var transformed_signals := [] - system.costume_transformed.connect(func(vnum, attrs): - transformed_signals.append({"vnum": vnum, "attrs": attrs}) - ) - - var inv: Array = [ - {"vnum": 70063, "count": 2}, # 重铸卷轴 x2 (slot 0) - {"vnum": 41001, "costume_subtype": "body", "attrs": []} # 基础时装铠甲 (slot 1) - ] - - # 重铸时装,强制赋予 3 条属性 - var custom_attrs = [ - {"type": "max_hp", "name": "生命上限", "value": 2000}, - {"type": "crit_pct", "name": "暴击几率", "value": 10}, - {"type": "att_speed", "name": "攻击速度", "value": 8} - ] - var res = system.transform_costume(inv, 0, 1, 3, custom_attrs) - _assert(res["ok"] == true, "Transformed costume successfully") - _assert(res["attrs_count"] == 3, "Acquired 3 attributes") - _assert(inv[1]["attrs"].size() == 3, "Costume holds 3 attributes in inventory") - _assert(inv[1]["attrs"][0]["value"] == 2000, "First attribute is 2000 HP") - _assert(inv[0]["count"] == 1, "Scroll count decremented from 2 to 1") - _assert(transformed_signals.size() == 1, "costume_transformed signal fired") - -func test_costume_enchant_70064() -> void: - print("\n--- Test 2: Costume Enchant 70064 (40250 1:1) ---") - var system = CostumeAttrTransferSystem.new() - - var inv: Array = [ - {"vnum": 70064, "count": 1}, # 附魔石 (slot 0) - {"vnum": 41001, "costume_subtype": "body", "attrs": [ - {"type": "max_hp", "value": 500}, - {"type": "max_sp", "value": 200} - ]}, # 带有 2 条词条的时装 (slot 1) - {"vnum": 41002, "costume_subtype": "body", "attrs": []} # 0 词条时装 (slot 2) - ] - - # 1. 尝试对 0 词条时装附魔 -> 拦截 - var res = system.enchant_costume(inv, 0, 2) - _assert(res["ok"] == false, "Rejected enchanting costume with 0 attributes") - _assert(res["reason"] == "NO_ATTRS_TO_ENCHANT", "Reason is NO_ATTRS_TO_ENCHANT") - - # 2. 对 2 词条时装附魔 -> 成功保留 2 词条结构并刷新 - var new_custom = [ - {"type": "crit_pct", "name": "暴击几率", "value": 10}, - {"type": "penetrate_pct", "name": "穿刺伤害几率", "value": 10} - ] - res = system.enchant_costume(inv, 0, 1, new_custom) - _assert(res["ok"] == true, "Enchanted costume successfully") - _assert(res["attrs_count"] == 2, "Preserved 2 attributes count") - _assert(inv[1]["attrs"][0]["type"] == "crit_pct", "New first attribute is crit_pct") - _assert(inv[0] == null, "Consumed enchant scroll to null") - -func test_costume_attribute_transfer() -> void: - print("\n--- Test 3: Costume Attribute Transfer 72325 (40250 1:1) ---") - var system = CostumeAttrTransferSystem.new() - - var inv: Array = [ - {"vnum": 72325, "count": 1}, # 转移符 (slot 0) - {"vnum": 41001, "costume_subtype": "body", "attrs": [ - {"type": "max_hp", "value": 2000}, - {"type": "crit_pct", "value": 10} - ]}, # 旧时装铠甲 (slot 1) - {"vnum": 41050, "costume_subtype": "body", "attrs": []}, # 崭新神圣铠甲 (slot 2) - {"vnum": 45001, "costume_subtype": "hair", "attrs": []} # 时装发型 (slot 3, 异类) - ] - - # 1. 铠甲转移给发型 -> 异类拦截 - var res = system.transfer_costume_attributes(inv, 0, 1, 3) - _assert(res["ok"] == false, "Rejected mismatch transfer (body to hair)") - _assert(res["reason"] == "TYPE_MISMATCH", "Reason is TYPE_MISMATCH") - - # 2. 铠甲转移给铠甲 -> 成功 - res = system.transfer_costume_attributes(inv, 0, 1, 2) - _assert(res["ok"] == true, "Transfer body to body succeeded") - _assert(inv[2]["attrs"].size() == 2, "Destination costume received 2 attributes") - _assert(inv[2]["attrs"][0]["value"] == 2000, "Transferred 2000 HP") - _assert(inv[1]["attrs"].is_empty(), "Source costume attributes cleared") - _assert(inv[0] == null, "Consumed transfer scroll") - -func test_rejections_and_safety() -> void: - print("\n--- Test 4: Safety Rejections (Equipped & Non-costume) ---") - var system = CostumeAttrTransferSystem.new() - - var inv: Array = [ - {"vnum": 70063, "count": 1}, - {"vnum": 149, "attrs": []}, # 普通武器战神之剑+9 (slot 1, 非时装) - {"vnum": 41001, "costume_subtype": "body", "is_equipped": true, "attrs": []} # 穿戴中时装 (slot 2) - ] - - # 1. 非时装物品 -> 拦截 - var res = system.transform_costume(inv, 0, 1) - _assert(res["ok"] == false, "Rejected non-costume item") - _assert(res["reason"] == "NOT_COSTUME", "Reason is NOT_COSTUME") - - # 2. 穿戴中时装 -> 拦截 - res = system.transform_costume(inv, 0, 2) - _assert(res["ok"] == false, "Rejected equipped costume") - _assert(res["reason"] == "EQUIPPED_ITEM", "Reason is EQUIPPED_ITEM") diff --git a/project/test_costume_attr_transfer_parity.gd.uid b/project/test_costume_attr_transfer_parity.gd.uid deleted file mode 100644 index 427dd320..00000000 --- a/project/test_costume_attr_transfer_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bfnliwq4oxrju diff --git a/project/test_costume_system_parity.gd b/project/test_costume_system_parity.gd deleted file mode 100644 index 36668215..00000000 --- a/project/test_costume_system_parity.gd +++ /dev/null @@ -1,146 +0,0 @@ -# test_costume_system_parity.gd —— 40250 独立时装衣柜系统 1:1 纯净回归测试套件 -extends SceneTree - -const CostumeSystem = preload("res://costume_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_costume_system_parity (Direction 1: Costume System 1:1) ===") - _test_slot_type_validation() - _test_costume_equip_and_unequip() - _test_stat_stacking_and_appearance() - _test_costume_expiration() - _finish() - -func _test_slot_type_validation() -> void: - print("\n--- Test 1: Slot Type & Item Category Validation ---") - var cs := CostumeSystem.new() - - var body_costume := {"vnum": 41001, "name": "沙漠战袍", "costume_type": "body", "model_id": 101} - var hair_costume := {"vnum": 45001, "name": "头巾发型", "costume_type": "hair", "model_id": 201} - - # Invalid slot - var r_bad = cs.equip_costume(99, body_costume) - _assert(r_bad["ok"] == false and r_bad["reason"] == "INVALID_SLOT", "Invalid slot rejected") - - # Put body costume into hair slot - var r_mismatch1 = cs.equip_costume(CostumeSystem.CostumeSlot.COSTUME_HAIR, body_costume) - _assert(r_mismatch1["ok"] == false and r_mismatch1["reason"] == "SLOT_TYPE_MISMATCH", "Body costume rejected in Hair slot") - - # Put hair costume into weapon slot - var r_mismatch2 = cs.equip_costume(CostumeSystem.CostumeSlot.COSTUME_WEAPON, hair_costume) - _assert(r_mismatch2["ok"] == false and r_mismatch2["reason"] == "SLOT_TYPE_MISMATCH", "Hair costume rejected in Weapon slot") - -func _test_costume_equip_and_unequip() -> void: - print("\n--- Test 2: Equip & Unequip Operations ---") - var cs := CostumeSystem.new() - var body_item := { - "vnum": 41001, - "name": "黑骑士战袍", - "costume_type": "body", - "model_id": 105, - "bonuses": {"max_hp": 1000} - } - - var equip_signal := {"fired": false, "slot": -1} - cs.costume_equipped.connect(func(s: int, _it: Dictionary): - equip_signal["fired"] = true - equip_signal["slot"] = s - ) - - var unequip_signal := {"fired": false, "slot": -1} - cs.costume_unequipped.connect(func(s: int): - unequip_signal["fired"] = true - unequip_signal["slot"] = s - ) - - # Equip - var r_eq = cs.equip_costume(CostumeSystem.CostumeSlot.COSTUME_BODY, body_item) - _assert(r_eq["ok"] == true, "Body costume equipped successfully") - _assert(cs.equipped_costumes[CostumeSystem.CostumeSlot.COSTUME_BODY] != null, "Body slot occupied") - _assert(equip_signal["fired"] == true and equip_signal["slot"] == CostumeSystem.CostumeSlot.COSTUME_BODY, "costume_equipped signal fired") - - # Unequip - var r_uneq = cs.unequip_costume(CostumeSystem.CostumeSlot.COSTUME_BODY) - _assert(r_uneq["ok"] == true, "Body costume unequipped") - _assert(cs.equipped_costumes[CostumeSystem.CostumeSlot.COSTUME_BODY] == null, "Body slot now null") - _assert(unequip_signal["fired"] == true and unequip_signal["slot"] == CostumeSystem.CostumeSlot.COSTUME_BODY, "costume_unequipped signal fired") - - # Unequip empty slot - var r_empty = cs.unequip_costume(CostumeSystem.CostumeSlot.COSTUME_BODY) - _assert(r_empty["ok"] == false and r_empty["reason"] == "SLOT_EMPTY", "Unequipping empty slot rejected") - -func _test_stat_stacking_and_appearance() -> void: - print("\n--- Test 3: Stat Stacking & 3D Appearance Overrides ---") - var cs := CostumeSystem.new() - - var body := {"vnum": 41001, "costume_type": "body", "model_id": 101, "bonuses": {"max_hp": 1000, "att_pct": 5}} - var hair := {"vnum": 45001, "costume_type": "hair", "model_id": 202, "bonuses": {"max_hp": 500, "cast_speed": 10}} - var weapon := {"vnum": 49001, "costume_type": "weapon", "model_id": 303, "bonuses": {"monster_dmg_pct": 10}} - - cs.equip_costume(CostumeSystem.CostumeSlot.COSTUME_BODY, body) - cs.equip_costume(CostumeSystem.CostumeSlot.COSTUME_HAIR, hair) - cs.equip_costume(CostumeSystem.CostumeSlot.COSTUME_WEAPON, weapon) - - # Verify Stat Stacking - var bonuses = cs.get_costume_bonuses() - _assert(bonuses["max_hp"] == 1500, "Stacked Max HP is 1500 (1000 + 500)") - _assert(bonuses["att_pct"] == 5, "ATK is +5%") - _assert(bonuses["cast_speed"] == 10, "Casting Speed is +10") - _assert(bonuses["monster_dmg_pct"] == 10, "Monster Dmg is +10%") - - # Verify 3D Model Overrides - var models = cs.get_appearance_overrides() - _assert(models["body_model"] == 101, "Body model is 101") - _assert(models["hair_model"] == 202, "Hair model is 202") - _assert(models["weapon_model"] == 303, "Weapon model is 303") - -func _test_costume_expiration() -> void: - print("\n--- Test 4: Duration Decay & Expiration Auto-Unequip ---") - var cs := CostumeSystem.new() - - var temp_body := { - "vnum": 41002, - "name": "限时战袍", - "costume_type": "body", - "remaining_time": 10.0, - "bonuses": {"max_hp": 800} - } - cs.equip_costume(CostumeSystem.CostumeSlot.COSTUME_BODY, temp_body) - - var expire_signal := {"fired": false, "name": ""} - cs.costume_expired.connect(func(_s: int, iname: String): - expire_signal["fired"] = true - expire_signal["name"] = iname - ) - - # Update 5.0 seconds -> still active - cs.update(5.0) - _assert(cs.equipped_costumes[CostumeSystem.CostumeSlot.COSTUME_BODY] != null, "Still active after 5s") - _assert(cs.get_costume_bonuses()["max_hp"] == 800, "Bonus HP still active") - - # Update 6.0 seconds -> expired! - var expired = cs.update(6.0) - _assert(expired.size() == 1, "1 item expired") - _assert(cs.equipped_costumes[CostumeSystem.CostumeSlot.COSTUME_BODY] == null, "Auto-unequipped on expiration") - _assert(expire_signal["fired"] == true and expire_signal["name"] == "限时战袍", "costume_expired signal fired") - _assert(cs.get_costume_bonuses().is_empty(), "Bonuses cleared after expiration") - -func _finish() -> void: - print("\n==========================================") - print("Costume System Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_costume_system_parity.gd.uid b/project/test_costume_system_parity.gd.uid deleted file mode 100644 index 5533c5de..00000000 --- a/project/test_costume_system_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dcffixkinh5pd diff --git a/project/test_cube_crafting_parity.gd b/project/test_cube_crafting_parity.gd deleted file mode 100644 index cdb32e90..00000000 --- a/project/test_cube_crafting_parity.gd +++ /dev/null @@ -1,158 +0,0 @@ -# test_cube_crafting_parity.gd —— Metin2 40250 官方魔方炼金合成台 1:1 测试套件 -extends SceneTree - -const CubeCraftingSystem = preload("res://cube_crafting_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_cube_crafting_parity (Direction 3: Cube Crafting System 1:1) ===") - - test_baek_go_herb_crafting() - test_huahn_so_dew_crafting() - test_jae_seon_kim_triton_sword() - test_recipe_and_npc_mismatch() - test_history_and_serialization() - - print("\n=======================================================") - print("Cube Crafting Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - - if _failed > 0: - quit(1) - else: - quit(0) - -func test_baek_go_herb_crafting() -> void: - print("\n--- Test 1: Baek-Go Herb Juice Crafting (40250 1:1) ---") - var system = CubeCraftingSystem.new() - - var opened_emitted := [] - system.cube_opened.connect(func(npc, name): - opened_emitted.append({"npc": npc, "name": name}) - ) - - # 打开白高魔方界面 - _assert(system.open_cube(20018) == true, "Opened cube at Baek-Go (20018)") - _assert(opened_emitted.size() == 1, "cube_opened emitted") - - # 放入 1 朵桃花 (50721) - var slots: Array = [{"vnum": 50721, "count": 1}] - var inv: Array = [] - var res = system.craft_item(slots, inv, 0) - _assert(res["ok"] == true and res["success"] == true, "Crafted Peach Flower Juice") - _assert(res["reward_vnum"] == 50801, "Reward vnum 50801") - _assert(slots[0] == null, "Cube slot material consumed") - _assert(inv.size() == 1 and inv[0]["vnum"] == 50801, "Inventory received juice") - - # 放入 1 朵紫丁香 (50723) -> 批量产出 100 瓶药水 (50803) - slots = [{"vnum": 50723, "count": 1}] - res = system.craft_item(slots, inv, 0) - _assert(res["ok"] == true, "Crafted Lilac Juice batch") - _assert(res["reward_count"] == 100, "Yielded 100 count") - system.close_cube() - _assert(system.is_cube_open == false, "Closed cube") - -func test_huahn_so_dew_crafting() -> void: - print("\n--- Test 2: Huahn-So Dew Crafting & Gold Fee (40250 1:1) ---") - var system = CubeCraftingSystem.new() - system.open_cube(20022) # 幻素 - - # 材料: 桃花汁 x10 (50801) + 空瓶 x10 (70031) - var slots: Array = [ - {"vnum": 50801, "count": 10}, - {"vnum": 70031, "count": 10} - ] - var inv: Array = [] - - # 金币不足拦截 (需要 10,000 金币,玩家只有 5,000) - var res = system.craft_item(slots, inv, 5000) - _assert(res["ok"] == false, "Rejected due to insufficient gold") - _assert(res["reason"] == "NOT_ENOUGH_GOLD", "Reason is NOT_ENOUGH_GOLD") - _assert(slots[0]["count"] == 10, "Materials not consumed on gold error") - - # 金币充足,成功合成 - res = system.craft_item(slots, inv, 20000) - _assert(res["ok"] == true and res["success"] == true, "Crafted Peach Flower Dew (50821)") - _assert(res["remaining_gold"] == 10000, "Deducted 10,000 gold fee") - _assert(res["reward_vnum"] == 50821, "Received Dew item 50821") - -func test_jae_seon_kim_triton_sword() -> void: - print("\n--- Test 3: Jae-Seon Kim Triton Sword Crafting (40250 1:1) ---") - var system = CubeCraftingSystem.new() - system.open_cube(20383) # 金在善 - - # 1. 战神之剑+5 (145) + 30500 x10 + 30505 x10 (成功率 5%) - var slots: Array = [ - {"vnum": 145, "count": 1}, - {"vnum": 30500, "count": 10}, - {"vnum": 30505, "count": 10} - ] - var inv: Array = [] - - # 模拟失败 (roll = 50 > 5) - var res = system.craft_item(slots, inv, 150000, 50) - _assert(res["ok"] == true and res["success"] == false, "Low tier Triton craft failed") - _assert(res["remaining_gold"] == 50000, "Fee deducted even on failure") - _assert(inv.is_empty(), "No reward given on failure") - - # 2. 战神之剑+9 (149) + 30500 x10 + 30505 x10 (保底 100% 成功) - slots = [ - {"vnum": 149, "count": 1}, - {"vnum": 30500, "count": 10}, - {"vnum": 30505, "count": 10} - ] - res = system.craft_item(slots, inv, 100000, 10) - _assert(res["ok"] == true and res["success"] == true, "Master tier +9 Triton craft succeeded 100%") - _assert(res["reward_vnum"] == 460, "Reward is Triton Sword+0 (460)") - _assert(inv.size() == 1 and inv[0]["vnum"] == 460, "Triton Sword added to inventory") - -func test_recipe_and_npc_mismatch() -> void: - print("\n--- Test 4: Recipe & NPC Mismatch Handling ---") - var system = CubeCraftingSystem.new() - system.open_cube(20018) # 白高 - - # 放入海神之剑的材料到白高面前 -> 配方不匹配 - var slots: Array = [ - {"vnum": 149, "count": 1}, - {"vnum": 30500, "count": 10}, - {"vnum": 30505, "count": 10} - ] - var inv: Array = [] - var res = system.craft_item(slots, inv, 100000) - _assert(res["ok"] == false, "Baek-Go cannot craft Triton Sword") - _assert(res["reason"] == "NO_MATCHING_RECIPE", "Reason is NO_MATCHING_RECIPE") - - # 窗口关闭时调用 -> 拦截 - system.close_cube() - res = system.craft_item(slots, inv, 100000) - _assert(res["ok"] == false, "Rejected when cube closed") - _assert(res["reason"] == "CUBE_NOT_OPEN", "Reason is CUBE_NOT_OPEN") - -func test_history_and_serialization() -> void: - print("\n--- Test 5: Crafting History & Serialization ---") - var system = CubeCraftingSystem.new() - system.open_cube(20018) - var slots: Array = [{"vnum": 50721, "count": 1}] - var inv: Array = [] - system.craft_item(slots, inv, 0) - - var hist = system.get_history() - _assert(hist.size() == 1, "Recorded 1 crafting history") - _assert(hist[0]["recipe_id"] == "herb_peach_flower", "Recorded recipe herb_peach_flower") - - var data = system.serialize() - var restored = CubeCraftingSystem.new() - restored.deserialize(data) - _assert(restored.get_history().size() == 1, "Restored 1 history record") - system.clear_history() - _assert(system.get_history().is_empty(), "Cleared history") diff --git a/project/test_cube_crafting_parity.gd.uid b/project/test_cube_crafting_parity.gd.uid deleted file mode 100644 index 0a28ab62..00000000 --- a/project/test_cube_crafting_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c48cl6a1t5f5v diff --git a/project/test_cursed_monolith_spire_parity.gd b/project/test_cursed_monolith_spire_parity.gd deleted file mode 100644 index ac536d9e..00000000 --- a/project/test_cursed_monolith_spire_parity.gd +++ /dev/null @@ -1,157 +0,0 @@ -# test_cursed_monolith_spire_parity.gd —— 40250 封印魔塔限时防守挑战 1:1 回归测试套件 -extends SceneTree - -const CursedMonolithSpireSystem = preload("res://cursed_monolith_spire_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_cursed_monolith_spire_parity (Direction 2: Cursed Monolith Spire 1:1) ===") - _test_start_trial_and_waves() - _test_full_victory_and_chest() - _test_timeout_defeat() - _finish() - -func _test_start_trial_and_waves() -> void: - print("\n--- Test 1: Spire Emergence & Multi-Wave Progression ---") - var spire := CursedMonolithSpireSystem.new() - - var emerge_sig := {"fired": false, "time": 0.0} - spire.spire_emerged.connect(func(_p: Vector3, t: float): - emerge_sig["fired"] = true - emerge_sig["time"] = t - ) - - var wave_sig := {"fired": false, "wave": 0, "count": 0} - spire.wave_spawned.connect(func(w: int, _n: String, c: int): - wave_sig["fired"] = true - wave_sig["wave"] = w - wave_sig["count"] = c - ) - - var res = spire.start_trial(Vector3(100, 0, 100), 50) - _assert(res["ok"] == true, "Trial started") - _assert(spire.is_active == true, "Spire trial is active") - _assert(emerge_sig["fired"] == true and emerge_sig["time"] == 180.0, "spire_emerged signal emitted with 180s") - _assert(wave_sig["fired"] == true and wave_sig["wave"] == 1 and wave_sig["count"] == 6, "Wave 1 spawned 6 mobs") - _assert(spire.active_mobs.size() == 6, "6 active mobs in wave 1") - - # Starting another trial while active must fail - var fail_dup = spire.start_trial(Vector3.ZERO) - _assert(fail_dup["ok"] == false and fail_dup["reason"] == "TRIAL_ALREADY_ACTIVE", "Cannot start duplicate trial") - - # Clear wave 1 (kill all 6 mobs) - var mob_ids = spire.active_mobs.keys().duplicate() - for i in range(mob_ids.size()): - spire.kill_mob(mob_ids[i]) - - # Wave 2 should have automatically triggered (2 mobs) - _assert(spire.current_wave == 2, "Transitioned to Wave 2") - _assert(spire.active_mobs.size() == 2, "Wave 2 has 2 mobs") - -func _test_full_victory_and_chest() -> void: - print("\n--- Test 2: Full Victory & Ancient Chest Looting ---") - var spire := CursedMonolithSpireSystem.new() - spire.start_trial(Vector3.ZERO, 60) - - var victory_sig := {"fired": false, "success": false, "time": 0.0} - spire.trial_completed.connect(func(s: bool, t: float): - victory_sig["fired"] = true - victory_sig["success"] = s - victory_sig["time"] = t - ) - - var chest_sig := {"fired": false, "vnum": 0} - spire.reward_chest_spawned.connect(func(v: int, _p: Vector3): - chest_sig["fired"] = true - chest_sig["vnum"] = v - ) - - # Clear wave 1 - var w1_ids = spire.active_mobs.keys().duplicate() - for mid in w1_ids: - spire.kill_mob(mid) - - # Clear wave 2 - var w2_ids = spire.active_mobs.keys().duplicate() - for mid in w2_ids: - spire.kill_mob(mid) - - _assert(spire.current_wave == 3, "Reached Wave 3 (Ancient Guardian)") - _assert(spire.active_mobs.size() == 1, "Wave 3 has 1 boss") - - # Advance 20 seconds of game time - spire.update(20.0) - - # Kill final boss - var boss_id = spire.active_mobs.keys()[0] - var boss_kill = spire.kill_mob(boss_id) - _assert(boss_kill["ok"] == true and boss_kill["victory"] == true, "Boss defeated, victory achieved") - _assert(spire.is_active == false and spire.is_victory == true, "Trial finished in victory") - _assert(victory_sig["fired"] == true and victory_sig["success"] == true, "trial_completed emitted victory") - _assert(chest_sig["fired"] == true and chest_sig["vnum"] == 50130, "reward_chest_spawned emitted 50130") - _assert(spire.chest_available == true, "Chest available for looting") - - # Open Chest - var drops = spire.open_reward_chest() - _assert(drops.size() == 4, "Chest gave 4 rewards") - _assert(spire.chest_available == false, "Chest emptied") - - var has_scroll := false - var has_soul_stone := false - var has_marble := false - var has_gold := false - - for d in drops: - if d["vnum"] == 71003: has_scroll = true - elif d["vnum"] == 50513: has_soul_stone = true - elif d["vnum"] == 70024: has_marble = true - elif d["vnum"] == 1 and d["count"] >= 3000000: has_gold = true - - _assert(has_scroll, "Reward contains Blessing Scroll (71003)") - _assert(has_soul_stone, "Reward contains Soul Stone (50513)") - _assert(has_marble, "Reward contains Blessing Marble (70024)") - _assert(has_gold, "Reward contains 3.5M gold") - - # Second open attempt gives nothing - var empty = spire.open_reward_chest() - _assert(empty.is_empty(), "Cannot open chest twice") - -func _test_timeout_defeat() -> void: - print("\n--- Test 3: Timeout Defeat (180s expired) ---") - var spire := CursedMonolithSpireSystem.new() - spire.start_trial(Vector3.ZERO, 50) - - var defeat_sig := {"fired": false, "success": true} - spire.trial_completed.connect(func(s: bool, _t: float): - defeat_sig["fired"] = true - defeat_sig["success"] = s - ) - - # Simulate 185 seconds passing without clearing mobs - var tick = spire.update(185.0) - _assert(tick["victory"] == false, "Defeat on timeout") - _assert(spire.is_active == false and spire.is_victory == false, "Trial marked as inactive defeat") - _assert(defeat_sig["fired"] == true and defeat_sig["success"] == false, "trial_completed emitted defeat") - _assert(spire.chest_available == false, "No chest awarded on defeat") - -func _finish() -> void: - print("\n==========================================") - print("Cursed Monolith Spire Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Cursed Monolith Spire parity tests passed!") - quit(0) diff --git a/project/test_cursed_monolith_spire_parity.gd.uid b/project/test_cursed_monolith_spire_parity.gd.uid deleted file mode 100644 index c8228a6e..00000000 --- a/project/test_cursed_monolith_spire_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://db1h7dcg2utjx diff --git a/project/test_demon_tower_parity.gd b/project/test_demon_tower_parity.gd deleted file mode 100644 index 33e94d1b..00000000 --- a/project/test_demon_tower_parity.gd +++ /dev/null @@ -1,140 +0,0 @@ -# test_demon_tower_parity.gd —— 验证方向 1:恶魔之塔 1~9 层单机爬塔循环与塔顶免费铁匠 40250 1:1 对齐 -extends SceneTree - -const DemonTowerSystem = preload("res://demon_tower_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_demon_tower_parity (Direction 1: Demon Tower 1~9 Parity 1:1) ===") - test_floors_1_to_3() - test_floor_4_puzzle() - test_floor_5_seals() - test_floor_6_blacksmith_and_free_refine() - test_floors_7_to_9_reaper() - - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed > 0: - printerr("TEST FAILED!") - quit(1) - else: - print("ALL TESTS PASSED!") - quit(0) - -func test_floors_1_to_3() -> void: - print("\n--- Test 1: Floors 1 to 3 Progression ---") - var tower := DemonTowerSystem.new() - tower.start_tower() - _assert(tower.get_floor() == 1, "Tower started on Floor 1") - _assert("恶魔入口" in tower.get_floor_name(), "Floor 1 name matches") - - # Floor 1: Metin 8015 - _assert(not tower.on_floor1_metin_destroyed(9999), "Wrong metin does not advance") - _assert(tower.on_floor1_metin_destroyed(DemonTowerSystem.VNUM_METIN_FLOOR1), "Destroying Metin 8015 advances to Floor 2") - _assert(tower.get_floor() == 2, "Now on Floor 2") - - # Floor 2: Clear mobs - _assert(not tower.on_floor2_mobs_cleared(10), "Floor 2 remains while mobs remain") - _assert(tower.on_floor2_mobs_cleared(0), "Clearing mobs advances to Floor 3") - _assert(tower.get_floor() == 3, "Now on Floor 3") - - # Floor 3: Demon King 1091 - _assert(tower.on_floor3_boss_killed(DemonTowerSystem.VNUM_DEMON_KING), "Killing Demon King 1091 advances to Floor 4") - _assert(tower.get_floor() == 4, "Now on Floor 4") - -func test_floor_4_puzzle() -> void: - print("\n--- Test 2: Floor 4 7-Metin Puzzle ---") - var tower := DemonTowerSystem.new() - tower.current_floor = 4 - tower.is_active = true - tower.setup_floor4_metins() - - var real_idx := tower.floor4_real_metin_idx - _assert(real_idx >= 0 and real_idx < 7, "Real metin index is between 0 and 6") - - var fake_idx := (real_idx + 1) % 7 - var res_fake := tower.on_floor4_metin_hit(fake_idx) - _assert(res_fake.ok and not res_fake.is_real, "Hitting fake metin gives clue, does not advance") - _assert(tower.get_floor() == 4, "Still on Floor 4") - - var res_real := tower.on_floor4_metin_hit(real_idx) - _assert(res_real.ok and res_real.is_real, "Hitting real metin advances to Floor 5") - _assert(tower.get_floor() == 5, "Now on Floor 5") - -func test_floor_5_seals() -> void: - print("\n--- Test 3: Floor 5 5-Stone Pillar Seals ---") - var tower := DemonTowerSystem.new() - tower.current_floor = 5 - tower.is_active = true - - # Unlocking with wrong key fails - var res_bad := tower.on_floor5_unlock_pillar(12345) - _assert(not res_bad.ok, "Wrong key fails") - - # Unlocking pillars 1..4 - for i in 4: - var res := tower.on_floor5_unlock_pillar(DemonTowerSystem.VNUM_ZIN_KEY_FLOOR5) - _assert(res.ok and not res.completed, "Pillar %d unlocked" % (i + 1)) - - # Unlocking 5th pillar completes floor 5 - var res5 := tower.on_floor5_unlock_pillar(DemonTowerSystem.VNUM_ZIN_KEY_FLOOR5) - _assert(res5.ok and res5.completed, "5th pillar unlocked, floor completed") - _assert(tower.get_floor() == 6, "Now on Floor 6") - -func test_floor_6_blacksmith_and_free_refine() -> void: - print("\n--- Test 4: Floor 6 Elite Demon King & Free Blacksmith ---") - var tower := DemonTowerSystem.new() - tower.current_floor = 6 - tower.is_active = true - - # Kill Elite Demon King 1092 - var res_boss := tower.on_floor6_boss_killed(DemonTowerSystem.VNUM_ELITE_DEMON_KING) - _assert(res_boss.ok, "Elite Demon King killed") - _assert(tower.floor6_blacksmith_vnum in [20074, 20075, 20076], "Tower Blacksmith spawned") - - # Free Refine with Blacksmith - tower.floor6_blacksmith_vnum = DemonTowerSystem.NPC_TOWER_WEAPON_SMITH - var sword := {"vnum": 18} # Sword +8 - var res_refine := tower.perform_tower_free_refine(sword, 1) - _assert(res_refine.ok and res_refine.new_vnum == 19, "Sword+8 free refined to Sword+9 without materials") - - # Second attempt rejected (only 1 free refine permitted per run) - var res_refine2 := tower.perform_tower_free_refine(sword, 1) - _assert(not res_refine2.ok and res_refine2.code == "ALREADY_USED", "Second free refine attempt rejected") - - # Advance to floor 7 level gate (requires level 75) - var res_adv_low := tower.advance_to_floor7(70) - _assert(not res_adv_low.ok and res_adv_low.code == "LEVEL_TOO_LOW", "Level < 75 rejected from Floor 7") - - var res_adv_ok := tower.advance_to_floor7(75) - _assert(res_adv_ok.ok, "Level 75 advances to Floor 7") - _assert(tower.get_floor() == 7, "Now on Floor 7") - -func test_floors_7_to_9_reaper() -> void: - print("\n--- Test 5: Floors 7 to 9 & Death Reaper Defeat ---") - var tower := DemonTowerSystem.new() - tower.current_floor = 7 - tower.is_active = true - - # Floor 7 Map 30302 - _assert(tower.on_floor7_map_used(DemonTowerSystem.VNUM_ZIN_MAP_FLOOR7), "Using Zin Map 30302 advances to Floor 8") - _assert(tower.get_floor() == 8, "Now on Floor 8") - - # Floor 8 Key 30304 - _assert(tower.on_floor8_key_used(DemonTowerSystem.VNUM_ZIN_KEY_FLOOR8), "Using Zin Key 30304 advances to Floor 9") - _assert(tower.get_floor() == 9, "Now on Floor 9") - - # Floor 9 Final Boss: Death Reaper 1093 - var res_reaper := tower.on_floor9_boss_killed(DemonTowerSystem.VNUM_DEATH_REAPER) - _assert(res_reaper.ok and res_reaper.code == "TOWER_CLEAR", "Death Reaper defeated, tower cleared") - _assert(res_reaper.drop_vnum == DemonTowerSystem.VNUM_DEATH_REAPER_CHEST, "Rewards Death Reaper Chest (50082)") - _assert(not tower.is_active, "Tower challenge marked completed") diff --git a/project/test_demon_tower_parity.gd.uid b/project/test_demon_tower_parity.gd.uid deleted file mode 100644 index 7795707e..00000000 --- a/project/test_demon_tower_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://fdoa2j14nlxf diff --git a/project/test_devils_catacomb_parity.gd b/project/test_devils_catacomb_parity.gd deleted file mode 100644 index 33f86a62..00000000 --- a/project/test_devils_catacomb_parity.gd +++ /dev/null @@ -1,223 +0,0 @@ -# test_devils_catacomb_parity.gd —— 40250 地下墓穴与冥王阿兹瑞尔 1:1 纯净回归测试套件 -extends SceneTree - -const DevilsCatacombSystem = preload("res://devils_catacomb_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_devils_catacomb_parity (Direction 1: Devil's Catacomb 1:1) ===") - _test_entry_requirements() - _test_floor_1_to_4_progression() - _test_floor_5_tartaros_and_obelisk() - _test_floor_6_charon() - _test_floor_7_azrael_battle_and_drops() - _test_catacomb_timeout() - _finish() - -func _test_entry_requirements() -> void: - print("\n--- Test 1: Entry Requirements (Lv75 & Soul Key 30311) ---") - var dcs := DevilsCatacombSystem.new() - var inv: Array = [ - {"vnum": 30311, "name": "灵魂水晶钥匙", "count": 1} - ] - - # Level < 75 rejected - var r1 = dcs.enter_catacomb(70, inv) - _assert(r1["ok"] == false and r1["reason"] == "LEVEL_TOO_LOW", "Level < 75 rejected") - - # Missing key - var r2 = dcs.enter_catacomb(75, [null]) - _assert(r2["ok"] == false and r2["reason"] == "MISSING_SOUL_KEY", "Missing soul crystal key rejected") - - # Valid entry - var r3 = dcs.enter_catacomb(75, inv) - _assert(r3["ok"] == true, "Entry succeeds with Lv 75 and Soul Crystal Key") - _assert(dcs.is_active == true, "Catacomb is active") - _assert(dcs.current_floor == 1, "Starts on Floor 1") - _assert(dcs.remaining_time == 3600.0, "60-minute countdown initialized") - -func _test_floor_1_to_4_progression() -> void: - print("\n--- Test 2: Floor 1 to 4 Progression (Key, Maze, Real Metin) ---") - var dcs := DevilsCatacombSystem.new() - var inv: Array = [ - {"vnum": 30311, "name": "灵魂水晶钥匙", "count": 1} - ] - dcs.enter_catacomb(75, inv) - - var floor_signal := {"fired": false, "floor": 0} - dcs.floor_advanced.connect(func(f: int): - floor_signal["fired"] = true - floor_signal["floor"] = f - ) - - # 1. Floor 1: Unlock seal using 30311 key - var r1 = dcs.unlock_floor_1(inv) - _assert(r1["ok"] == true and r1["floor"] == 2, "Unlocked Floor 1 -> Advanced to Floor 2") - _assert(inv[0] == null, "Soul crystal key consumed") - _assert(floor_signal["fired"] == true and floor_signal["floor"] == 2, "floor_advanced signal fired for floor 2") - - # 2. Floor 2: Complete maze - var r2 = dcs.complete_floor_2() - _assert(r2["ok"] == true and r2["floor"] == 3, "Maze completed -> Advanced to Floor 3") - - # 3. Floor 3: Break fake vs real Metin (8035) - dcs.correct_metin_id = 4 # Force correct ID to 4 for deterministic test - - var metin_signal := {"fired": false, "correct": false} - dcs.metin_destroyed.connect(func(corr: bool): - metin_signal["fired"] = true - metin_signal["correct"] = corr - ) - - # Break fake metin 1 - var r_fake = dcs.break_floor_3_metin(1) - _assert(r_fake["ok"] == true and r_fake["correct"] == false, "Fake metin detected") - _assert(dcs.current_floor == 3, "Stays on floor 3 after fake metin") - _assert(metin_signal["fired"] == true and metin_signal["correct"] == false, "metin_destroyed signal fired false") - - # Break real metin 4 - var r_real = dcs.break_floor_3_metin(4) - _assert(r_real["ok"] == true and r_real["correct"] == true, "Real metin destroyed") - _assert(dcs.current_floor == 4, "Advanced to Floor 4") - _assert(metin_signal["correct"] == true, "metin_destroyed signal fired true") - - # 4. Floor 4: Complete pillar maze - var r4 = dcs.complete_floor_4() - _assert(r4["ok"] == true and r4["floor"] == 5, "Floor 4 completed -> Advanced to Floor 5") - -func _test_floor_5_tartaros_and_obelisk() -> void: - print("\n--- Test 3: Floor 5 Tartaros Defeat & Obelisk Activation ---") - var dcs := DevilsCatacombSystem.new() - dcs.enter_catacomb(75, [{"vnum": 30311}]) - dcs.current_floor = 5 # Jump to floor 5 - - var inv: Array = [null] - var tartaros_signal := {"fired": false} - dcs.tartaros_killed.connect(func(_drop: Dictionary): - tartaros_signal["fired"] = true - ) - - # Defeat Tartaros - var r_tar = dcs.defeat_tartaros(inv) - _assert(r_tar["ok"] == true, "Tartaros defeated") - _assert(inv[0] != null and inv[0]["vnum"] == 30312, "Grim Soul Stone (30312) dropped into inventory") - _assert(tartaros_signal["fired"] == true, "tartaros_killed signal fired") - - # Insert stone into obelisk - var obelisk_signal := {"fired": false} - dcs.obelisk_activated.connect(func(): - obelisk_signal["fired"] = true - ) - - var r_ob = dcs.insert_grim_stone_to_obelisk(inv) - _assert(r_ob["ok"] == true and r_ob["floor"] == 6, "Obelisk activated -> Advanced to Floor 6") - _assert(inv[0] == null, "Grim Soul Stone consumed by obelisk") - _assert(obelisk_signal["fired"] == true, "obelisk_activated signal fired") - -func _test_floor_6_charon() -> void: - print("\n--- Test 4: Floor 6 Charon Defeat ---") - var dcs := DevilsCatacombSystem.new() - dcs.enter_catacomb(75, [{"vnum": 30311}]) - dcs.current_floor = 6 - - var charon_signal := {"fired": false} - dcs.charon_killed.connect(func(): charon_signal["fired"] = true) - - var r = dcs.defeat_charon() - _assert(r["ok"] == true and r["floor"] == 7, "Charon defeated -> Advanced to Floor 7") - _assert(charon_signal["fired"] == true, "charon_killed signal fired") - _assert(dcs.is_azrael_alive == true, "Azrael is now active on floor 7") - _assert(dcs.azrael_hp == 900000, "Azrael initial HP is 900,000") - -func _test_floor_7_azrael_battle_and_drops() -> void: - print("\n--- Test 5: Floor 7 Azrael Battle, Minion Summon & Drops ---") - var dcs := DevilsCatacombSystem.new() - dcs.enter_catacomb(75, [{"vnum": 30311}]) - dcs.current_floor = 6 - dcs.defeat_charon() # Transitions to floor 7 - - var azrael_hp_signal := {"fired": false, "hp": 0} - dcs.azrael_hp_changed.connect(func(hp: int, _max: int): - azrael_hp_signal["fired"] = true - azrael_hp_signal["hp"] = hp - ) - - # 1. Normal damage: Raw 1680 - DEF 680 = 1000 net dmg - var r1 = dcs.attack_azrael(1680) - _assert(r1["ok"] == true and r1["net_dmg"] == 1000, "Raw 1680 - 680 DEF = 1000 net dmg") - _assert(dcs.azrael_hp == 899000, "Azrael HP reduced to 899,000") - _assert(azrael_hp_signal["fired"] == true, "azrael_hp_changed signal fired") - - # 2. Cross 50% HP threshold (450,000 HP) -> summons minions - # Current HP: 899,000. Deal 450,000 net dmg -> raw 450,680 - var r2 = dcs.attack_azrael(450680) - _assert(dcs.azrael_hp == 449000, "Azrael HP below 50% (449,000)") - _assert(r2["summoned_minions"] == true, "Azrael summoned undead minions at <= 50% HP") - - # 3. Lethal damage kills Azrael - var clear_signal := {"fired": false, "drops": []} - dcs.catacomb_cleared.connect(func(drops: Array): - clear_signal["fired"] = true - clear_signal["drops"] = drops - ) - - var r3 = dcs.attack_azrael(1000000) - _assert(r3["ok"] == true and r3["killed"] == true, "Azrael killed") - _assert(dcs.is_active == false, "Catacomb completed") - _assert(clear_signal["fired"] == true, "catacomb_cleared signal fired") - - # Verify drops - var drops: Array = r3["drops"] - var has_chest := false - var has_soul_stone := false - var has_shield := false - var has_gold := false - - for d in drops: - if d["vnum"] == 50187: # Azrael Chest - has_chest = true - elif d["vnum"] == 50513: # Soul Stone - has_soul_stone = true - elif d["vnum"] == 13149: # Titan Shield - has_shield = true - elif d["vnum"] == 1: - has_gold = true - - _assert(has_chest, "Guaranteed Azrael Chest (50187) dropped") - _assert(has_soul_stone, "Guaranteed Soul Stone (50513) dropped") - _assert(has_shield, "Titan Shield (13149) dropped") - _assert(has_gold, "Yang dropped") - -func _test_catacomb_timeout() -> void: - print("\n--- Test 6: Catacomb Timeout Failure ---") - var dcs := DevilsCatacombSystem.new() - dcs.enter_catacomb(75, [{"vnum": 30311}]) - - var fail_signal := {"fired": false, "reason": ""} - dcs.catacomb_failed.connect(func(r: String): - fail_signal["fired"] = true - fail_signal["reason"] = r - ) - - dcs.update(3601.0) - _assert(fail_signal["fired"] == true and fail_signal["reason"] == "TIME_OUT", "Timeout failure emitted") - _assert(dcs.is_active == false, "Catacomb deactivated on timeout") - -func _finish() -> void: - print("\n==========================================") - print("Devil's Catacomb Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_devils_catacomb_parity.gd.uid b/project/test_devils_catacomb_parity.gd.uid deleted file mode 100644 index 1a01d545..00000000 --- a/project/test_devils_catacomb_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b58pa84vnihp6 diff --git a/project/test_dragon_lair_parity.gd b/project/test_dragon_lair_parity.gd deleted file mode 100644 index 68bcac88..00000000 --- a/project/test_dragon_lair_parity.gd +++ /dev/null @@ -1,185 +0,0 @@ -# test_dragon_lair_parity.gd —— 40250 冰龙贝兰-塞陶巢穴 1:1 纯净回归测试套件 -extends SceneTree - -const DragonLairSystem = preload("res://dragon_lair_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_dragon_lair_parity (Direction 1: Dragon Lair Beran-Setaou 1:1) ===") - _test_entry_and_key_consumption() - _test_metin_buffs_and_debuffs() - _test_metin_hp_regen() - _test_dragon_defeat_and_drops() - _test_dungeon_timeout() - _finish() - -func _test_entry_and_key_consumption() -> void: - print("\n--- Test 1: Entry Requirements & Key Consumption (30179 x3) ---") - var dls := DragonLairSystem.new() - var inv: Array = [ - {"vnum": 30179, "name": "扭曲钥匙", "count": 2}, - {"vnum": 30179, "name": "扭曲钥匙", "count": 1}, - null - ] - - # Level 70 rejected - var r1 = dls.enter_lair(70, inv) - _assert(r1["ok"] == false and r1["reason"] == "LEVEL_TOO_LOW", "Level < 75 rejected") - - # Level 75 but lacking keys - var empty_inv: Array = [null] - var r2 = dls.enter_lair(75, empty_inv) - _assert(r2["ok"] == false and r2["reason"] == "MISSING_KEYS", "Missing keys rejected") - - # Level 75 with 3 keys across 2 stacks succeeds - var r3 = dls.enter_lair(75, inv) - _assert(r3["ok"] == true, "Entry succeeds with Lv 75 and 3 keys") - _assert(inv[0] == null and inv[1] == null, "3 keys across both slots consumed") - _assert(dls.is_active == true, "Dungeon is active") - _assert(dls.remaining_time == 3600.0, "60-minute countdown initialized") - _assert(dls.get_alive_metin_count() == 4, "4 Metin stones initialized") - _assert(dls.dragon_hp == 800000, "Dragon HP initialized to 800,000") - -func _test_metin_buffs_and_debuffs() -> void: - print("\n--- Test 2: Metin Stone Buffs & Debuff Removal (8031, 8033, 8034) ---") - var dls := DragonLairSystem.new() - dls.enter_lair(80, [{"vnum": 30179, "count": 3}]) - - # 1. 8034 (复仇之石) ATK buff (+40%): 1800 * 1.4 = 2520 - _assert(dls.get_dragon_atk() == 2520, "Dragon ATK has +40% buff (2520) while 8034 alive") - - var metin_signal := {"fired": false, "vnum": 0, "name": ""} - dls.metin_destroyed.connect(func(v: int, n: String): - metin_signal["fired"] = true - metin_signal["vnum"] = v - metin_signal["name"] = n - ) - - var minion_signal := {"fired": false, "count": 0} - dls.minions_spawned.connect(func(cnt: int, _mob: int): - minion_signal["fired"] = true - minion_signal["count"] = cnt - ) - - # Destroy 8034 - var des_res = dls.destroy_metin(8034) - _assert(des_res["ok"] == true, "8034 destroyed") - _assert(metin_signal["fired"] == true and metin_signal["vnum"] == 8034, "metin_destroyed signal fired") - _assert(minion_signal["fired"] == true and minion_signal["count"] == 4, "minions_spawned signal fired with 4 minions") - _assert(dls.get_dragon_atk() == 1800, "Dragon ATK dropped to base 1800 after 8034 destroyed") - - # 2. 8031 (傲慢之石) Physical Resistance (-30% damage taken) - # Raw dmg: 1650. DEF: 650. Net: 1000. With 30% reduction: 700. - var phys_res = dls.apply_damage(1650, false) - _assert(phys_res["net_dmg"] == 700, "Physical damage reduced by 30% (700 net) while 8031 alive") - - # Destroy 8031 - dls.destroy_metin(8031) - var phys_res2 = dls.apply_damage(1650, false) - _assert(phys_res2["net_dmg"] == 1000, "Physical damage unreduced (1000 net) after 8031 destroyed") - - # 3. 8033 (平静之石) Magic Resistance (-30% magic damage taken) - var mag_res = dls.apply_damage(1650, true) - _assert(mag_res["net_dmg"] == 700, "Magic damage reduced by 30% (700 net) while 8033 alive") - - # Destroy 8033 - dls.destroy_metin(8033) - var mag_res2 = dls.apply_damage(1650, true) - _assert(mag_res2["net_dmg"] == 1000, "Magic damage unreduced (1000 net) after 8033 destroyed") - -func _test_metin_hp_regen() -> void: - print("\n--- Test 3: 8032 Metin HP Regeneration ---") - var dls := DragonLairSystem.new() - dls.enter_lair(80, [{"vnum": 30179, "count": 3}]) - - # Deal 50,000 net damage (damage boss from 800,000 to 750,000) - dls.destroy_metin(8031) # remove physical reduction for easy calculation - dls.apply_damage(50650, false) # 50650 - 650 DEF = 50000 net dmg - _assert(dls.dragon_hp == 750000, "Dragon HP reduced to 750,000") - - # Advance 5.0 seconds -> 8032 heals 2% (16,000 HP) -> HP 766,000 - dls.update(5.0) - _assert(dls.dragon_hp == 766000, "8032 regenerated 16,000 HP (+2%) at 5s tick") - - # Destroy 8032 -> advance 5.0s -> no healing occurs - dls.destroy_metin(8032) - dls.update(5.0) - _assert(dls.dragon_hp == 766000, "No regen occurs after 8032 destroyed") - -func _test_dragon_defeat_and_drops() -> void: - print("\n--- Test 4: Dragon Defeat & Epic Drop Generation ---") - var dls := DragonLairSystem.new() - dls.enter_lair(80, [{"vnum": 30179, "count": 3}]) - - var kill_signal := {"fired": false, "drops": []} - dls.dragon_defeated.connect(func(drops: Array): - kill_signal["fired"] = true - kill_signal["drops"] = drops - ) - - # Deal lethal damage - dls.destroy_metin(8031) - var res = dls.apply_damage(1000000, false) - _assert(res["ok"] == true and res["killed"] == true, "Beran-Setaou killed") - _assert(dls.is_dragon_alive == false, "is_dragon_alive is false") - _assert(dls.is_active == false, "Dungeon completed") - _assert(kill_signal["fired"] == true, "dragon_defeated signal fired") - - var drops: Array = res["drops"] - var has_scale := false - var has_claw := false - var has_chest := false - var has_soul_stone := false - var has_gold := false - - for d in drops: - if d["vnum"] == 71123: # Dragon Scale - has_scale = true - elif d["vnum"] == 71129: # Dragon Claw - has_claw = true - elif d["vnum"] == 50186: # Beran-Setaou Chest - has_chest = true - elif d["vnum"] == 50513: # Soul Stone - has_soul_stone = true - elif d["vnum"] == 1: - has_gold = true - - _assert(has_scale, "Guaranteed Dragon Scale (71123) dropped") - _assert(has_claw, "Guaranteed Dragon Claw (71129) dropped") - _assert(has_chest, "Guaranteed Beran-Setaou Chest (50186) dropped") - _assert(has_soul_stone, "Guaranteed Soul Stone (50513) dropped") - _assert(has_gold, "Yang dropped") - -func _test_dungeon_timeout() -> void: - print("\n--- Test 5: Dungeon Timeout Failure ---") - var dls := DragonLairSystem.new() - dls.enter_lair(80, [{"vnum": 30179, "count": 3}]) - - var fail_signal := {"fired": false, "reason": ""} - dls.dungeon_failed.connect(func(reason: String): - fail_signal["fired"] = true - fail_signal["reason"] = reason - ) - - dls.update(3601.0) - _assert(fail_signal["fired"] == true and fail_signal["reason"] == "TIME_OUT", "Timeout signal emitted") - _assert(dls.is_active == false, "Dungeon closed on timeout") - -func _finish() -> void: - print("\n==========================================") - print("Dragon Lair Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_dragon_lair_parity.gd.uid b/project/test_dragon_lair_parity.gd.uid deleted file mode 100644 index f4698100..00000000 --- a/project/test_dragon_lair_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d4dwx8og8rh5m diff --git a/project/test_dragon_soul_parity.gd b/project/test_dragon_soul_parity.gd deleted file mode 100644 index 47c00313..00000000 --- a/project/test_dragon_soul_parity.gd +++ /dev/null @@ -1,122 +0,0 @@ -# test_dragon_soul_parity.gd —— 40250 龙神炼金石系统 1:1 纯净回归测试套件 -extends SceneTree - -const DragonSoulSystem = preload("res://dragon_soul_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_dragon_soul_parity (Direction 1: Dragon Soul Alchemy 1:1) ===") - _test_stone_creation_and_attributes() - _test_grade_and_clarity_refinement() - _test_deck_equipping_and_switching() - _test_active_bonuses_and_duration_decay() - _finish() - -func _test_stone_creation_and_attributes() -> void: - print("\n--- Test 1: Stone Creation & 40250 Attribute Scaling ---") - # Diamond: Rough, Matt, +0 - var diamond := DragonSoulSystem.create_dragon_stone(DragonSoulSystem.DragonStoneType.DIAMOND, 0, 0, 0) - var attrs: Dictionary = diamond["attributes"] - _assert(attrs["int_bonus"] == 5, "Diamond +0 int_bonus is 5") - _assert(attrs["magic_defense"] == 2, "Diamond +0 magic_defense is 2") - - # Ruby: Legendary (Grade 4), Excellent (Clarity 4), +6 Refine - var ruby := DragonSoulSystem.create_dragon_stone(DragonSoulSystem.DragonStoneType.RUBY, 4, 4, 6) - attrs = ruby["attributes"] - # str_bonus: 5 + (4*3) + (4*2) + 6 = 31 - _assert(attrs["str_bonus"] == 31, "Legendary Ruby STR bonus is 31") - # att_grade: 20 + (4*15) + (4*10) + (6*5) = 20 + 60 + 40 + 30 = 150 - _assert(attrs["att_grade"] == 150, "Legendary Ruby Attack Power bonus is 150") - - # Jade: Max HP - var jade := DragonSoulSystem.create_dragon_stone(DragonSoulSystem.DragonStoneType.JADE, 4, 0, 0) - _assert(jade["attributes"]["max_hp"] == 1300, "Legendary Jade Max HP is 1300") - -func _test_grade_and_clarity_refinement() -> void: - print("\n--- Test 2: Grade & Clarity Refinement ---") - var r1 := DragonSoulSystem.create_dragon_stone(DragonSoulSystem.DragonStoneType.RUBY, DragonSoulSystem.Grade.ROUGH, 0) - var r2 := DragonSoulSystem.create_dragon_stone(DragonSoulSystem.DragonStoneType.RUBY, DragonSoulSystem.Grade.ROUGH, 0) - var d1 := DragonSoulSystem.create_dragon_stone(DragonSoulSystem.DragonStoneType.DIAMOND, DragonSoulSystem.Grade.ROUGH, 0) - - # Mismatched type rejected - var res = DragonSoulSystem.refine_grade(r1, d1) - _assert(res["ok"] == false and res["reason"] == "MISMATCHED_STONES", "Cannot fuse Ruby with Diamond") - - # Grade refine success - res = DragonSoulSystem.refine_grade(r1, r2, true) - _assert(res["ok"] == true and res["success"] == true, "Ruby successfully upgraded to Cut grade") - _assert(res["stone"]["grade"] == DragonSoulSystem.Grade.CUT, "New stone grade is Cut (1)") - - # Clarity refine success - var c1 := DragonSoulSystem.create_dragon_stone(DragonSoulSystem.DragonStoneType.RUBY, DragonSoulSystem.Grade.CUT, DragonSoulSystem.Clarity.MATT) - var c2 := DragonSoulSystem.create_dragon_stone(DragonSoulSystem.DragonStoneType.RUBY, DragonSoulSystem.Grade.CUT, DragonSoulSystem.Clarity.MATT) - res = DragonSoulSystem.refine_clarity(c1, c2, true) - _assert(res["ok"] == true and res["success"] == true, "Ruby clarity successfully upgraded to Clear") - _assert(res["stone"]["clarity"] == DragonSoulSystem.Clarity.CLEAR, "New stone clarity is Clear (1)") - -func _test_deck_equipping_and_switching() -> void: - print("\n--- Test 3: Sky Deck & Earth Deck Equipping ---") - var dss := DragonSoulSystem.new() - var diamond := DragonSoulSystem.create_dragon_stone(DragonSoulSystem.DragonStoneType.DIAMOND) - var ruby := DragonSoulSystem.create_dragon_stone(DragonSoulSystem.DragonStoneType.RUBY) - - # Equip in Sky Deck (0) - var res = dss.equip_stone(DragonSoulSystem.DECK_SKY, diamond) - _assert(res["ok"] == true and res["slot"] == 0, "Diamond equipped into Sky Deck slot 0") - _assert(dss.decks[DragonSoulSystem.DECK_SKY][0] != null, "Sky Deck slot 0 is filled") - - res = dss.equip_stone(DragonSoulSystem.DECK_SKY, ruby) - _assert(res["ok"] == true and res["slot"] == 1, "Ruby equipped into Sky Deck slot 1") - - # Unequip from Sky Deck - res = dss.unequip_stone(DragonSoulSystem.DECK_SKY, 0) - _assert(res["ok"] == true, "Diamond unequipped") - _assert(dss.decks[DragonSoulSystem.DECK_SKY][0] == null, "Sky Deck slot 0 is now empty") - -func _test_active_bonuses_and_duration_decay() -> void: - print("\n--- Test 4: Active Deck Stat Injection & Duration Decay ---") - var dss := DragonSoulSystem.new() - var ruby := DragonSoulSystem.create_dragon_stone(DragonSoulSystem.DragonStoneType.RUBY, 4, 4, 6, 10.0) # 10s duration - dss.equip_stone(DragonSoulSystem.DECK_SKY, ruby) - - # Initially inactive: 0 bonuses - var bonuses = dss.get_active_bonuses() - _assert(bonuses.is_empty(), "No bonuses when deck is inactive") - - # Toggle Sky Deck on - var active = dss.toggle_deck(DragonSoulSystem.DECK_SKY) - _assert(active == true and dss.active_deck == DragonSoulSystem.DECK_SKY, "Sky Deck activated") - - bonuses = dss.get_active_bonuses() - _assert(bonuses["str_bonus"] == 31, "Active deck injects +31 STR bonus") - _assert(bonuses["att_grade"] == 150, "Active deck injects +150 Attack Power") - - # Update 5s -> still active - var expired = dss.update(5.0) - _assert(expired.is_empty(), "No stones expired after 5s") - _assert(dss.decks[DragonSoulSystem.DECK_SKY][1]["duration"] == 5.0, "5s duration remaining") - - # Update 6s -> expires - expired = dss.update(6.0) - _assert(expired.size() == 1, "1 stone expired after duration reached 0") - bonuses = dss.get_active_bonuses() - _assert(bonuses.is_empty(), "Expired stones no longer provide stat bonuses") - -func _finish() -> void: - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - quit(0) - else: - printerr("SOME TESTS FAILED!\n") - quit(1) diff --git a/project/test_dragon_soul_parity.gd.uid b/project/test_dragon_soul_parity.gd.uid deleted file mode 100644 index 03a24089..00000000 --- a/project/test_dragon_soul_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cirpqlmeph2lr diff --git a/project/test_dungeon_ranking_parity.gd b/project/test_dungeon_ranking_parity.gd deleted file mode 100644 index 9bc75720..00000000 --- a/project/test_dungeon_ranking_parity.gd +++ /dev/null @@ -1,108 +0,0 @@ -# test_dungeon_ranking_parity.gd —— 40250 地牢信息与最速榜单 1:1 纯净回归测试套件 -extends SceneTree - -const DungeonRankingSystem = preload("res://dungeon_ranking_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_dungeon_ranking_parity (Direction 1: Dungeon Ranking & Info 1:1) ===") - _test_dungeon_registry_configurations() - _test_clear_time_and_records() - _test_cooldown_tracking() - _test_summaries_list() - _finish() - -func _test_dungeon_registry_configurations() -> void: - print("\n--- Test 1: Dungeon Registry Configurations ---") - var drs := DungeonRankingSystem.new() - _assert(DungeonRankingSystem.DUNGEON_REGISTRY.has("demon_tower"), "Demon Tower defined") - _assert(DungeonRankingSystem.DUNGEON_REGISTRY.has("spider_dungeon_3"), "Spider Dungeon 3 defined") - _assert(DungeonRankingSystem.DUNGEON_REGISTRY.has("dragon_lair"), "Dragon Lair defined") - _assert(DungeonRankingSystem.DUNGEON_REGISTRY.has("devils_catacomb"), "Devil's Catacomb defined") - - var dl = DungeonRankingSystem.DUNGEON_REGISTRY["dragon_lair"] - _assert(dl["min_level"] == 75, "Dragon Lair requires Lv 75") - _assert(dl["ticket_vnum"] == 30179 and dl["ticket_count"] == 3, "Requires 3x Twisted Key (30179)") - - var sd3 = DungeonRankingSystem.DUNGEON_REGISTRY["spider_dungeon_3"] - _assert(sd3["min_level"] == 50, "Spider Dungeon 3 requires Lv 50") - _assert(sd3["ticket_vnum"] == 30327 and sd3["ticket_count"] == 1, "Requires Arachnid Key (30327)") - -func _test_clear_time_and_records() -> void: - print("\n--- Test 2: Clear Time Tracking & Personal Best Record ---") - var drs := DungeonRankingSystem.new() - - var record_signal := {"fired": false, "time": 0.0} - drs.new_fastest_record.connect(func(_d: String, t: float, _old: float): - record_signal["fired"] = true - record_signal["time"] = t - ) - - # 1. First clear of Dragon Lair (600.0s, peak dmg 8500) - var r1 = drs.record_clear("dragon_lair", 600.0, 8500, 1000.0) - _assert(r1["ok"] == true, "Clear recorded") - _assert(r1["is_new_record"] == true, "First clear is a new personal best") - _assert(r1["fastest_time"] == 600.0, "Fastest time is 600.0s") - _assert(r1["total_clears"] == 1, "Total clears is 1") - _assert(r1["highest_peak_damage"] == 8500, "Highest peak damage is 8500") - _assert(record_signal["fired"] == true and record_signal["time"] == 600.0, "new_fastest_record signal fired") - - # 2. Slower clear (700.0s) -> doesn't replace 600.0s - record_signal["fired"] = false - var r2 = drs.record_clear("dragon_lair", 700.0, 9000, 2000.0) - _assert(r2["is_new_record"] == false, "Slower clear is not a new record") - _assert(r2["fastest_time"] == 600.0, "Fastest time remains 600.0s") - _assert(r2["total_clears"] == 2, "Total clears incremented to 2") - _assert(r2["highest_peak_damage"] == 9000, "Peak damage updated to 9000") - _assert(record_signal["fired"] == false, "new_fastest_record signal did NOT fire") - - # 3. Faster clear (450.0s) -> replaces record! - var r3 = drs.record_clear("dragon_lair", 450.0, 9200, 3000.0) - _assert(r3["is_new_record"] == true, "Faster clear sets new record") - _assert(r3["fastest_time"] == 450.0, "Fastest time updated to 450.0s") - _assert(r3["total_clears"] == 3, "Total clears is 3") - _assert(record_signal["fired"] == true and record_signal["time"] == 450.0, "new_fastest_record signal fired for 450.0s") - -func _test_cooldown_tracking() -> void: - print("\n--- Test 3: Dungeon Cooldown Tracking ---") - var drs := DungeonRankingSystem.new() - - # Spider dungeon cooldown is 1800s - drs.record_clear("spider_dungeon_3", 300.0, 5000, 1000.0) # cleared at time 1000.0 - - # 1. Query at timestamp 1500.0 (elapsed 500s < 1800s) -> on cooldown - var info_cd = drs.get_dungeon_info("spider_dungeon_3", 1500.0) - _assert(info_cd["is_on_cooldown"] == true, "Dungeon is on cooldown") - _assert(info_cd["remaining_cooldown"] == 1300.0, "1300.0s remaining cooldown") - - # 2. Query at timestamp 3000.0 (elapsed 2000s > 1800s) -> ready - var info_ready = drs.get_dungeon_info("spider_dungeon_3", 3000.0) - _assert(info_ready["is_on_cooldown"] == false, "Dungeon is ready") - _assert(info_ready["remaining_cooldown"] == 0.0, "0.0s remaining cooldown") - -func _test_summaries_list() -> void: - print("\n--- Test 4: All Dungeon Summaries List ---") - var drs := DungeonRankingSystem.new() - var list = drs.get_all_dungeon_summaries() - _assert(list.size() == 4, "Summaries list contains all 4 dungeons") - _assert(list[0]["dungeon_id"] == "demon_tower", "Contains Demon Tower") - _assert(list[1]["dungeon_id"] == "spider_dungeon_3", "Contains Spider Dungeon 3") - -func _finish() -> void: - print("\n==========================================") - print("Dungeon Ranking Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_dungeon_ranking_parity.gd.uid b/project/test_dungeon_ranking_parity.gd.uid deleted file mode 100644 index 2ceca8d7..00000000 --- a/project/test_dungeon_ranking_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b1vnsae05dm38 diff --git a/project/test_dynamic_bounty_event_parity.gd b/project/test_dynamic_bounty_event_parity.gd deleted file mode 100644 index eb218c66..00000000 --- a/project/test_dynamic_bounty_event_parity.gd +++ /dev/null @@ -1,136 +0,0 @@ -# test_dynamic_bounty_event_parity.gd —— 40250 动态随机事件与赏金通缉令 1:1 回归测试套件 -extends SceneTree - -const DynamicBountyEventSystem = preload("res://dynamic_bounty_event_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_dynamic_bounty_event_parity (Direction 4: Bounty Events 1:1) ===") - _test_bounty_generation() - _test_hunt_and_metin_progress() - _test_boss_bounty_completion() - _test_claim_rewards_and_validation() - _finish() - -func _test_bounty_generation() -> void: - print("\n--- Test 1: Bounty Generation & Roster ---") - var sys := DynamicBountyEventSystem.new() - - var ref_sig := {"fired": false, "count": 0} - sys.bounties_refreshed.connect(func(list: Array): - ref_sig["fired"] = true - ref_sig["count"] = list.size() - ) - - var bounties = sys.refresh_daily_bounties() - _assert(bounties.size() == 3, "Generated 3 daily bounties") - _assert(ref_sig["fired"] == true and ref_sig["count"] == 3, "bounties_refreshed signal emitted") - _assert(sys.active_bounties.has("bounty_hunt_barbarian"), "Has Barbarian Hunt bounty") - _assert(sys.active_bounties.has("bounty_metin_jealousy"), "Has Metin Shatter bounty") - _assert(sys.active_bounties.has("bounty_boss_ninetails"), "Has Nine Tails Boss bounty") - -func _test_hunt_and_metin_progress() -> void: - print("\n--- Test 2: Hunt & Metin Progress Tracking ---") - var sys := DynamicBountyEventSystem.new() - sys.refresh_daily_bounties() - - var prog_sig := {"fired": false, "cur": 0, "tgt": 0} - sys.bounty_progress_updated.connect(func(_id: String, cur: int, tgt: int): - prog_sig["fired"] = true - prog_sig["cur"] = cur - prog_sig["tgt"] = tgt - ) - - var comp_sig := {"fired": false, "title": ""} - sys.bounty_completed.connect(func(_id: String, title: String): - comp_sig["fired"] = true - comp_sig["title"] = title - ) - - # 1. Kill 10 barbarians (vnum 501) - sys.on_mob_killed(501, 10) - _assert(prog_sig["fired"] == true and prog_sig["cur"] == 10 and prog_sig["tgt"] == 25, "Progress updated to 10/25") - _assert(sys.active_bounties["bounty_hunt_barbarian"]["is_completed"] == false, "Not completed yet") - - # Kill 15 more barbarians -> completes 25/25 - sys.on_mob_killed(501, 15) - _assert(sys.active_bounties["bounty_hunt_barbarian"]["is_completed"] == true, "Barbarian hunt completed") - _assert(comp_sig["fired"] == true, "bounty_completed signal emitted") - - # 2. Shatter 2 Metin stones (vnum 8008) - comp_sig["fired"] = false - sys.on_metin_destroyed(8008) - _assert(sys.active_bounties["bounty_metin_jealousy"]["current_count"] == 1, "Metin count is 1/2") - - sys.on_metin_destroyed(8008) - _assert(sys.active_bounties["bounty_metin_jealousy"]["is_completed"] == true, "Metin bounty completed") - _assert(comp_sig["fired"] == true, "bounty_completed emitted for Metin") - -func _test_boss_bounty_completion() -> void: - print("\n--- Test 3: World Boss Bounty Completion ---") - var sys := DynamicBountyEventSystem.new() - sys.refresh_daily_bounties() - - # Slay Nine Tails (1901) - sys.on_mob_killed(1901, 1) - _assert(sys.active_bounties["bounty_boss_ninetails"]["is_completed"] == true, "Nine Tails bounty completed") - _assert(sys.total_completed_count == 1, "total_completed_count is 1") - -func _test_claim_rewards_and_validation() -> void: - print("\n--- Test 4: Claiming Bounty Rewards & Double-Claim Guard ---") - var sys := DynamicBountyEventSystem.new() - sys.refresh_daily_bounties() - - var player := {"exp": 1000, "gold": 5000} - var inv: Array = [] - inv.resize(5) - for i in range(5): inv[i] = null - - # Try claim incomplete bounty - var fail_inc = sys.claim_bounty_reward("bounty_boss_ninetails", player, inv) - _assert(fail_inc["ok"] == false and fail_inc["reason"] == "NOT_COMPLETED", "Cannot claim incomplete bounty") - - # Slay Nine Tails - sys.on_mob_killed(1901, 1) - - var claim_sig := {"fired": false, "exp": 0, "gold": 0} - sys.bounty_reward_claimed.connect(func(_id: String, e: int, g: int): - claim_sig["fired"] = true - claim_sig["exp"] = e - claim_sig["gold"] = g - ) - - # Claim Boss Reward (1,000,000 XP, 3,000,000 Gold, Chest + Scroll) - var claim_res = sys.claim_bounty_reward("bounty_boss_ninetails", player, inv) - _assert(claim_res["ok"] == true, "Reward claimed") - _assert(player["exp"] == 1001000, "XP awarded (+1,000,000)") - _assert(player["gold"] == 3005000, "Gold awarded (+3,000,000)") - _assert(inv[0]["vnum"] == DynamicBountyEventSystem.BOUNTY_CHEST_VNUM, "Received Bounty Chest in slot 0") - _assert(inv[1]["vnum"] == 71003, "Received Blessing Scroll in slot 1") - _assert(claim_sig["fired"] == true and claim_sig["exp"] == 1000000, "bounty_reward_claimed signal fired") - - # Second claim attempt must fail - var fail_double = sys.claim_bounty_reward("bounty_boss_ninetails", player, inv) - _assert(fail_double["ok"] == false and fail_double["reason"] == "ALREADY_CLAIMED", "Double claim rejected") - -func _finish() -> void: - print("\n==========================================") - print("Dynamic Bounty Event Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Dynamic Bounty Event parity tests passed!") - quit(0) diff --git a/project/test_dynamic_bounty_event_parity.gd.uid b/project/test_dynamic_bounty_event_parity.gd.uid deleted file mode 100644 index 0243a82a..00000000 --- a/project/test_dynamic_bounty_event_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://5epekcbiqs2l diff --git a/project/test_emotion_action_parity.gd b/project/test_emotion_action_parity.gd deleted file mode 100644 index a6e3ee85..00000000 --- a/project/test_emotion_action_parity.gd +++ /dev/null @@ -1,75 +0,0 @@ -# test_emotion_action_parity.gd —— 验证方向 3:角色情绪动作与表情快捷键 40250 1:1 对齐 -extends SceneTree - -const EmotionActionSystem = preload("res://emotion_action_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_emotion_action_parity (Direction 3: Emotions & Hotkeys 1:1) ===") - test_emotion_definitions() - test_command_resolution() - test_ctrl_hotkeys() - test_action_prerequisites() - - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed > 0: - printerr("TEST FAILED!") - quit(1) - else: - print("ALL TESTS PASSED!") - quit(0) - -func test_emotion_definitions() -> void: - print("\n--- Test 1: 18 Official Emotions Definitions (emotion.py) ---") - _assert(EmotionActionSystem.is_valid_emotion(EmotionActionSystem.EMOTION_CLAP), "Clap (1) is valid") - _assert(EmotionActionSystem.is_valid_emotion(EmotionActionSystem.EMOTION_ANGRY), "Angry (4) is valid") - _assert(EmotionActionSystem.is_valid_emotion(EmotionActionSystem.EMOTION_DANCE_1), "Dance 1 (13) is valid") - _assert(EmotionActionSystem.is_valid_emotion(EmotionActionSystem.EMOTION_DANCE_6), "Dance 6 (18) is valid") - _assert(not EmotionActionSystem.is_valid_emotion(999), "Invalid emotion 999 rejected") - -func test_command_resolution() -> void: - print("\n--- Test 2: Chat Command Resolution (/clap, /dance1..) ---") - _assert(EmotionActionSystem.get_emotion_from_command("/clap") == EmotionActionSystem.EMOTION_CLAP, - "/clap resolves to EMOTION_CLAP (1)") - _assert(EmotionActionSystem.get_emotion_from_command("/dance1") == EmotionActionSystem.EMOTION_DANCE_1, - "/dance1 resolves to EMOTION_DANCE_1 (13)") - _assert(EmotionActionSystem.get_emotion_from_command("/angry") == EmotionActionSystem.EMOTION_ANGRY, - "/angry resolves to EMOTION_ANGRY (4)") - _assert(EmotionActionSystem.get_emotion_from_command("/unknown") == 0, - "/unknown resolves to 0") - -func test_ctrl_hotkeys() -> void: - print("\n--- Test 3: Ctrl + 1..9 Hotkey Mapping ---") - # Ctrl+1 (idx 0) -> Clap - _assert(EmotionActionSystem.get_hotkey_emotion(0) == EmotionActionSystem.EMOTION_CLAP, "Ctrl+1 triggers Clap") - # Ctrl+2 (idx 1) -> Congratulation - _assert(EmotionActionSystem.get_hotkey_emotion(1) == EmotionActionSystem.EMOTION_CONGRATULATION, "Ctrl+2 triggers Congratulation") - # Ctrl+4 (idx 3) -> Angry - _assert(EmotionActionSystem.get_hotkey_emotion(3) == EmotionActionSystem.EMOTION_ANGRY, "Ctrl+4 triggers Angry") - # Ctrl+9 (idx 8) -> Banter - _assert(EmotionActionSystem.get_hotkey_emotion(8) == EmotionActionSystem.EMOTION_BANTER, "Ctrl+9 triggers Banter") - -func test_action_prerequisites() -> void: - print("\n--- Test 4: Action Execution Prerequisites (Combat & Mounted Restrictions) ---") - # Successful execution - var res_ok := EmotionActionSystem.perform_emotion(EmotionActionSystem.EMOTION_CLAP, false, false) - _assert(res_ok.ok and res_ok.code == "SUCCESS", "Emotion performed successfully") - _assert(res_ok.motion == "clap", "Motion is clap") - - # Rejected while in combat - var res_combat := EmotionActionSystem.perform_emotion(EmotionActionSystem.EMOTION_CLAP, true, false) - _assert(not res_combat.ok and res_combat.code == "CANNOT_IN_BATTLE", "Emotion rejected during combat") - - # Rejected while mounted - var res_mount := EmotionActionSystem.perform_emotion(EmotionActionSystem.EMOTION_CLAP, false, true) - _assert(not res_mount.ok and res_mount.code == "CANNOT_ON_MOUNT", "Emotion rejected while mounted") diff --git a/project/test_emotion_action_parity.gd.uid b/project/test_emotion_action_parity.gd.uid deleted file mode 100644 index 11bf942f..00000000 --- a/project/test_emotion_action_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dv0ciwk1wsuhb diff --git a/project/test_empire_kingdom_parity.gd b/project/test_empire_kingdom_parity.gd deleted file mode 100644 index ba2c379b..00000000 --- a/project/test_empire_kingdom_parity.gd +++ /dev/null @@ -1,146 +0,0 @@ -# test_empire_kingdom_parity.gd —— 40250 三圣帝国归属与领地国运系统 1:1 回归测试套件 -extends SceneTree - -const EmpireKingdomSystem = preload("res://empire_kingdom_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_empire_kingdom_parity (Direction 4: Empire Kingdom & Territory 1:1) ===") - _test_empire_selection_and_blessings() - _test_territory_hostility_checks() - _test_tincture_of_empires_transfer() - _test_serialization() - _finish() - -func _test_empire_selection_and_blessings() -> void: - print("\n--- Test 1: Empire Selection & Daily Blessing Prayer ---") - var sys := EmpireKingdomSystem.new() - _assert(sys.current_empire == EmpireKingdomSystem.EMPIRE_NONE, "Initially unaligned") - - var sel_sig := {"fired": false, "id": 0, "name": ""} - sys.empire_selected.connect(func(i: int, n: String): - sel_sig["fired"] = true - sel_sig["id"] = i - sel_sig["name"] = n - ) - - # Select Shinsoo (Red Empire 1) - var sel_res = sys.select_empire(EmpireKingdomSystem.EMPIRE_SHINSOO) - _assert(sel_res["ok"] == true, "Selected Shinsoo") - _assert(sys.current_empire == EmpireKingdomSystem.EMPIRE_SHINSOO, "Current empire is Shinsoo") - _assert(sel_sig["fired"] == true, "empire_selected signal emitted") - _assert(sel_sig["name"] == "慎修帝国", "Name is 慎修帝国") - - # Activate Daily Prayer - var player_stats := {"gold_bonus_pct": 0, "shop_discount_pct": 0} - var pray_res = sys.activate_empire_prayer(player_stats) - _assert(pray_res["ok"] == true, "Prayer activated") - _assert(sys.is_blessing_active == true, "Blessing flag is active") - _assert(player_stats["gold_bonus_pct"] == 10, "Shinsoo gold bonus +10% applied") - _assert(player_stats["shop_discount_pct"] == 5, "Shinsoo shop discount +5% applied") - - # Advance time 86400s -> expiry - sys.update(86400.0, player_stats) - _assert(sys.is_blessing_active == false, "Blessing expired after 24h") - _assert(player_stats["gold_bonus_pct"] == 0, "Gold bonus cleanly deducted") - _assert(player_stats["shop_discount_pct"] == 0, "Shop discount cleanly deducted") - -func _test_territory_hostility_checks() -> void: - print("\n--- Test 2: Territory Hostility & Intruder Checks ---") - var sys := EmpireKingdomSystem.new() - sys.select_empire(EmpireKingdomSystem.EMPIRE_SHINSOO) - - # 1. Home territory (Yongan / Jayang) - var home_check = sys.check_territory("Yongan") - _assert(home_check["is_home"] == true, "Yongan is Shinsoo homeland") - _assert(home_check["is_hostile"] == false, "Homeland is not hostile") - _assert(home_check["relation"] == "homeland", "Relation is homeland") - - # 2. Hostile enemy territory (Pyungmoo - Jinno homeland) - var enemy_check = sys.check_territory("Pyungmoo") - _assert(enemy_check["is_home"] == false, "Pyungmoo is not homeland") - _assert(enemy_check["is_hostile"] == true, "Pyungmoo is hostile foreign territory") - _assert(enemy_check["relation"] == "hostile_invasion", "Relation is hostile invasion") - - # 3. Neutral territory (Desert) - var neutral_check = sys.check_territory("Desert") - _assert(neutral_check["is_home"] == false, "Desert is not homeland") - _assert(neutral_check["is_hostile"] == false, "Desert is not hostile") - _assert(neutral_check["relation"] == "neutral", "Relation is neutral territory") - -func _test_tincture_of_empires_transfer() -> void: - print("\n--- Test 3: Tincture of Empires (71054) Nation Transfer ---") - var sys := EmpireKingdomSystem.new() - sys.select_empire(EmpireKingdomSystem.EMPIRE_SHINSOO) - var player_stats := {"gold_bonus_pct": 0, "shop_discount_pct": 0, "attack_bonus": 0, "monster_damage_pct": 0} - sys.activate_empire_prayer(player_stats) - - var inv: Array = [ - {"vnum": EmpireKingdomSystem.VNUM_TINCTURE_OF_EMPIRES, "name": "转国卷轴", "count": 1} - ] - - # 1. Transfer while in a guild -> rejected (40250 rule) - var res_guild = sys.change_empire(EmpireKingdomSystem.EMPIRE_JINNO, 0, inv, player_stats, true) - _assert(res_guild["ok"] == false, "Rejected: player is in a guild") - _assert(res_guild["reason"] == "MUST_LEAVE_GUILD", "Reason is MUST_LEAVE_GUILD") - - # 2. Transfer to same empire -> rejected - var res_same = sys.change_empire(EmpireKingdomSystem.EMPIRE_SHINSOO, 0, inv, player_stats, false) - _assert(res_same["ok"] == false, "Rejected: cannot transfer to same empire") - _assert(res_same["reason"] == "CANNOT_CHANGE_TO_SAME_EMPIRE", "Reason is CANNOT_CHANGE_TO_SAME_EMPIRE") - - # 3. Successful transfer to Jinno (Blue Empire 3) - var change_sig := {"fired": false, "old": 0, "new": 0} - sys.empire_changed.connect(func(o: int, n: int): - change_sig["fired"] = true - change_sig["old"] = o - change_sig["new"] = n - ) - - var res_succ = sys.change_empire(EmpireKingdomSystem.EMPIRE_JINNO, 0, inv, player_stats, false) - _assert(res_succ["ok"] == true, "Transferred to Jinno successfully") - _assert(sys.current_empire == EmpireKingdomSystem.EMPIRE_JINNO, "Current empire updated to Jinno") - _assert(inv[0] == null, "Tincture scroll consumed") - _assert(change_sig["fired"] == true, "empire_changed signal emitted") - _assert(change_sig["old"] == 1 and change_sig["new"] == 3, "Signal reported 1 -> 3") - _assert(player_stats["gold_bonus_pct"] == 0, "Previous Shinsoo blessing removed upon transfer") - - # Activate Jinno Prayer - sys.activate_empire_prayer(player_stats) - _assert(player_stats["attack_bonus"] == 50, "Jinno attack bonus +50 applied") - _assert(player_stats["monster_damage_pct"] == 5, "Jinno monster damage +5% applied") - -func _test_serialization() -> void: - print("\n--- Test 4: Serialization & Restoration ---") - var sys1 := EmpireKingdomSystem.new() - sys1.select_empire(EmpireKingdomSystem.EMPIRE_CHUNJO) # Yellow - var dummy_stats := {} - sys1.activate_empire_prayer(dummy_stats) - - var data = sys1.serialize() - _assert(data["current_empire"] == EmpireKingdomSystem.EMPIRE_CHUNJO, "Serialized empire is Chunjo") - _assert(data["is_blessing_active"] == true, "Serialized blessing is active") - - var sys2 := EmpireKingdomSystem.new() - sys2.deserialize(data) - _assert(sys2.current_empire == EmpireKingdomSystem.EMPIRE_CHUNJO, "Restored empire is Chunjo") - _assert(sys2.is_blessing_active == true, "Restored blessing active state") - -func _finish() -> void: - print("\n=======================================================") - print("Empire Kingdom Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_empire_kingdom_parity.gd.uid b/project/test_empire_kingdom_parity.gd.uid deleted file mode 100644 index c46cb621..00000000 --- a/project/test_empire_kingdom_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dmi8kj2uqfsa7 diff --git a/project/test_endless_spire_dungeon_parity.gd b/project/test_endless_spire_dungeon_parity.gd deleted file mode 100644 index a63457a2..00000000 --- a/project/test_endless_spire_dungeon_parity.gd +++ /dev/null @@ -1,148 +0,0 @@ -# test_endless_spire_dungeon_parity.gd —— 40250 无尽天关爬塔挑战 1:1 回归测试套件 -extends SceneTree - -const EndlessSpireDungeonSystem = preload("res://endless_spire_dungeon_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_endless_spire_dungeon_parity (Direction 1: Endless Spire Dungeon 1:1) ===") - _test_start_and_normal_floors() - _test_milestone_boss_and_checkpoint() - _test_resume_from_checkpoint() - _test_full_clear_floor_50() - _test_timeout_defeat() - _finish() - -func _test_start_and_normal_floors() -> void: - print("\n--- Test 1: Starting Floor 1 & Clearing Mobs ---") - var spire := EndlessSpireDungeonSystem.new() - - var start_sig := {"fired": false, "floor": 0, "is_boss": true} - spire.floor_started.connect(func(f: int, is_b: bool, _c: int): - start_sig["fired"] = true - start_sig["floor"] = f - start_sig["is_boss"] = is_b - ) - - var res = spire.start_trial(false) - _assert(res["ok"] == true and spire.current_floor == 1, "Trial started at Floor 1") - _assert(start_sig["fired"] == true and start_sig["floor"] == 1 and start_sig["is_boss"] == false, "floor_started signal emitted for regular floor") - _assert(spire.active_mobs.size() == 6, "Floor 1 has 6 mobs (4 + (1%5)*2 = 6)") - - # Kill all mobs on floor 1 - var m_ids = spire.active_mobs.keys().duplicate() - for mid in m_ids: - spire.kill_mob(mid) - - # Transitioned to floor 2 - _assert(spire.current_floor == 2, "Transitioned to Floor 2") - _assert(spire.total_points == 10, "Points earned 10 (1 * 10)") - -func _test_milestone_boss_and_checkpoint() -> void: - print("\n--- Test 2: Floor 5 Milestone Boss & Checkpoint Save ---") - var spire := EndlessSpireDungeonSystem.new() - spire.start_trial(false) - - var cp_sig := {"fired": false, "cp": 0} - spire.checkpoint_saved.connect(func(cp: int): - cp_sig["fired"] = true - cp_sig["cp"] = cp - ) - - # Advance to floor 5 - while spire.current_floor < 5: - var ids = spire.active_mobs.keys().duplicate() - for mid in ids: - spire.kill_mob(mid) - - _assert(spire.current_floor == 5, "Reached Floor 5 (Milestone Boss)") - _assert(spire.active_mobs.size() == 1, "Floor 5 has 1 boss") - var boss_id = spire.active_mobs.keys()[0] - _assert(spire.active_mobs[boss_id]["vnum"] == 691, "Boss is Orc Chief (691)") - - # Kill Floor 5 Boss - spire.kill_mob(boss_id) - _assert(spire.current_floor == 6, "Advanced to Floor 6") - _assert(cp_sig["fired"] == true and cp_sig["cp"] == 6, "checkpoint_saved signal fired with Floor 6") - _assert(spire.highest_checkpoint == 6, "highest_checkpoint is 6") - -func _test_resume_from_checkpoint() -> void: - print("\n--- Test 3: Exit & Resume from Checkpoint ---") - var spire := EndlessSpireDungeonSystem.new() - spire.start_trial(false) - - # Progress to checkpoint 6 - while spire.current_floor < 6: - var ids = spire.active_mobs.keys().duplicate() - for mid in ids: - spire.kill_mob(mid) - - # Exit trial - spire.exit_trial() - _assert(spire.is_in_trial == false, "Exited trial") - - # Resume from checkpoint - var resume_res = spire.start_trial(true) - _assert(resume_res["ok"] == true, "Resumed trial") - _assert(spire.current_floor == 6, "Resumed directly at Floor 6") - -func _test_full_clear_floor_50() -> void: - print("\n--- Test 4: Floor 50 Final Boss & Spire Mastery ---") - var spire := EndlessSpireDungeonSystem.new() - spire.highest_checkpoint = 50 - spire.start_trial(true) - - var mastery_sig := {"fired": false, "floor": 0} - spire.spire_trial_completed.connect(func(f: int, _p: int): - mastery_sig["fired"] = true - mastery_sig["floor"] = f - ) - - _assert(spire.current_floor == 50, "At final Floor 50") - var boss_id = spire.active_mobs.keys()[0] - _assert(spire.active_mobs[boss_id]["vnum"] == 8027, "Final boss is Chaos Overlord (8027)") - - # Kill final boss - var finish_res = spire.kill_mob(boss_id) - _assert(finish_res["ok"] == true and finish_res["trial_completed"] == true, "Trial fully completed") - _assert(spire.is_in_trial == false, "Trial marked as finished") - _assert(mastery_sig["fired"] == true and mastery_sig["floor"] == 50, "spire_trial_completed emitted") - -func _test_timeout_defeat() -> void: - print("\n--- Test 5: Timeout Defeat ---") - var spire := EndlessSpireDungeonSystem.new() - spire.start_trial(false) - - var fail_sig := {"fired": false, "floor": 0} - spire.spire_trial_failed.connect(func(f: int, _r: String): - fail_sig["fired"] = true - fail_sig["floor"] = f - ) - - # Advance time past 120s limit - var tick = spire.update(125.0) - _assert(tick["status"] == "failed" and tick["reason"] == "TIME_EXPIRED", "Failed on timeout") - _assert(fail_sig["fired"] == true and fail_sig["floor"] == 1, "spire_trial_failed emitted") - _assert(spire.is_in_trial == false, "Trial ended") - -func _finish() -> void: - print("\n==========================================") - print("Endless Spire Dungeon Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Endless Spire Dungeon parity tests passed!") - quit(0) diff --git a/project/test_endless_spire_dungeon_parity.gd.uid b/project/test_endless_spire_dungeon_parity.gd.uid deleted file mode 100644 index 734a0a06..00000000 --- a/project/test_endless_spire_dungeon_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dpm4xk85qw21y diff --git a/project/test_energy_system_parity.gd b/project/test_energy_system_parity.gd deleted file mode 100644 index 2bc14a7c..00000000 --- a/project/test_energy_system_parity.gd +++ /dev/null @@ -1,167 +0,0 @@ -# test_energy_system_parity.gd —— 40250 官方能量碎片与水晶熔炼系统 1:1 回归测试套件 -extends SceneTree - -const EnergySystem = preload("res://energy_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_energy_system_parity (Direction 1: Energy System 1:1) ===") - _test_fragment_extraction() - _test_crystal_fusion() - _test_crystal_usage_and_buff_lifecycle() - _test_serialization() - _finish() - -func _test_fragment_extraction() -> void: - print("\n--- Test 1: Energy Fragment Extraction (40250 Quest Rules) ---") - var sys := EnergySystem.new() - var inv: Array = [ - {"vnum": 10, "name": "长剑+0", "level_limit": 1}, - {"vnum": 8000, "name": "木箭", "level_limit": 40, "type": 1, "subtype": 5}, - {"vnum": 160, "name": "战斗剑+9", "level_limit": 65} - ] - - # 1. Player level too low - var res_low_player = sys.extract_energy_fragments(inv, 2, 30) - _assert(res_low_player["ok"] == false, "Extraction rejected: player lv 30 < 35") - _assert(res_low_player["reason"] == "PLAYER_LEVEL_TOO_LOW", "Reason is PLAYER_LEVEL_TOO_LOW") - - # 2. Equipment level too low (< 35) - var res_low_equip = sys.extract_energy_fragments(inv, 0, 40) - _assert(res_low_equip["ok"] == false, "Extraction rejected: equip lv 1 < 35") - _assert(res_low_equip["reason"] == "EQUIP_LEVEL_TOO_LOW", "Reason is EQUIP_LEVEL_TOO_LOW") - - # 3. Arrow weapon prohibited - var res_arrow = sys.extract_energy_fragments(inv, 1, 40) - _assert(res_arrow["ok"] == false, "Extraction rejected: arrow weapon not allowed") - _assert(res_arrow["reason"] == "ARROW_NOT_ALLOWED", "Reason is ARROW_NOT_ALLOWED") - - var extract_sig := {"fired": false, "cnt": 0} - sys.fragments_extracted.connect(func(c: int, _n: String): - extract_sig["fired"] = true - extract_sig["cnt"] = c - ) - - # 4. Valid extraction of Lv 65 sword - var res_succ = sys.extract_energy_fragments(inv, 2, 70) - _assert(res_succ["ok"] == true, "Lv 65 sword extracted successfully") - _assert(inv[2] != null and inv[2]["vnum"] == EnergySystem.VNUM_ENERGY_STONE, "Slot 2 converted to Energy Stones") - _assert(inv[2]["count"] >= 3 and inv[2]["count"] <= 8, "Yield is between 3 and 8 fragments") - _assert(extract_sig["fired"] == true, "fragments_extracted signal emitted") - -func _test_crystal_fusion() -> void: - print("\n--- Test 2: Energy Crystal (51002) Fusion (30 Stones + 1000 Gold) ---") - var sys := EnergySystem.new() - var player_gold := {"gold": 500} # Less than 1000 - var inv: Array = [ - {"vnum": EnergySystem.VNUM_ENERGY_STONE, "name": "能量碎片", "count": 25}, - null - ] - - # 1. Gold not enough - var res_no_gold = sys.fuse_energy_crystal(inv, player_gold) - _assert(res_no_gold["ok"] == false, "Fusion rejected: gold 500 < 1000") - _assert(res_no_gold["reason"] == "NOT_ENOUGH_GOLD", "Reason is NOT_ENOUGH_GOLD") - - # 2. Stones not enough (25 < 30) - player_gold["gold"] = 5000 - var res_no_stone = sys.fuse_energy_crystal(inv, player_gold) - _assert(res_no_stone["ok"] == false, "Fusion rejected: stones 25 < 30") - _assert(res_no_stone["reason"] == "NOT_ENOUGH_STONES", "Reason is NOT_ENOUGH_STONES") - - # 3. Valid fusion with 35 stones - inv[0]["count"] = 35 - var fuse_sig := {"fired": false} - sys.crystal_fused.connect(func(_c: Dictionary): - fuse_sig["fired"] = true - ) - - var res_succ = sys.fuse_energy_crystal(inv, player_gold) - _assert(res_succ["ok"] == true, "Fusion succeeded") - _assert(player_gold["gold"] == 4000, "1,000 gold deducted (5000 -> 4000)") - _assert(inv[0]["count"] == 5, "Stones deducted (35 - 30 = 5 remaining)") - _assert(inv[1] != null and inv[1]["vnum"] == EnergySystem.VNUM_CHARGING_STONE, "Energy Crystal generated in slot 1") - _assert(fuse_sig["fired"] == true, "crystal_fused signal emitted") - -func _test_crystal_usage_and_buff_lifecycle() -> void: - print("\n--- Test 3: Crystal Usage, 10% Attribute Buff & 2-Hour Lifecycle ---") - var sys := EnergySystem.new() - var inv: Array = [ - {"vnum": EnergySystem.VNUM_CHARGING_STONE, "name": "能量水晶", "count": 1} - ] - var equipped: Array = [ - {"attack_power": 200, "defense": 0, "max_hp": 0}, - {"attack_power": 0, "defense": 150, "max_hp": 0}, - {"attack_power": 0, "defense": 0, "max_hp": 1000} - ] - var player_stats := {"attack_power": 500, "defense": 300, "max_hp": 5000} - - var act_sig := {"fired": false, "dur": 0.0, "pct": 0} - sys.energy_activated.connect(func(d: float, p: int): - act_sig["fired"] = true - act_sig["dur"] = d - act_sig["pct"] = p - ) - - var exp_sig := {"fired": false} - sys.energy_expired.connect(func(): - exp_sig["fired"] = true - ) - - # Use crystal - var use_res = sys.use_energy_crystal(inv, 0, equipped, player_stats) - _assert(use_res["ok"] == true, "Crystal activated") - _assert(sys.is_energy_active == true, "is_energy_active is true") - _assert(inv[0] == null, "Energy crystal consumed from inventory") - _assert(act_sig["fired"] == true, "energy_activated signal emitted") - _assert(act_sig["dur"] == 7200.0, "Duration is 7200.0s (2 hours)") - - # Check 10% stats: - # atk: 500 + 20 = 520 - # def: 300 + 15 = 315 - # hp: 5000 + 100 = 5100 - _assert(player_stats["attack_power"] == 520, "Attack boosted by 10% of 200 = +20") - _assert(player_stats["defense"] == 315, "Defense boosted by 10% of 150 = +15") - _assert(player_stats["max_hp"] == 5100, "HP boosted by 10% of 1000 = +100") - - # Advance time 7200s -> timeout - sys.update(7200.0, player_stats) - _assert(sys.is_energy_active == false, "Energy expired after 2 hours") - _assert(player_stats["attack_power"] == 500, "Attack cleanly reversed back to 500") - _assert(player_stats["defense"] == 300, "Defense cleanly reversed back to 300") - _assert(player_stats["max_hp"] == 5000, "HP cleanly reversed back to 5000") - _assert(exp_sig["fired"] == true, "energy_expired signal emitted") - -func _test_serialization() -> void: - print("\n--- Test 4: Serialization & Restoration ---") - var sys1 := EnergySystem.new() - sys1.is_energy_active = true - sys1.energy_time_remaining = 3600.0 - sys1.applied_bonuses = {"attack_power": 25, "defense": 18} - - var data = sys1.serialize() - var sys2 := EnergySystem.new() - sys2.deserialize(data) - - _assert(sys2.is_energy_active == true, "Restored is_energy_active") - _assert(is_equal_approx(sys2.energy_time_remaining, 3600.0), "Restored time remaining") - _assert(sys2.applied_bonuses["attack_power"] == 25, "Restored attack bonus") - -func _finish() -> void: - print("\n=======================================================") - print("Energy System Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_energy_system_parity.gd.uid b/project/test_energy_system_parity.gd.uid deleted file mode 100644 index ca929a90..00000000 --- a/project/test_energy_system_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://yfkpp5ksk57y diff --git a/project/test_epic_refine_parity.gd b/project/test_epic_refine_parity.gd deleted file mode 100644 index 75dab5dc..00000000 --- a/project/test_epic_refine_parity.gd +++ /dev/null @@ -1,183 +0,0 @@ -# test_epic_refine_parity.gd —— 40250 修元平史诗级装备升阶转换 1:1 纯净回归测试套件 -extends SceneTree - -const EpicRefineSystem = preload("res://epic_refine_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_epic_refine_parity (Direction 2: Seon-Pyeong Epic Refine 1:1) ===") - _test_recipe_definitions() - _test_material_and_gold_validation() - _test_weapon_epic_refine_and_inheritance() - _test_armor_epic_refine_and_inheritance() - _finish() - -func _test_recipe_definitions() -> void: - print("\n--- Test 1: Recipe Table & Eligibility Validation ---") - var ers := EpicRefineSystem.new() - - # Valid Lv65 +9 weapons - _assert(ers.can_epic_refine({"vnum": 169}), "Battle Sword +9 (169) is eligible") - _assert(ers.can_epic_refine({"vnum": 3159}), "Ghost Fang Blade +9 (3159) is eligible") - _assert(ers.can_epic_refine({"vnum": 1109}), "Partisan +9 (1109) is eligible") - _assert(ers.can_epic_refine({"vnum": 1119}), "Dragon Knife +9 (1119) is eligible") - _assert(ers.can_epic_refine({"vnum": 2149}), "Yellow Dragon Bow +9 (2149) is eligible") - _assert(ers.can_epic_refine({"vnum": 5109}), "Salvation Bell +9 (5109) is eligible") - - # Valid Lv66 +9 armors - _assert(ers.can_epic_refine({"vnum": 11299}), "Black Steel Armor +9 (11299) is eligible") - _assert(ers.can_epic_refine({"vnum": 11499}), "Black Wind Suit +9 (11499) is eligible") - _assert(ers.can_epic_refine({"vnum": 11699}), "Black Magic Armor +9 (11699) is eligible") - _assert(ers.can_epic_refine({"vnum": 11899}), "Black Clothing +9 (11899) is eligible") - - # Ineligible items - _assert(not ers.can_epic_refine({"vnum": 10}), "Wooden Sword (10) ineligible") - _assert(not ers.can_epic_refine({"vnum": 168}), "Battle Sword +8 (168) ineligible") - _assert(not ers.can_epic_refine({}), "Empty dict ineligible") - - # Check target vnums - _assert(ers.get_recipe(169)["target_vnum"] == 279, "Battle Sword +9 -> Triton Sword +0 (279)") - _assert(ers.get_recipe(11299)["target_vnum"] == 12019, "Black Steel +9 -> Blue Steel +0 (12019)") - -func _test_material_and_gold_validation() -> void: - print("\n--- Test 2: Material & Yang Requirement Validation ---") - var ers := EpicRefineSystem.new() - - var player_data := {"gold": 100000} # Less than 200,000 - var inv: Array = [ - {"vnum": 169, "name": "战神之剑+9", "count": 1}, - {"vnum": 71123, "name": "龙鳞", "count": 2}, # need 3 - {"vnum": 71129, "name": "龙爪", "count": 3} # need 4 - ] - - # Insufficient Yang - var r1 = ers.do_epic_refine(0, inv, player_data) - _assert(r1["ok"] == false and r1["reason"] == "INSUFFICIENT_YANG", "Blocked when Yang < 200,000") - - player_data["gold"] = 500000 # Give enough Yang - - # Missing Dragon Scales (has 2, needs 3) - var r2 = ers.do_epic_refine(0, inv, player_data) - _assert(r2["ok"] == false and r2["reason"] == "MISSING_DRAGON_SCALES", "Blocked when Dragon Scales < 3") - - inv[1]["count"] = 3 # Now have 3 scales - - # Missing Dragon Claws (has 3, needs 4) - var r3 = ers.do_epic_refine(0, inv, player_data) - _assert(r3["ok"] == false and r3["reason"] == "MISSING_DRAGON_CLAWS", "Blocked when Dragon Claws < 4") - - # Locked item blocked - inv[0]["is_locked"] = true - inv[2]["count"] = 4 - var r4 = ers.do_epic_refine(0, inv, player_data) - _assert(r4["ok"] == false and r4["reason"] == "ITEM_LOCKED", "Blocked when item is locked") - -func _test_weapon_epic_refine_and_inheritance() -> void: - print("\n--- Test 3: Weapon Refine (169 -> 279) with 100% 5-Attr & Sockets Preserved ---") - var ers := EpicRefineSystem.new() - var signal_data := {"fired": false, "old": 0, "new": 0} - ers.epic_refine_succeeded.connect(func(o: int, n: int, _item: Dictionary): - signal_data["fired"] = true - signal_data["old"] = o - signal_data["new"] = n - ) - - var base_weapon: Dictionary = { - "vnum": 169, - "name": "战神之剑+9", - "count": 1, - "attributes": [ - {"type": 17, "value": 10}, # 半人增伤 +10% - {"type": 15, "value": 10}, # 致命一击 +10% - {"type": 5, "value": 12}, # 力量 +12 - {"type": 16, "value": 10}, # 穿透一击 +10% - {"type": 9, "value": 20} # 施法加速 +20% - ], - "sockets": [28430, 28431, 28432], # 战士灵石+4, 刺客灵石+4, 修罗灵石+4 - "socket_count": 3 - } - - var player_data := {"gold": 500000} - var inv: Array = [ - base_weapon, - {"vnum": 71123, "name": "龙鳞", "count": 5}, # 5 scales (consume 3, leave 2) - {"vnum": 71129, "name": "龙爪", "count": 4} # 4 claws (consume all 4) - ] - - var res = ers.do_epic_refine(0, inv, player_data) - _assert(res["ok"] == true, "Epic refine succeeded") - _assert(signal_data["fired"] == true, "epic_refine_succeeded signal fired") - _assert(signal_data["old"] == 169 and signal_data["new"] == 279, "Signal values match 169 -> 279") - - # Check costs - _assert(player_data["gold"] == 300000, "200,000 Yang deducted (500k -> 300k)") - _assert(inv[1]["count"] == 2, "3 Dragon Scales consumed (5 -> 2)") - _assert(inv[2] == null, "4 Dragon Claws fully consumed (slot becomes null)") - - # Check upgraded item in slot 0 - var new_item = inv[0] - _assert(new_item["vnum"] == 279, "New item vnum is 279 (Triton Sword +0)") - _assert(new_item["name"] == "崔顿之剑+0", "New item name is 崔顿之剑+0") - - # Check 5 Attributes preserved exactly - _assert(new_item["attributes"].size() == 5, "All 5 attributes preserved") - _assert(new_item["attributes"][0]["type"] == 17 and new_item["attributes"][0]["value"] == 10, "Attr 1: Semi-Human +10% preserved") - _assert(new_item["attributes"][1]["type"] == 15 and new_item["attributes"][1]["value"] == 10, "Attr 2: Critical +10% preserved") - _assert(new_item["attributes"][2]["type"] == 5 and new_item["attributes"][2]["value"] == 12, "Attr 3: STR +12 preserved") - _assert(new_item["attributes"][3]["type"] == 16 and new_item["attributes"][3]["value"] == 10, "Attr 4: Piercing +10% preserved") - _assert(new_item["attributes"][4]["type"] == 9 and new_item["attributes"][4]["value"] == 20, "Attr 5: Casting Speed +20% preserved") - - # Check 3 Sockets preserved exactly - _assert(new_item["sockets"].size() == 3, "All 3 sockets preserved") - _assert(new_item["sockets"][0] == 28430, "Socket 1 preserved (28430)") - _assert(new_item["sockets"][1] == 28431, "Socket 2 preserved (28431)") - _assert(new_item["sockets"][2] == 28432, "Socket 3 preserved (28432)") - -func _test_armor_epic_refine_and_inheritance() -> void: - print("\n--- Test 4: Armor Refine (11299 -> 12019) with Attributes & Sockets ---") - var ers := EpicRefineSystem.new() - - var base_armor: Dictionary = { - "vnum": 11299, - "name": "黑钢甲+9", - "count": 1, - "attributes": [ - {"type": 1, "value": 2000}, # 最大生命 +2000 - {"type": 29, "value": 15}, # 剑防御 +15% - {"type": 30, "value": 15} # 双手防御 +15% - ], - "sockets": [28438, 28439, 28440] # 防御灵石+4, 闪避灵石+4, 生命灵石+4 - } - - var player_data := {"gold": 300000} - var inv: Array = [ - base_armor, - {"vnum": 71123, "name": "龙鳞", "count": 3}, - {"vnum": 71129, "name": "龙爪", "count": 4} - ] - - var res = ers.do_epic_refine(0, inv, player_data) - _assert(res["ok"] == true, "Armor epic refine succeeded") - _assert(inv[0]["vnum"] == 12019, "Armor converted to Blue Steel Armor +0 (12019)") - _assert(inv[0]["attributes"].size() == 3, "All 3 armor bonus attributes preserved") - _assert(inv[0]["attributes"][0]["value"] == 2000, "Max HP +2000 preserved") - _assert(inv[0]["sockets"].size() == 3, "All 3 armor sockets preserved") - -func _finish() -> void: - print("\n==========================================") - print("Epic Refine Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_epic_refine_parity.gd.uid b/project/test_epic_refine_parity.gd.uid deleted file mode 100644 index 9f5b739c..00000000 --- a/project/test_epic_refine_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c3ybhrt4ablle diff --git a/project/test_equipment_inspect_parity.gd b/project/test_equipment_inspect_parity.gd deleted file mode 100644 index 8cb0502a..00000000 --- a/project/test_equipment_inspect_parity.gd +++ /dev/null @@ -1,116 +0,0 @@ -# test_equipment_inspect_parity.gd —— 40250 装备透视检视与 GM 监察系统回归测试 -extends SceneTree - -const SystemClass = preload("res://equipment_inspect_system.gd") - -var _passed: int = 0 -var _failed: int = 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_equipment_inspect_parity (Batch 28 Direction 2) ===") - _test_open_and_load_slots() - _test_privacy_and_gm_bypass() - _test_slot_tooltip_generation() - _test_close_lifecycle() - - print("\n=== Result: %d Passed, %d Failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - else: - printerr("SOME TESTS FAILED!\n") - quit(0 if _failed == 0 else 1) - -func _test_open_and_load_slots() -> void: - print("\n--- Test 1: Open Dialog & Equipment Slots Loading ---") - var ins = SystemClass.new() - - var dummy_equips = { - SystemClass.WEAR_WEAPON: { - "vnum": 189, - "name": "毒剑+9 (Poison Sword+9)", - "sockets": [28430, 28431, 28432], - "attributes": [{ "type": 72, "value": 42 }] - }, - SystemClass.WEAR_BODY: { - "vnum": 11299, - "name": "黑铁战甲+9 (Black Steel Armor+9)", - "sockets": [28440, 28441, 0], - "attributes": [{ "type": 1, "value": 2000 }] - }, - SystemClass.WEAR_COSTUME_BODY: { - "vnum": 41001, - "name": "沙漠武士服 (Desert Warrior Body)", - "attributes": [{ "type": 1, "value": 1500 }] - } - } - - var r = ins.open_dialog(1001, "TargetWarrior", dummy_equips, true, false) - _assert(r["success"] == true, "Opened inspect dialog for TargetWarrior") - _assert(ins.is_open == true, "Inspect dialog is open") - _assert(ins.inspected_vid == 1001, "Target VID is 1001") - - # Verify Weapon Slot - var wpn = ins.get_slot_item(SystemClass.WEAR_WEAPON) - _assert(wpn["vnum"] == 189, "Weapon Vnum matches 189") - _assert(wpn["sockets"].size() == 3, "Weapon has 3 sockets") - - # Verify Costume Body Slot - var cst = ins.get_slot_item(SystemClass.WEAR_COSTUME_BODY) - _assert(cst["vnum"] == 41001, "Costume body loaded") - -func _test_privacy_and_gm_bypass() -> void: - print("\n--- Test 2: Privacy Shield & GM Privilege Bypass ---") - var ins = SystemClass.new() - var dummy_equips = { SystemClass.WEAR_WEAPON: { "vnum": 10, "name": "Sword+0" } } - - # 1. Normal player inspecting private player -> REJECTED - var r_fail = ins.open_dialog(1002, "PrivateLord", dummy_equips, false, false) - _assert(r_fail["error_code"] == "INSPECT_FORBIDDEN", "Reject inspection if target hidden privacy") - _assert(ins.is_open == false, "Dialog remains closed") - - # 2. GM inspecting private player -> BYPASS SUCCESS - var r_gm = ins.open_dialog(1002, "PrivateLord", dummy_equips, false, true) - _assert(r_gm["success"] == true, "GM successfully bypassed privacy shield") - _assert(ins.is_open == true, "Dialog opened for GM") - -func _test_slot_tooltip_generation() -> void: - print("\n--- Test 3: Slot Tooltip Generation on Hover ---") - var ins = SystemClass.new() - var dummy_equips = { - SystemClass.WEAR_WEAPON: { - "vnum": 189, - "name": "Poison Sword+9", - "sockets": [28430, 28431, 0], - "attributes": [{ "type": 72, "value": 45 }] - } - } - ins.open_dialog(1001, "TargetWarrior", dummy_equips, true, false) - - # Equipped weapon tooltip - var tip_wpn = ins.build_slot_tooltip(SystemClass.WEAR_WEAPON) - _assert(tip_wpn.contains("Poison Sword+9"), "Tooltip contains weapon name") - _assert(tip_wpn.contains("部位: 武器"), "Tooltip contains slot title") - _assert(tip_wpn.contains("属性 72 +45"), "Tooltip contains bonus attribute") - - # Empty shield slot tooltip - var tip_shield = ins.build_slot_tooltip(SystemClass.WEAR_SHIELD) - _assert(tip_shield.contains("[盾牌] 未佩戴装备"), "Empty slot displays placeholder notice") - -func _test_close_lifecycle() -> void: - print("\n--- Test 4: Close Dialog & Memory Cleanup ---") - var ins = SystemClass.new() - ins.open_dialog(1001, "Target", { SystemClass.WEAR_BODY: { "vnum": 1, "name": "Armor" } }) - _assert(ins.is_open == true, "Dialog open") - - ins.close_dialog() - _assert(ins.is_open == false, "Dialog closed") - _assert(ins.equipment_slots.is_empty(), "Slots wiped on close") - _assert(ins.get_slot_item(SystemClass.WEAR_BODY).is_empty(), "Cannot retrieve slot when closed") diff --git a/project/test_equipment_inspect_parity.gd.uid b/project/test_equipment_inspect_parity.gd.uid deleted file mode 100644 index 9d2814fa..00000000 --- a/project/test_equipment_inspect_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bpo135hh7nool diff --git a/project/test_event_manager_parity.gd b/project/test_event_manager_parity.gd deleted file mode 100644 index 94714d82..00000000 --- a/project/test_event_manager_parity.gd +++ /dev/null @@ -1,143 +0,0 @@ -# test_event_manager_parity.gd —— 40250 全服掉落活动与倍率调度器 1:1 纯净回归测试套件 -extends SceneTree - -const EventManagerSystem = preload("res://event_manager_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_event_manager_parity (Direction 4: Solo Event Manager 1:1) ===") - _test_rate_adjustments() - _test_event_toggling() - _test_event_drop_chances() - _test_box_opening() - _finish() - -func _test_rate_adjustments() -> void: - print("\n--- Test 1: Rate Adjustments (EXP, Drop, Yang) ---") - var ems := EventManagerSystem.new() - - # Invalid multipliers - var r_bad1 = ems.set_rate("exp", 0.5) - _assert(r_bad1["ok"] == false and r_bad1["reason"] == "INVALID_MULTIPLIER", "< 1.0 multiplier rejected") - - var r_bad2 = ems.set_rate("exp", 10.0) - _assert(r_bad2["ok"] == false and r_bad2["reason"] == "INVALID_MULTIPLIER", "> 5.0 multiplier rejected") - - var rate_signal := {"fired": false, "type": "", "mult": 0.0} - ems.rate_changed.connect(func(t: String, m: float): - rate_signal["fired"] = true - rate_signal["type"] = t - rate_signal["mult"] = m - ) - - # 1. EXP Rate 2.0x - var r_exp = ems.set_rate("exp", 2.0) - _assert(r_exp["ok"] == true, "EXP rate set to 2.0x") - _assert(ems.calculate_exp(1000) == 2000, "1000 base EXP doubled to 2000") - _assert(rate_signal["fired"] == true and rate_signal["type"] == "exp", "rate_changed signal fired") - - # 2. Gold Rate 3.0x - var r_gold = ems.set_rate("gold", 3.0) - _assert(r_gold["ok"] == true, "Gold rate set to 3.0x") - _assert(ems.calculate_gold(500) == 1500, "500 base Gold tripled to 1500") - - # 3. Drop Rate 1.5x - var r_drop = ems.set_rate("drop", 1.5) - _assert(r_drop["ok"] == true, "Drop rate set to 1.5x") - _assert(ems.drop_rate == 1.5, "drop_rate is 1.5") - -func _test_event_toggling() -> void: - print("\n--- Test 2: Event Toggling (Moonlight & Hexagonal) ---") - var ems := EventManagerSystem.new() - - var ev_signal := {"fired": false, "name": "", "active": false} - ems.event_state_changed.connect(func(n: String, act: bool): - ev_signal["fired"] = true - ev_signal["name"] = n - ev_signal["active"] = act - ) - - # Invalid event - var r_bad = ems.toggle_event("christmas", true) - _assert(r_bad["ok"] == false and r_bad["reason"] == "INVALID_EVENT_NAME", "Invalid event name rejected") - - # Toggle Moonlight event ON - var r1 = ems.toggle_event("moonlight", true) - _assert(r1["ok"] == true and ems.is_moonlight_active == true, "Moonlight event enabled") - _assert(ev_signal["fired"] == true and ev_signal["name"] == "moonlight" and ev_signal["active"] == true, "event_state_changed signal fired") - - # Toggle Hexagonal event ON - var r2 = ems.toggle_event("hexagonal", true) - _assert(r2["ok"] == true and ems.is_hexagonal_active == true, "Hexagonal event enabled") - - # Toggle Moonlight event OFF - ems.toggle_event("moonlight", false) - _assert(ems.is_moonlight_active == false, "Moonlight event disabled") - -func _test_event_drop_chances() -> void: - print("\n--- Test 3: Event Box Drops on Mob Kills ---") - var ems := EventManagerSystem.new() - ems.toggle_event("moonlight", true) - ems.toggle_event("hexagonal", true) - - var drop_signal := {"fired": false, "vnum": 0} - ems.event_box_dropped.connect(func(v: int, _n: String): - drop_signal["fired"] = true - drop_signal["vnum"] = v - ) - - # 1. Level difference > 15 (Mob Lv 10 vs Player Lv 50) -> No drop - var drops_far = ems.check_event_drops(10, 50, 0.01) - _assert(drops_far.is_empty(), "Level diff > 15 drops nothing") - - # 2. Level difference <= 15 with winning rolls - # Moonlight chance = 10% (0.10). Forced roll = 0.05 -> drops 50011 - # Hexagonal chance = 15% (0.15). Forced roll = 0.05 -> drops 50037 - var drops = ems.check_event_drops(50, 52, 0.05) - _assert(drops.size() == 2, "Both Moonlight and Hexagonal boxes dropped on winning roll") - _assert(drops[0]["vnum"] == EventManagerSystem.ITEM_MOONLIGHT_BOX, "Moonlight Box (50011) dropped") - _assert(drops[1]["vnum"] == EventManagerSystem.ITEM_HEXAGONAL_BOX, "Hexagonal Box (50037) dropped") - _assert(drop_signal["fired"] == true, "event_box_dropped signal fired") - - # 3. Losing roll (e.g. 0.80) -> No drop - var drops_fail = ems.check_event_drops(50, 52, 0.80) - _assert(drops_fail.is_empty(), "Losing roll drops nothing") - -func _test_box_opening() -> void: - print("\n--- Test 4: Event Box Opening (50011 Loot Pool) ---") - var ems := EventManagerSystem.new() - var inv: Array = [ - {"vnum": EventManagerSystem.ITEM_MOONLIGHT_BOX, "name": "月光宝盒", "count": 1}, - null - ] - - # Open Moonlight Box at slot 0 - var res = ems.open_event_box(0, inv) - _assert(res["ok"] == true, "Moonlight box opened successfully") - - var reward = res["reward"] - _assert(reward.has("vnum") and reward.has("name"), "Valid reward drawn from pool") - print(" Drew Moonlight Box reward: %s (vnum: %d)" % [reward["name"], reward["vnum"]]) - - var stored_slot = res["stored_slot"] - _assert(stored_slot >= 0 and inv[stored_slot] != null, "Reward saved in inventory") - _assert(inv[stored_slot]["vnum"] == reward["vnum"], "Inventory item matches reward") - -func _finish() -> void: - print("\n==========================================") - print("Event Manager Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_event_manager_parity.gd.uid b/project/test_event_manager_parity.gd.uid deleted file mode 100644 index ff7c4158..00000000 --- a/project/test_event_manager_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://gatt417wythe diff --git a/project/test_fish_clam_roast_parity.gd b/project/test_fish_clam_roast_parity.gd deleted file mode 100644 index 473dc05b..00000000 --- a/project/test_fish_clam_roast_parity.gd +++ /dev/null @@ -1,207 +0,0 @@ -# test_fish_clam_roast_parity.gd —— Metin2 40250 活鱼宰杀、珍珠蚌开珠与篝火炙烤盛宴 1:1 测试套件 -extends SceneTree - -const FishClamRoastSystem = preload("res://fish_clam_roast_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_fish_clam_roast_parity (Direction 1: Fish Gutting & Clam Pearls 1:1) ===") - - test_live_fish_gutting() - test_clam_pearl_opening() - test_campfire_grilling() - test_cooked_fish_buffs() - test_serialization() - - print("\n=======================================================") - print("Fish & Clam Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - - if _failed > 0: - quit(1) - else: - quit(0) - -func test_live_fish_gutting() -> void: - print("\n--- Test 1: Live Fish Gutting (40250 1:1) ---") - var system = FishClamRoastSystem.new() - - var inv: Array = [ - {"vnum": 27806, "count": 1}, # 活鲤鱼 (Sazan) - {"vnum": 27808, "count": 1}, # 活草鱼 (Ot Sazanı) - {"vnum": 27803, "count": 1}, # 活鲈鱼 - {"vnum": 27803, "count": 1}, # 活鲈鱼 - {"vnum": 27803, "count": 1} # 活鲈鱼 - ] - - # 1. 产生死鱼 (roll >= 4001) - var res = system.gut_live_fish(inv, 0, 5000) - _assert(res["ok"] == true, "Gut fish succeeded") - _assert(res["result"] == "dead_fish", "Result is dead_fish") - _assert(res["vnum"] == 27836, "Spawned dead Sazan (27836)") - - # 2. 产生鱼骨 (roll 2001~4000) - res = system.gut_live_fish(inv, 1, 3000) - _assert(res["ok"] == true, "Gut fish succeeded for bone") - _assert(res["result"] == "fish_bone", "Result is fish_bone") - _assert(res["vnum"] == 27995, "Spawned Fish Bone (27995)") - - # 3. 产生珍珠蚌 (roll <= 1000) - res = system.gut_live_fish(inv, 2, 500) - _assert(res["ok"] == true, "Found clam inside fish!") - _assert(res["result"] == "clam", "Result is clam") - _assert(res["vnum"] == 27987, "Spawned Clam (27987)") - - # 4. 产生蚯蚓 (roll 1001~1500) - res = system.gut_live_fish(inv, 3, 1200) - _assert(res["ok"] == true, "Found earthworm") - _assert(res["result"] == "earthworm", "Result is earthworm") - _assert(res["vnum"] == 27801, "Spawned Earthworm (27801)") - - # 5. 滑入水中溜走 (roll > 1500) - res = system.gut_live_fish(inv, 4, 1800) - _assert(res["ok"] == true, "Escaped") - _assert(res["result"] == "escape", "Fish escaped back to water") - -func test_clam_pearl_opening() -> void: - print("\n--- Test 2: Clam Opening & 3 Pearls (40250 1:1) ---") - var system = FishClamRoastSystem.new() - - var inv: Array = [ - {"vnum": 27987, "count": 1}, # 蚌 1: 碎石 - {"vnum": 27987, "count": 1}, # 蚌 2: 空 - {"vnum": 27987, "count": 1}, # 蚌 3: 白珍珠 - {"vnum": 27987, "count": 1}, # 蚌 4: 蓝珍珠 - {"vnum": 27987, "count": 1} # 蚌 5: 血红珍珠 - ] - - # 1. 碎石 (branch roll <= 50) - var res = system.open_clam(inv, 0, 30) - _assert(res["ok"] == true, "Opened clam for stone") - _assert(res["result"] == "stone", "Result is stone") - _assert(res["vnum"] == 27990, "Vnum is 27990 (Stone Piece)") - - # 2. 空蚌 (branch roll > 50, pearl roll <= 80) - res = system.open_clam(inv, 1, 70, 50) - _assert(res["ok"] == true, "Opened empty clam") - _assert(res["result"] == "empty", "Result is empty") - - # 3. 白珍珠 (pearl roll 81~90) - res = system.open_clam(inv, 2, 70, 85) - _assert(res["ok"] == true, "Opened White Pearl") - _assert(res["result"] == "white_pearl", "Result is white_pearl") - _assert(res["vnum"] == 27992, "Vnum is 27992 (White Pearl)") - - # 4. 蓝珍珠 (pearl roll 91~97) - res = system.open_clam(inv, 3, 70, 95) - _assert(res["ok"] == true, "Opened Blue Pearl") - _assert(res["result"] == "blue_pearl", "Result is blue_pearl") - _assert(res["vnum"] == 27993, "Vnum is 27993 (Blue Pearl)") - - # 5. 血红珍珠 (pearl roll >= 98) - res = system.open_clam(inv, 4, 70, 99) - _assert(res["ok"] == true, "Opened Blood Pearl") - _assert(res["result"] == "blood_pearl", "Result is blood_pearl") - _assert(res["vnum"] == 27994, "Vnum is 27994 (Blood Pearl)") - -func test_campfire_grilling() -> void: - print("\n--- Test 3: Campfire Grilling (40250 1:1) ---") - var system = FishClamRoastSystem.new() - - var inv: Array = [ - {"vnum": 27836, "count": 3}, # 3 条死鲤鱼 - {"vnum": 27838, "count": 1} # 1 条死草鱼 - ] - - # 未靠近营火拒绝烤制 - var res = system.grill_fish(inv, 0, false) - _assert(res["ok"] == false, "Rejected grilling without campfire") - _assert(res["reason"] == "NO_CAMPFIRE", "Reason is NO_CAMPFIRE") - - # 点燃营火 - system.place_campfire(600.0) - _assert(system.is_near_active_campfire == true, "Campfire is active") - - # 烤制 3 条死鲤鱼 (27836 -> 27866 烤鲤鱼) - res = system.grill_fish(inv, 0) - _assert(res["ok"] == true, "Grilling Sazan succeeded") - _assert(res["cooked_vnum"] == 27866, "Cooked Sazan vnum is 27866") - _assert(res["count"] == 3, "Preserved count 3") - _assert(inv[0] != null and inv[0]["vnum"] == 27866 and inv[0]["count"] == 3, "Slot holds 3 cooked fish") - -func test_cooked_fish_buffs() -> void: - print("\n--- Test 4: Cooked Fish Consumption & Buffs (40250 1:1) ---") - var system = FishClamRoastSystem.new() - - var inv: Array = [ - {"vnum": 27866, "count": 1}, # 烤鲤鱼: 移速 +20 (600s) - {"vnum": 27868, "count": 1}, # 烤草鱼: 攻速 +20 (600s) - {"vnum": 27877, "count": 1}, # 烤泥鳅: 隐身 (300s) - {"vnum": 27874, "count": 1}, # 烤海鲈鱼: 治愈中毒 - {"vnum": 27863, "count": 1} # 烤鲈鱼: 恢复 180 HP - ] - - # 1. 食用烤鲤鱼 - var res = system.eat_cooked_fish(inv, 0) - _assert(res["ok"] == true, "Ate cooked Sazan") - _assert(res["effect"] == "move_speed", "Effect is move_speed") - _assert(res["value"] == 20, "Value is 20") - _assert(res["duration"] == 600.0, "Duration is 600s") - - # 2. 食用烤草鱼 - res = system.eat_cooked_fish(inv, 1) - _assert(res["ok"] == true, "Ate cooked Ot Sazani") - _assert(res["effect"] == "attack_speed", "Effect is attack_speed") - _assert(res["value"] == 20, "Value is 20") - - # 3. 食用烤泥鳅 - res = system.eat_cooked_fish(inv, 2) - _assert(res["ok"] == true, "Ate cooked Copra") - _assert(res["effect"] == "invisibility", "Effect is invisibility") - _assert(res["duration"] == 300.0, "Duration is 300s") - - # 4. 食用解毒鱼与瞬间恢复鱼 - res = system.eat_cooked_fish(inv, 3) - _assert(res["effect"] == "cure_poison", "Effect is cure_poison") - res = system.eat_cooked_fish(inv, 4) - _assert(res["effect"] == "instant_hp" and res["value"] == 180, "Instant HP restored 180") - - # 检查增益状态列表 - var buffs = system.get_active_buffs() - _assert(buffs.has("move_speed"), "Active buff has move_speed") - _assert(buffs.has("attack_speed"), "Active buff has attack_speed") - _assert(buffs.has("invisibility"), "Active buff has invisibility") - - # 时间推移 100 秒 - system.update(100.0) - _assert(buffs["invisibility"]["time_remaining"] <= 200.0, "Invisibility ticked down") - - # 时间推移 250 秒 (隐身 300s 到期清除) - system.update(250.0) - _assert(not system.get_active_buffs().has("invisibility"), "Invisibility expired and removed") - _assert(system.get_active_buffs().has("move_speed"), "Move speed still active") - -func test_serialization() -> void: - print("\n--- Test 5: Buff Serialization & Restoration ---") - var system = FishClamRoastSystem.new() - system.place_campfire(500.0) - var inv: Array = [{"vnum": 27866, "count": 1}] - system.eat_cooked_fish(inv, 0) - - var data = system.serialize() - _assert(data.has("active_buffs") and data["active_buffs"].has("move_speed"), "Serialized move_speed buff") - - var restored = FishClamRoastSystem.new() - restored.deserialize(data) - _assert(restored.is_near_active_campfire == true, "Restored active campfire") - _assert(restored.get_active_buffs().has("move_speed"), "Restored move_speed buff") diff --git a/project/test_fish_clam_roast_parity.gd.uid b/project/test_fish_clam_roast_parity.gd.uid deleted file mode 100644 index 7f977309..00000000 --- a/project/test_fish_clam_roast_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dm545ejmajyj3 diff --git a/project/test_fishing_cooking_camp_parity.gd b/project/test_fishing_cooking_camp_parity.gd deleted file mode 100644 index 179bd38c..00000000 --- a/project/test_fishing_cooking_camp_parity.gd +++ /dev/null @@ -1,162 +0,0 @@ -# test_fishing_cooking_camp_parity.gd —— 40250 野外钓鱼烹饪与露营增益 1:1 回归测试套件 -extends SceneTree - -const FishingCookingCampSystem = preload("res://fishing_cooking_camp_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_fishing_cooking_camp_parity (Direction 2: Fishing Cooking Camp 1:1) ===") - _test_campfire_lighting() - _test_cooking_fish_recipes() - _test_eating_and_stat_buffs() - _test_buff_expiration_and_cleanup() - _finish() - -func _test_campfire_lighting() -> void: - print("\n--- Test 1: Lighting & Extinguishing Campfire (27600) ---") - var camp := FishingCookingCampSystem.new() - var inv: Array = [ - {"vnum": FishingCookingCampSystem.CAMPFIRE_KIT_VNUM, "name": "营火工具", "count": 2} - ] - - var light_sig := {"fired": false, "time": 0.0} - camp.campfire_lit.connect(func(_p: Vector3, d: float): - light_sig["fired"] = true - light_sig["time"] = d - ) - - var res = camp.light_campfire(Vector3(10, 0, 10), inv) - _assert(res["ok"] == true, "Campfire lit") - _assert(camp.is_campfire_active == true, "Campfire is active") - _assert(inv[0]["count"] == 1, "1 campfire kit consumed (2 -> 1)") - _assert(light_sig["fired"] == true and light_sig["time"] == 300.0, "campfire_lit emitted with 300s") - - # Extinguish on timeout (advance 305 seconds) - var ext_sig := {"fired": false} - camp.campfire_extinguished.connect(func(): ext_sig["fired"] = true) - - var dummy_p := {} - camp.update(305.0, dummy_p) - _assert(camp.is_campfire_active == false, "Campfire extinguished") - _assert(ext_sig["fired"] == true, "campfire_extinguished emitted") - -func _test_cooking_fish_recipes() -> void: - print("\n--- Test 2: Cooking Fish Recipes on Campfire ---") - var camp := FishingCookingCampSystem.new() - var inv: Array = [ - {"vnum": 27600, "count": 1}, - {"vnum": 27802, "name": "生鲤鱼", "count": 1}, - {"vnum": 27815, "name": "生红鳟鱼", "count": 2} - ] - - # Try cook without active fire - var fail_c = camp.cook_fish(1, inv) - _assert(fail_c["ok"] == false and fail_c["reason"] == "NO_ACTIVE_CAMPFIRE", "Cannot cook without campfire") - - # Light fire - camp.light_campfire(Vector3.ZERO, inv) - - var cook_sig := {"fired": false, "name": ""} - camp.fish_cooked.connect(func(_raw: int, _ckd: int, cname: String): - cook_sig["fired"] = true - cook_sig["name"] = cname - ) - - # Cook Carp (slot 1) - var res1 = camp.cook_fish(1, inv) - _assert(res1["ok"] == true, "Cooked Carp") - _assert(cook_sig["fired"] == true and cook_sig["name"] == "烤鲤鱼", "fish_cooked signal emitted") - - var has_cooked_carp := false - for it in inv: - if it != null and it.get("vnum", 0) == 27832: - has_cooked_carp = true - break - _assert(has_cooked_carp, "Cooked carp found in inventory") - - # Cook Trout (slot 2) - camp.cook_fish(2, inv) - _assert(inv[2]["count"] == 1, "Raw trout count decremented (2 -> 1)") - - var has_cooked_trout := false - for it in inv: - if it != null and it.get("vnum", 0) == 27845: - has_cooked_trout = true - break - _assert(has_cooked_trout, "Cooked trout found in inventory") - -func _test_eating_and_stat_buffs() -> void: - print("\n--- Test 3: Eating Cooked Fish & 30-Minute Buffs ---") - var camp := FishingCookingCampSystem.new() - var player := { - "max_hp": 8000, - "atk": 500, - "move_speed": 100 - } - var inv: Array = [ - {"vnum": 27832, "name": "烤鲤鱼", "count": 1}, - {"vnum": 27845, "name": "烤红鳟鱼", "count": 1} - ] - - var buff_sig := {"fired": false, "id": "", "dur": 0.0} - camp.food_buff_applied.connect(func(bid: String, _n: String, d: float): - buff_sig["fired"] = true - buff_sig["id"] = bid - buff_sig["dur"] = d - ) - - # Eat Grilled Carp (max_hp +1000) - var eat1 = camp.eat_cooked_fish(0, inv, player) - _assert(eat1["ok"] == true, "Ate Grilled Carp") - _assert(player["max_hp"] == 9000, "Max HP increased 8000 -> 9000 (+1000)") - _assert(inv[0] == null, "Grilled carp consumed") - _assert(buff_sig["fired"] == true and buff_sig["id"] == "buff_grilled_carp", "food_buff_applied emitted") - - # Eat Grilled Trout (atk +50) - var eat2 = camp.eat_cooked_fish(1, inv, player) - _assert(eat2["ok"] == true, "Ate Grilled Trout") - _assert(player["atk"] == 550, "Atk increased 500 -> 550 (+50)") - _assert(camp.active_buffs.size() == 2, "2 active food buffs running") - -func _test_buff_expiration_and_cleanup() -> void: - print("\n--- Test 4: Buff Duration & Clean Expiration ---") - var camp := FishingCookingCampSystem.new() - var player := {"max_hp": 8000} - var inv: Array = [{"vnum": 27832, "name": "烤鲤鱼", "count": 1}] - - camp.eat_cooked_fish(0, inv, player) - _assert(player["max_hp"] == 9000, "HP is 9000") - - var expire_sig := {"fired": false, "id": ""} - camp.food_buff_expired.connect(func(bid: String): - expire_sig["fired"] = true - expire_sig["id"] = bid - ) - - # Simulate 1805 seconds (exceeding 1800s duration) - camp.update(1805.0, player) - _assert(expire_sig["fired"] == true and expire_sig["id"] == "buff_grilled_carp", "food_buff_expired emitted") - _assert(player["max_hp"] == 8000, "Max HP cleanly reverted back to baseline 8000") - _assert(camp.active_buffs.is_empty(), "active_buffs cleared") - -func _finish() -> void: - print("\n==========================================") - print("Fishing Cooking Camp Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Fishing Cooking Camp parity tests passed!") - quit(0) diff --git a/project/test_fishing_cooking_camp_parity.gd.uid b/project/test_fishing_cooking_camp_parity.gd.uid deleted file mode 100644 index c8b5bc05..00000000 --- a/project/test_fishing_cooking_camp_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://eofbxtt6ac3b diff --git a/project/test_fishing_mining_parity.gd b/project/test_fishing_mining_parity.gd deleted file mode 100644 index 7dfdfb50..00000000 --- a/project/test_fishing_mining_parity.gd +++ /dev/null @@ -1,100 +0,0 @@ -# test_fishing_mining_parity.gd —— 验证方向 4:钓鱼、开蚌壳出珍珠与采矿生活系统 40250 1:1 对齐 -extends SceneTree - -const FishingMiningSystem = preload("res://fishing_mining_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_fishing_mining_parity (Direction 4: Fishing & Mining 1:1) ===") - test_fishing_equipment_detection() - test_fishing_flow_and_bait_consumption() - test_open_live_fish_drops() - test_open_clam_pearl_rates() - test_mining_veins_and_ores() - - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed > 0: - printerr("TEST FAILED!") - quit(1) - else: - print("ALL TESTS PASSED!") - quit(0) - -func test_fishing_equipment_detection() -> void: - print("\n--- Test 1: Fishing Equipment Identification ---") - _assert(FishingMiningSystem.is_fishing_rod(27400), "27400 is Fishing Rod +0") - _assert(FishingMiningSystem.is_fishing_rod(27490), "27490 is Fishing Rod +9") - _assert(not FishingMiningSystem.is_fishing_rod(10), "Sword is not a fishing rod") - - _assert(FishingMiningSystem.is_bait(27800), "27800 (Earthworm) is valid bait") - _assert(FishingMiningSystem.is_bait(27801), "27801 (Fish Paste) is valid bait") - _assert(not FishingMiningSystem.is_bait(27001), "Potion is not bait") - -func test_fishing_flow_and_bait_consumption() -> void: - print("\n--- Test 2: Fishing Flow & Bait Requirements (fishing.cpp:210) ---") - # 1. Fishing without bait is rejected - var res_no_bait := FishingMiningSystem.perform_fishing(false, 0) - _assert(not res_no_bait.ok and res_no_bait.code == "NO_BAIT", "Fishing without bait is rejected") - - # 2. Fishing with bait and force success - var res_success := FishingMiningSystem.perform_fishing(true, 5, true) - _assert(res_success.ok and res_success.code == "SUCCESS", "Fishing succeeded with bait") - _assert(res_success.consumed_bait, "Bait consumed on fish attempt") - _assert(FishingMiningSystem.is_live_fish(res_success.fish_vnum), "Caught item is a live fish") - -func test_open_live_fish_drops() -> void: - print("\n--- Test 3: Opening Live Fish (Fishbone, Dead Fish, Clam) ---") - # Live fish 27802 (Golden Carp) - var drops := {} - for _i in 100: - var res := FishingMiningSystem.open_live_fish(27802) - if res.ok: - drops[res.code] = true - - _assert(drops.has("DEAD_FISH"), "Opening fish can yield dead fish") - _assert(drops.has("FISHBONE"), "Opening fish can yield Fishbone (27990)") - _assert(drops.has("CLAM"), "Opening fish can yield Clam (27987)") - -func test_open_clam_pearl_rates() -> void: - print("\n--- Test 4: Opening Clam for Pearls (White, Blue, Blood Pearls) ---") - # Force opening White Pearl (27992) - var res_white := FishingMiningSystem.open_clam(FishingMiningSystem.VNUM_WHITE_PEARL) - _assert(res_white.ok and res_white.result_vnum == 27992, "Yields White Pearl (27992)") - - # Force opening Blue Pearl (27993) - var res_blue := FishingMiningSystem.open_clam(FishingMiningSystem.VNUM_BLUE_PEARL) - _assert(res_blue.ok and res_blue.result_vnum == 27993, "Yields Blue Pearl (27993)") - - # Force opening Blood Pearl (27994) - var res_blood := FishingMiningSystem.open_clam(FishingMiningSystem.VNUM_BLOOD_PEARL) - _assert(res_blood.ok and res_blood.result_vnum == 27994, "Yields Blood Pearl (27994)") - -func test_mining_veins_and_ores() -> void: - print("\n--- Test 5: Mining Pickaxe, Veins & Rough Ore Yield (mining.cpp) ---") - _assert(FishingMiningSystem.is_pickaxe(29101), "29101 is Pickaxe +0") - _assert(FishingMiningSystem.is_pickaxe(29110), "29110 is Pickaxe +9") - _assert(not FishingMiningSystem.is_pickaxe(10), "Sword is not a pickaxe") - - _assert(FishingMiningSystem.is_vein(20047), "20047 is Diamond Vein") - _assert(FishingMiningSystem.is_vein(20059), "20059 is Heaven's Tear Vein") - _assert(not FishingMiningSystem.is_vein(101), "Wild dog is not a vein") - - # Mining Diamond Vein (20047 -> 50601) - var mine_res := FishingMiningSystem.perform_mining(20047, 5, true) - _assert(mine_res.ok and mine_res.code == "SUCCESS", "Mining succeeded") - _assert(mine_res.ore_vnum == 50601, "Yields Diamond Ore (50601)") - _assert(mine_res.count >= 1 and mine_res.count <= 3, "Yields 1..3 rough ores") - - # Mining invalid target rejected - var mine_bad := FishingMiningSystem.perform_mining(99999, 0) - _assert(not mine_bad.ok and mine_bad.code == "NOT_A_VEIN", "Mining invalid target rejected") diff --git a/project/test_fishing_mining_parity.gd.uid b/project/test_fishing_mining_parity.gd.uid deleted file mode 100644 index 9e08ba73..00000000 --- a/project/test_fishing_mining_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bumaq1hdlegee diff --git a/project/test_fishing_rod_reel_parity.gd b/project/test_fishing_rod_reel_parity.gd deleted file mode 100644 index e31ba097..00000000 --- a/project/test_fishing_rod_reel_parity.gd +++ /dev/null @@ -1,169 +0,0 @@ -# test_fishing_rod_reel_parity.gd —— Metin2 40250 钓鱼抛竿拉杆与浮漂反应小游戏 1:1 测试套件 -extends SceneTree - -const FishingRodReelSystem = preload("res://fishing_rod_reel_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_fishing_rod_reel_parity (Direction 3: Fishing Rod & Reel 1:1) ===") - - test_bait_attachment_and_cast() - test_premature_pull() - test_bite_and_successful_catch() - test_reaction_timeout() - test_rod_refine_at_fisherman() - test_serialization() - - print("\n=======================================================") - print("Fishing Rod & Reel Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - - if _failed > 0: - quit(1) - else: - quit(0) - -func test_bait_attachment_and_cast() -> void: - print("\n--- Test 1: Bait Attachment & Line Casting (40250 1:1) ---") - var system = FishingRodReelSystem.new() - system.equip_rod(0) - - var inv: Array = [ - {"vnum": 27801, "count": 2}, # 蚯蚓 x2 (slot 0) - {"vnum": 50027, "count": 1} # 彩票 (slot 1, 非鱼饵) - ] - - # 未挂饵尝试抛竿 -> 拦截 - var res = system.cast_line() - _assert(res["ok"] == false, "Rejected casting without bait") - _assert(res["reason"] == "NO_BAIT", "Reason is NO_BAIT") - - # 非鱼饵道具挂饵 -> 拦截 - res = system.attach_bait(inv, 1) - _assert(res["ok"] == false, "Rejected invalid bait item") - _assert(res["reason"] == "NOT_BAIT", "Reason is NOT_BAIT") - - # 挂上蚯蚓 - res = system.attach_bait(inv, 0) - _assert(res["ok"] == true, "Attached earthworm bait") - _assert(system.has_bait == true, "System has bait") - _assert(inv[0]["count"] == 1, "Consumed 1 earthworm") - - # 抛竿 - res = system.cast_line(3.0) - _assert(res["ok"] == true, "Cast fishing line") - _assert(system.is_fishing == true, "State is fishing") - _assert(system.has_bait == false, "Bait consumed on cast") - -func test_premature_pull() -> void: - print("\n--- Test 2: Premature Rod Pull (40250 1:1) ---") - var system = FishingRodReelSystem.new() - system.equip_rod(0, 0) - var inv: Array = [{"vnum": 27801, "count": 1}] - system.attach_bait(inv, 0) - system.cast_line(5.0) # 5 秒后咬钩 - - # 仅过 1 秒便起竿 (尚未咬钩) - system.update(1.0) - var pull_res = system.pull_rod(inv) - _assert(pull_res["ok"] == false, "Premature pull failed to catch fish") - _assert(pull_res["reason"] == "PULLED_TOO_EARLY", "Reason is PULLED_TOO_EARLY") - _assert(system.rod_practice_points == 1, "Practiced +1 point even on premature pull") - _assert(system.is_fishing == false, "Fishing session ended") - -func test_bite_and_successful_catch() -> void: - print("\n--- Test 3: Fish Bite & Successful Catch (40250 1:1) ---") - var system = FishingRodReelSystem.new() - system.equip_rod(2, 5) # 钓竿+2,已有 5 点熟练度 - - var bite_signals := [] - system.fish_bite.connect(func(window): - bite_signals.append(window) - ) - - var inv: Array = [ - {"vnum": 27801, "count": 1} - ] - system.attach_bait(inv, 0) - system.cast_line(2.0) - - # 推进 2 秒,触发鱼咬钩 - system.update(2.0) - _assert(system.is_bite_active == true, "Fish is biting!") - _assert(bite_signals.size() == 1, "fish_bite signal emitted") - - # 提竿并强制捕获草鱼 (CATCH_POOL 索引 2) - var pull_res = system.pull_rod(inv, 2, true) - _assert(pull_res["ok"] == true, "Successfully caught fish") - _assert(pull_res["fish_vnum"] == 27808, "Caught Ot Sazani (27808)") - _assert(pull_res["practice_points"] == 6, "Practice points increased from 5 to 6") - # 检查背包是否新增了草鱼 - var has_fish := false - for it in inv: - if it != null and it.get("vnum", 0) == 27808: - has_fish = true - _assert(has_fish == true, "Caught fish inserted into inventory") - -func test_reaction_timeout() -> void: - print("\n--- Test 4: Reaction Window Timeout (40250 1:1) ---") - var system = FishingRodReelSystem.new() - system.equip_rod(0, 0) - var inv: Array = [{"vnum": 27801, "count": 1}] - system.attach_bait(inv, 0) - system.cast_line(1.0) - - system.update(1.0) # 咬钩 - _assert(system.is_bite_active == true, "Bite active") - - # 反应窗口为 3 秒,推进 3.5 秒超时 - system.update(3.5) - _assert(system.is_fishing == false, "Fishing ended due to timeout") - _assert(system.rod_practice_points == 1, "Practice point +1 on timeout") - -func test_rod_refine_at_fisherman() -> void: - print("\n--- Test 5: Rod Refine at Fisherman (40250 1:1) ---") - var system = FishingRodReelSystem.new() - # 设定钓竿+0,需要 10 点满熟练度 - system.equip_rod(0, 8) - - # 未满点数尝试精炼 -> 拦截 - var res = system.refine_rod_at_fisherman() - _assert(res["ok"] == false, "Rejected refine with 8/10 points") - _assert(res["reason"] == "PRACTICE_NOT_MAXED", "Reason is PRACTICE_NOT_MAXED") - - # 补满点数 10/10 - system.equip_rod(0, 10) - _assert(system.can_refine_rod() == true, "Rod is ready to refine") - - # 成功升级为钓鱼竿+1 (chance 100%) - res = system.refine_rod_at_fisherman(50) - _assert(res["ok"] == true and res["success"] == true, "Upgraded to Rod+1") - _assert(system.rod_level == 1, "New rod level is 1") - _assert(system.rod_practice_points == 0, "Practice points reset to 0") - - # 模拟失败降级: 设定钓竿+1 满点数 (20 点),强制失败 roll 95 > 90 - system.equip_rod(1, 20) - res = system.refine_rod_at_fisherman(95) - _assert(res["ok"] == true and res["success"] == false, "Refine failed and degraded") - _assert(system.rod_level == 0, "Degraded back to Rod+0") - -func test_serialization() -> void: - print("\n--- Test 6: Serialization & Restoration ---") - var system = FishingRodReelSystem.new() - system.equip_rod(3, 35) - var data = system.serialize() - - var restored = FishingRodReelSystem.new() - restored.deserialize(data) - _assert(restored.rod_level == 3, "Restored rod level 3") - _assert(restored.rod_practice_points == 35, "Restored practice points 35") diff --git a/project/test_fishing_rod_reel_parity.gd.uid b/project/test_fishing_rod_reel_parity.gd.uid deleted file mode 100644 index 87973b1b..00000000 --- a/project/test_fishing_rod_reel_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c10rqo2dewmgi diff --git a/project/test_flame_dungeon_razador_parity.gd b/project/test_flame_dungeon_razador_parity.gd deleted file mode 100644 index 85442187..00000000 --- a/project/test_flame_dungeon_razador_parity.gd +++ /dev/null @@ -1,169 +0,0 @@ -# test_flame_dungeon_razador_parity.gd —— 40250 红龙烈焰神殿与拉扎多尔试炼 1:1 回归测试套件 -extends SceneTree - -const FlameDungeonRazadorSystem = preload("res://flame_dungeon_razador_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_flame_dungeon_razador_parity (Direction 2: Flame Dungeon Razador 1:1) ===") - _test_entry_and_ticket_consumption() - _test_stage_advancement_and_altars() - _test_razador_boss_slay_and_rewards() - _test_timeout_failure() - _test_serialization() - _finish() - -func _test_entry_and_ticket_consumption() -> void: - print("\n--- Test 1: Flame Dungeon Entry & Ticket Consumption ---") - var sys := FlameDungeonRazadorSystem.new() - var inv: Array = [ - {"vnum": FlameDungeonRazadorSystem.VNUM_TICKET_71095, "name": "通行证", "count": 2} - ] - - # 1. Level too low (< 100) - var res_low = sys.enter_dungeon(95, inv) - _assert(res_low["ok"] == false, "Entry rejected: level 95 < 100") - _assert(res_low["reason"] == "LEVEL_TOO_LOW", "Reason is LEVEL_TOO_LOW") - - var enter_sig := {"fired": false, "dur": 0.0} - sys.dungeon_entered.connect(func(d: float): - enter_sig["fired"] = true - enter_sig["dur"] = d - ) - - # 2. Valid entry at Lv 100 - var res_succ = sys.enter_dungeon(100, inv) - _assert(res_succ["ok"] == true, "Entered Flame Dungeon successfully") - _assert(sys.is_active == true, "Dungeon is active") - _assert(sys.current_stage == 1, "Initial stage is 1") - _assert(inv[0]["count"] == 1, "Ticket count decremented 2 -> 1") - _assert(enter_sig["fired"] == true, "dungeon_entered signal emitted") - _assert(enter_sig["dur"] == 2700.0, "Time limit is 2700.0s (45 minutes)") - -func _test_stage_advancement_and_altars() -> void: - print("\n--- Test 2: Stage Advancement & Altar Lighting ---") - var sys := FlameDungeonRazadorSystem.new() - var inv: Array = [{"vnum": FlameDungeonRazadorSystem.VNUM_TICKET_71130, "name": "烈焰通行证", "count": 1}] - sys.enter_dungeon(105, inv) - - # Advance stages 1 -> 2 -> 3 -> 4 -> 5 - for i in range(4): - sys.advance_stage() - - _assert(sys.current_stage == 5, "Reached Stage 5 (Fire Altars)") - - var altar_sig := {"fired": false, "lit": 0} - sys.altar_lit.connect(func(l: int, _t: int): - altar_sig["fired"] = true - altar_sig["lit"] = l - ) - - # Light 4 altars - for i in range(4): - var res_altar = sys.light_fire_altar() - _assert(res_altar["stage_cleared"] == false, "Stage not yet cleared with < 5 altars") - - _assert(sys.altars_lit_count == 4, "4 altars lit") - - # Light 5th altar -> auto clears stage 5 and enters stage 6 - var res_5th = sys.light_fire_altar() - _assert(res_5th["stage_cleared"] == true, "5th altar lit: stage cleared") - _assert(sys.current_stage == 6, "Automatically advanced to Stage 6") - - var boss_sig := {"fired": false, "vnum": 0} - sys.razador_spawned.connect(func(v: int): - boss_sig["fired"] = true - boss_sig["vnum"] = v - ) - - # Advance stage 6 -> 7 (Boss Chamber) - var res_boss_room = sys.advance_stage() - _assert(res_boss_room["current_stage"] == 7, "Entered Stage 7 (Razador)") - _assert(sys.razador_alive == true, "Razador is alive") - _assert(boss_sig["fired"] == true, "razador_spawned signal emitted") - _assert(boss_sig["vnum"] == 6091, "Razador mob vnum is 6091") - -func _test_razador_boss_slay_and_rewards() -> void: - print("\n--- Test 3: Razador Boss Slay & Rewards ---") - var sys := FlameDungeonRazadorSystem.new() - var inv: Array = [ - {"vnum": FlameDungeonRazadorSystem.VNUM_TICKET_71095, "name": "通行证", "count": 1}, - null, null, null - ] - sys.enter_dungeon(105, inv) - sys.current_stage = 7 - sys.razador_alive = true - - var comp_sig := {"fired": false} - sys.dungeon_completed.connect(func(): - comp_sig["fired"] = true - ) - - # Slay Razador - var res_slay = sys.slay_razador(inv) - _assert(res_slay["ok"] == true, "Razador slain") - _assert(sys.is_active == false, "Dungeon completed and inactive") - _assert(comp_sig["fired"] == true, "dungeon_completed signal emitted") - - # Verify Razador's Chest received - var chest_found := false - for it in inv: - if it != null and int(it.get("vnum", 0)) == FlameDungeonRazadorSystem.VNUM_RAZADOR_CHEST: - chest_found = true - break - _assert(chest_found == true, "Razador's Chest (50186) found in inventory") - -func _test_timeout_failure() -> void: - print("\n--- Test 4: Dungeon Timeout Failure ---") - var sys := FlameDungeonRazadorSystem.new() - var inv: Array = [{"vnum": FlameDungeonRazadorSystem.VNUM_TICKET_71095, "name": "通行证", "count": 1}] - sys.enter_dungeon(100, inv) - - var fail_sig := {"fired": false, "reason": ""} - sys.dungeon_failed.connect(func(r: String): - fail_sig["fired"] = true - fail_sig["reason"] = r - ) - - # Advance 2700s - sys.update(2700.0) - _assert(sys.is_active == false, "Dungeon inactive on timeout") - _assert(fail_sig["fired"] == true, "dungeon_failed signal emitted") - _assert(fail_sig["reason"] == "TIMEOUT", "Reported TIMEOUT") - -func _test_serialization() -> void: - print("\n--- Test 5: Serialization & Restoration ---") - var sys1 := FlameDungeonRazadorSystem.new() - var inv: Array = [{"vnum": FlameDungeonRazadorSystem.VNUM_TICKET_71095, "name": "通行证", "count": 1}] - sys1.enter_dungeon(100, inv) - sys1.current_stage = 5 - sys1.altars_lit_count = 3 - sys1.update(600.0) # 2100s left - - var data = sys1.serialize() - var sys2 := FlameDungeonRazadorSystem.new() - sys2.deserialize(data) - - _assert(sys2.is_active == true, "Restored is_active") - _assert(sys2.current_stage == 5, "Restored current stage 5") - _assert(sys2.altars_lit_count == 3, "Restored altars lit count 3") - _assert(is_equal_approx(sys2.time_remaining, 2100.0), "Restored time remaining 2100s") - -func _finish() -> void: - print("\n=======================================================") - print("Flame Dungeon Razador Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_flame_dungeon_razador_parity.gd.uid b/project/test_flame_dungeon_razador_parity.gd.uid deleted file mode 100644 index fe11b16f..00000000 --- a/project/test_flame_dungeon_razador_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dmjjd2mqvm7kl diff --git a/project/test_game_button_money_dialog_parity.gd b/project/test_game_button_money_dialog_parity.gd deleted file mode 100644 index 2d3b3d08..00000000 --- a/project/test_game_button_money_dialog_parity.gd +++ /dev/null @@ -1,88 +0,0 @@ -# test_game_button_money_dialog_parity.gd -# 检验 40250 浮动任务栏状态红点通知与金币分堆丢弃输入窗 -extends SceneTree - -const GameButtonMoneyDialogSystem = preload("res://game_button_money_dialog_system.gd") - -func _init() -> void: - print("--- 开始测试 40250 浮动任务栏状态红点与金币分堆系统 (test_game_button_money_dialog_parity) ---") - var system = GameButtonMoneyDialogSystem.new() - var fails: int = 0 - - var btns = system.game_buttons - var money = system.money_dialog - - # 1. 浮动任务栏按钮状态检验 (新角色: 0 播放时间,有剩余未分配点数) - btns.check_game_buttons(5, 1, 0) - if not btns.button_visible["STATUS"]: - printerr("FAIL: 有未分配属性点时 STATUS 按钮未显示!") - fails += 1 - if not btns.button_visible["SKILL"]: - printerr("FAIL: 有未分配技能点时 SKILL 按钮未显示!") - fails += 1 - if not btns.button_visible["HELP"]: - printerr("FAIL: 0 游戏时长角色 HELP 按钮未显示!") - fails += 1 - - # 老玩家分配完点数后 (stat=0, skill=0, playtime=3600) - btns.check_game_buttons(0, 0, 3600) - if btns.button_visible["STATUS"] or btns.button_visible["SKILL"] or btns.button_visible["HELP"]: - printerr("FAIL: 属性点分配完毕且为老玩家时按钮应隐藏!") - fails += 1 - - # 2. 观战模式按钮与 /observer_exit 指令 - btns.set_observer_mode(true) - if not btns.button_visible["EXIT_OBSERVER"]: - printerr("FAIL: 观战模式下未显示 EXIT_OBSERVER 按钮!") - fails += 1 - btns.click_exit_observer() - if btns.last_chat_packet_sent != "/observer_exit": - printerr("FAIL: 退出观战发送指令错误: ", btns.last_chat_packet_sent) - fails += 1 - - btns.set_observer_mode(false) - if btns.button_visible["EXIT_OBSERVER"]: - printerr("FAIL: 非观战模式下 EXIT_OBSERVER 按钮未隐藏!") - fails += 1 - - # 3. 金币分堆/丢弃输入框坐标吸附计算测试 - # 屏幕 1024x768,点击位于中间 (500, 400) - # 宽度 170,高度 90 -> pos_x 应为 500 - 85 = 415, pos_y = 400 - 90 - 20 = 290 - money.open_dialog(50000, 100, 500, 400, 1024, 768) - if not money.is_open: - printerr("FAIL: 金币输入框未能打开!") - fails += 1 - if money.pos_x != 415 or money.pos_y != 290: - printerr("FAIL: 金币输入框坐标吸附错误: pos_x=", money.pos_x, " pos_y=", money.pos_y) - fails += 1 - - # 4. 边界屏幕右侧贴边吸附测试 (mouse_x = 1000) - money.open_dialog(50000, 100, 1000, 400, 1024, 768) - if money.pos_x != (1024 - 170): # 854 - printerr("FAIL: 贴右边缘防越界未生效,pos_x=", money.pos_x) - fails += 1 - - # 5. 输入数字与截断测试 (输入 80000,超出最大 50000 -> 截断为 50000) - money.input_text("80000") - var accepted = money.on_accept() - if not accepted or money.last_accepted_value != 50000: - printerr("FAIL: 超额金币未能正确截断至 50000,实际: ", money.last_accepted_value) - fails += 1 - if money.is_open: - printerr("FAIL: 接受后对话框未自动关闭!") - fails += 1 - - # 6. 非法字符过滤测试 - money.open_dialog(1000, 1, 100, 100) - money.input_text("abc12") - var bad_accept = money.on_accept() - if bad_accept or money.last_accepted_value != 0: - printerr("FAIL: 非法非数字输入未能拦截!") - fails += 1 - - if fails == 0: - print("PASS: test_game_button_money_dialog_parity (8/8 全部通过)") - quit(0) - else: - printerr("FAILED: test_game_button_money_dialog_parity (", fails, " 项失败)") - quit(1) diff --git a/project/test_game_button_money_dialog_parity.gd.uid b/project/test_game_button_money_dialog_parity.gd.uid deleted file mode 100644 index b17eceaa..00000000 --- a/project/test_game_button_money_dialog_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dnxlrrwxsebl5 diff --git a/project/test_gm_toolkit_parity.gd b/project/test_gm_toolkit_parity.gd deleted file mode 100644 index 2fecd1c9..00000000 --- a/project/test_gm_toolkit_parity.gd +++ /dev/null @@ -1,143 +0,0 @@ -# test_gm_toolkit_parity.gd —— 40250 单机内置 GM 命令解析控制台 1:1 纯净回归测试套件 -extends SceneTree - -const GmToolkitSystem = preload("res://gm_toolkit_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_gm_toolkit_parity (Direction 4: GM Toolkit Console 1:1) ===") - _test_item_command() - _test_level_and_gold_commands() - _test_set_skill_command() - _test_warp_and_mob_commands() - _test_help_and_unknown_command() - _finish() - -func _test_item_command() -> void: - print("\n--- Test 1: /item Command Execution ---") - var gm := GmToolkitSystem.new() - var inv: Array = [null, null] - var ctx := {"inventory": inv} - - # 1. Spawn Battle Sword +9 (169) - var r1 = gm.execute_command("/item 169 1", ctx) - _assert(r1["ok"] == true, "/item 169 1 succeeded") - _assert(inv[0] != null and inv[0]["vnum"] == 169, "Battle Sword spawned in slot 0") - - # 2. Spawn 5x Soul Stones (50513) - var r2 = gm.execute_command("/item 50513 5", ctx) - _assert(r2["ok"] == true, "/item 50513 5 succeeded") - _assert(inv[1] != null and inv[1]["vnum"] == 50513 and inv[1]["count"] == 5, "5x Soul Stones spawned in slot 1") - - # 3. Full inventory - var r3 = gm.execute_command("/item 10 1", ctx) - _assert(r3["ok"] == false and r3["reason"] == "INVENTORY_FULL", "Blocked when inventory full") - - # 4. Syntax error - var r4 = gm.execute_command("/item", ctx) - _assert(r4["ok"] == false and r4["reason"] == "SYNTAX_ERROR", "Syntax error caught on empty args") - -func _test_level_and_gold_commands() -> void: - print("\n--- Test 2: /level and /gold Commands ---") - var gm := GmToolkitSystem.new() - var pdata := {"level": 1, "gold": 0} - var ctx := {"player_data": pdata} - - # /level 75 - var r_lv = gm.execute_command("/level 75", ctx) - _assert(r_lv["ok"] == true and pdata["level"] == 75, "Level set to 75") - - # /level 200 (clamped to 105) - gm.execute_command("/level 200", ctx) - _assert(pdata["level"] == 105, "Level clamped to max 105") - - # /gold 1000000 - var r_gold = gm.execute_command("/gold 1000000", ctx) - _assert(r_gold["ok"] == true and pdata["gold"] == 1000000, "1,000,000 Yang injected") - -func _test_set_skill_command() -> void: - print("\n--- Test 3: /set_skill Command (M1, G1, P grades) ---") - var gm := GmToolkitSystem.new() - var skills := {} - var ctx := {"skills": skills} - - # Set skill 1 to M1 (Lv 20) - var r_m1 = gm.execute_command("/set_skill 1 20", ctx) - _assert(r_m1["ok"] == true and skills[1] == 20, "Skill 1 set to 20") - _assert(r_m1["grade"].begins_with("M级"), "Grade is M") - - # Set skill 1 to P (Lv 40) - var r_p = gm.execute_command("/set_skill 1 40", ctx) - _assert(r_p["ok"] == true and skills[1] == 40, "Skill 1 set to 40 (P-grade)") - _assert(r_p["grade"].begins_with("P级"), "Grade is P") - -func _test_warp_and_mob_commands() -> void: - print("\n--- Test 4: /warp, /map, and /spawn_mob Commands ---") - var gm := GmToolkitSystem.new() - var ctx := {"player_pos": Vector3(10.0, 0.0, 20.0)} - - var warp_signal := {"fired": false, "pos": Vector3.ZERO, "map": ""} - gm.player_warped.connect(func(pos: Vector3, mname: String): - warp_signal["fired"] = true - warp_signal["pos"] = pos - warp_signal["map"] = mname - ) - - # /map sohan - var r_map = gm.execute_command("/map sohan", ctx) - _assert(r_map["ok"] == true, "/map sohan succeeded") - _assert(warp_signal["fired"] == true and warp_signal["map"] == "sohan", "Warped to sohan preset") - _assert(warp_signal["pos"] == Vector3(400, 0, 600), "Sohan coords match (400, 0, 600)") - - # /warp 150 250 - var r_warp = gm.execute_command("/warp 150 250", ctx) - _assert(r_warp["ok"] == true, "/warp 150 250 succeeded") - _assert(warp_signal["pos"] == Vector3(150, 0, 250), "Custom warp coords match") - - # /spawn_mob 2493 1 (Beran-Setaou) - var mob_signal := {"fired": false, "vnum": 0, "count": 0} - gm.mob_spawned.connect(func(v: int, c: int, _p: Vector3): - mob_signal["fired"] = true - mob_signal["vnum"] = v - mob_signal["count"] = c - ) - - var r_mob = gm.execute_command("/spawn_mob 2493 1", ctx) - _assert(r_mob["ok"] == true, "/spawn_mob 2493 1 succeeded") - _assert(mob_signal["fired"] == true and mob_signal["vnum"] == 2493 and mob_signal["count"] == 1, "mob_spawned signal fired for Beran-Setaou") - -func _test_help_and_unknown_command() -> void: - print("\n--- Test 5: /help and Error Handling ---") - var gm := GmToolkitSystem.new() - var ctx := {} - - # /help - var r_help = gm.execute_command("/help", ctx) - _assert(r_help["ok"] == true and r_help["commands"].size() >= 5, "/help returned command list") - - # Not a command - var r_not = gm.execute_command("hello", ctx) - _assert(r_not["ok"] == false and r_not["reason"] == "NOT_A_COMMAND", "Plain string rejected as non-command") - - # Unknown command - var r_unk = gm.execute_command("/fly", ctx) - _assert(r_unk["ok"] == false and r_unk["reason"] == "UNKNOWN_COMMAND", "Unknown command rejected") - -func _finish() -> void: - print("\n==========================================") - print("GM Toolkit Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_gm_toolkit_parity.gd.uid b/project/test_gm_toolkit_parity.gd.uid deleted file mode 100644 index 8e487959..00000000 --- a/project/test_gm_toolkit_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c4yfwa1exjhj diff --git a/project/test_guild_mark_uploader_parity.gd b/project/test_guild_mark_uploader_parity.gd deleted file mode 100644 index 164f0f17..00000000 --- a/project/test_guild_mark_uploader_parity.gd +++ /dev/null @@ -1,84 +0,0 @@ -# test_guild_mark_uploader_parity.gd —— 40250 行会图标与战旗上传回归测试 -extends SceneTree - -const SystemClass = preload("res://guild_mark_uploader_system.gd") - -var _passed: int = 0 -var _failed: int = 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_guild_mark_uploader_parity (Batch 30 Direction 1) ===") - _test_authority_and_format_validation() - _test_mark_dimension_checks() - _test_symbol_dimension_checks() - _test_successful_mark_and_symbol_upload() - - print("\n=== Result: %d Passed, %d Failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - else: - printerr("SOME TESTS FAILED!\n") - quit(0 if _failed == 0 else 1) - -func _test_authority_and_format_validation() -> void: - print("\n--- Test 1: Authority & Format Verification ---") - var up = SystemClass.new() - - # 1. Non-guild master rejected - var r_auth = up.upload_guild_mark(101, false, 16, 12, "bmp") - _assert(r_auth["error_code"] == SystemClass.UploadError.ERROR_NOT_GUILD_MASTER, "Reject upload if not guild master") - - # 2. Unsupported format (PNG/JPG) - var r_fmt = up.upload_guild_mark(101, true, 16, 12, "png") - _assert(r_fmt["error_code"] == SystemClass.UploadError.ERROR_UNSUPPORTED_FORMAT, "Reject non-BMP formats") - -func _test_mark_dimension_checks() -> void: - print("\n--- Test 2: Mark (16x12) Dimensions Checking ---") - var up = SystemClass.new() - - # Wrong width (20 != 16) - var r_w = up.upload_guild_mark(101, true, 20, 12, "bmp") - _assert(r_w["error_code"] == SystemClass.UploadError.ERROR_WIDTH, "Reject mark width != 16") - - # Wrong height (16 != 12) - var r_h = up.upload_guild_mark(101, true, 16, 16, "bmp") - _assert(r_h["error_code"] == SystemClass.UploadError.ERROR_HEIGHT, "Reject mark height != 12") - -func _test_symbol_dimension_checks() -> void: - print("\n--- Test 3: Symbol (64x128) Dimensions Checking ---") - var up = SystemClass.new() - - # Wrong symbol width (32 != 64) - var r_w = up.upload_guild_symbol(101, true, 32, 128, "bmp") - _assert(r_w["error_code"] == SystemClass.UploadError.ERROR_WIDTH, "Reject symbol width != 64") - - # Wrong symbol height (64 != 128) - var r_h = up.upload_guild_symbol(101, true, 64, 64, "bmp") - _assert(r_h["error_code"] == SystemClass.UploadError.ERROR_HEIGHT, "Reject symbol height != 128") - -func _test_successful_mark_and_symbol_upload() -> void: - print("\n--- Test 4: Successful Upload & Cache Retrieval ---") - var up = SystemClass.new() - var dummy_pixels = PackedByteArray([0xFF, 0x00, 0xFF, 0x00]) - - # Upload 16x12 Mark - var r_mark = up.upload_guild_mark(101, true, 16, 12, "bmp", dummy_pixels) - _assert(r_mark["success"] == true, "Uploaded 16x12 mark successfully") - var m = up.get_guild_mark(101) - _assert(m != null, "Mark cached in manager") - _assert(m.width == 16 and m.height == 12, "Cached mark dimensions match 16x12") - - # Upload 64x128 Symbol - var r_sym = up.upload_guild_symbol(101, true, 64, 128, "bmp", dummy_pixels) - _assert(r_sym["success"] == true, "Uploaded 64x128 symbol successfully") - var s = up.get_guild_symbol(101) - _assert(s != null, "Symbol cached in manager") - _assert(s.width == 64 and s.height == 128, "Cached symbol dimensions match 64x128") diff --git a/project/test_guild_mark_uploader_parity.gd.uid b/project/test_guild_mark_uploader_parity.gd.uid deleted file mode 100644 index fe45bf5a..00000000 --- a/project/test_guild_mark_uploader_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dj0fgymtxpp2v diff --git a/project/test_guild_skill_parity.gd b/project/test_guild_skill_parity.gd deleted file mode 100644 index afd66004..00000000 --- a/project/test_guild_skill_parity.gd +++ /dev/null @@ -1,125 +0,0 @@ -# test_guild_skill_parity.gd —— 40250 公会等级与 7 大神龙技能 1:1 纯净回归测试套件 -extends SceneTree - -const GuildSkillSystem = preload("res://guild_skill_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_guild_skill_parity (Direction 3: Guild Skills 1:1) ===") - _test_guild_leveling_and_points() - _test_skill_leveling_and_passive_spirit() - _test_skill_casting_and_formulas() - _test_cooldowns_and_buff_decay() - _finish() - -func _test_guild_leveling_and_points() -> void: - print("\n--- Test 1: Guild EXP Donation & Leveling ---") - var gss := GuildSkillSystem.new() - _assert(gss.guild_level == 1, "Initial guild level is 1") - _assert(gss.skill_points == 0, "Initial skill points is 0") - - # Donate EXP to level up to 2 (needed: 50,000) - var res = gss.donate_exp(50000) - _assert(res["ok"] == true and res["leveled_up"] == true, "Leveled up successfully") - _assert(gss.guild_level == 2, "Guild level is now 2") - _assert(gss.skill_points == 1, "Awarded 1 skill point") - - # Donate huge amount to level up to 10 - gss.donate_exp(5000000) - _assert(gss.guild_level > 2, "Guild level increased with large EXP donation") - _assert(gss.skill_points > 1, "Multiple skill points accumulated") - -func _test_skill_leveling_and_passive_spirit() -> void: - print("\n--- Test 2: Skill Leveling & Dragon Eyes (151) Passive ---") - var gss := GuildSkillSystem.new() - gss.skill_points = 10 # Provide test points - - # Level up 151 (Dragon Eyes) to 7 (Max) - for i in range(7): - var res = gss.level_up_skill(GuildSkillSystem.SKILL_DRAGON_EYES) - _assert(res["ok"] == true, "Dragon Eyes upgraded to level %d" % (i + 1)) - - _assert(gss.skill_levels[GuildSkillSystem.SKILL_DRAGON_EYES] == 7, "Dragon Eyes is level 7 (Max)") - _assert(gss.max_spirit == 200, "Max Dragon Spirit expanded from 100 to 200") - - # 8th level attempt rejected - var res = gss.level_up_skill(GuildSkillSystem.SKILL_DRAGON_EYES) - _assert(res["ok"] == false and res["reason"] == "MAX_SKILL_LEVEL", "Cannot exceed max skill level 7") - -func _test_skill_casting_and_formulas() -> void: - print("\n--- Test 3: Skill Casting & 40250 Formula Scaling ---") - var gss := GuildSkillSystem.new() - gss.skill_points = 21 - - # Max out 151 (Dragon Eyes), 152 (Dragon Blood) and 156 (Dragon Rage) - for i in range(7): - gss.level_up_skill(GuildSkillSystem.SKILL_DRAGON_EYES) - gss.level_up_skill(GuildSkillSystem.SKILL_DRAGON_BLOOD) - gss.level_up_skill(GuildSkillSystem.SKILL_DRAGON_RAGE) - gss.current_spirit = 200 - - # Cannot cast unlearned skill (154 Holy Armor) - var res = gss.cast_guild_skill(GuildSkillSystem.SKILL_HOLY_ARMOR) - _assert(res["ok"] == false and res["reason"] == "NOT_LEARNED", "Cannot cast unlearned skill") - - # Cannot cast passive skill (151) - res = gss.cast_guild_skill(GuildSkillSystem.SKILL_DRAGON_EYES) - _assert(res["ok"] == false and res["reason"] == "PASSIVE_SKILL", "Cannot cast passive skill") - - # Cast 152 Dragon Blood (Level 7 -> k=1.0 -> Max HP +20%) - res = gss.cast_guild_skill(GuildSkillSystem.SKILL_DRAGON_BLOOD) - _assert(res["ok"] == true, "Dragon Blood cast successfully") - _assert(is_equal_approx(float(res["value"]), 20.0), "Level 7 Dragon Blood gives +20% Max HP") - _assert(gss.current_spirit == 180, "Spirit deducted: 200 - 20 = 180") - - # Cast 156 Dragon Rage (Level 7 -> k=1.0 -> Crit +50%) - res = gss.cast_guild_skill(GuildSkillSystem.SKILL_DRAGON_RAGE) - _assert(res["ok"] == true, "Dragon Rage cast successfully") - _assert(is_equal_approx(float(res["value"]), 50.0), "Level 7 Dragon Rage gives +50% Critical") - - var bonuses = gss.get_active_bonuses() - _assert(is_equal_approx(float(bonuses["max_hp_pct"]), 20.0), "Active bonus has +20% HP") - _assert(is_equal_approx(float(bonuses["critical_pct"]), 50.0), "Active bonus has +50% Crit") - -func _test_cooldowns_and_buff_decay() -> void: - print("\n--- Test 4: Cooldown Enforcement & Buff Decay ---") - var gss := GuildSkillSystem.new() - gss.skill_points = 7 - for i in range(7): - gss.level_up_skill(GuildSkillSystem.SKILL_DRAGON_BLOOD) - - # 1st cast enters cooldown - gss.cast_guild_skill(GuildSkillSystem.SKILL_DRAGON_BLOOD) - _assert(gss.skill_cooldowns.has(GuildSkillSystem.SKILL_DRAGON_BLOOD), "Skill is in cooldown") - - # 2nd cast blocked by cooldown - var res = gss.cast_guild_skill(GuildSkillSystem.SKILL_DRAGON_BLOOD) - _assert(res["ok"] == false and res["reason"] == "COOLDOWN_ACTIVE", "Recasting blocked by cooldown") - - # Fast forward 61 seconds (cooldown is 60s, buff is 300s) - gss.update(61.0) - _assert(gss.skill_cooldowns.has(GuildSkillSystem.SKILL_DRAGON_BLOOD) == false, "Cooldown expired after 61s") - _assert(gss.active_buffs.has(GuildSkillSystem.SKILL_DRAGON_BLOOD) == true, "Buff remains active after 61s") - - # Fast forward remaining 240 seconds -> buff expires - gss.update(240.0) - _assert(gss.active_buffs.has(GuildSkillSystem.SKILL_DRAGON_BLOOD) == false, "Buff expired after 300s total") - -func _finish() -> void: - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - quit(0) - else: - printerr("SOME TESTS FAILED!\n") - quit(1) diff --git a/project/test_guild_skill_parity.gd.uid b/project/test_guild_skill_parity.gd.uid deleted file mode 100644 index 52ff765c..00000000 --- a/project/test_guild_skill_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c25o0whbg064b diff --git a/project/test_guild_war_observer_arena_parity.gd b/project/test_guild_war_observer_arena_parity.gd deleted file mode 100644 index 9c5d45ff..00000000 --- a/project/test_guild_war_observer_arena_parity.gd +++ /dev/null @@ -1,112 +0,0 @@ -# test_guild_war_observer_arena_parity.gd —— Metin2 40250 公会战备观战与武斗擂台传送 1:1 测试套件 -extends SceneTree - -const GuildWarObserverArenaSystem = preload("res://guild_war_observer_arena_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_guild_war_observer_arena_parity (Direction 4: Guild War Observer 1:1) ===") - - test_active_wars_and_display() - test_observer_entry_and_ghost_mode() - test_manual_leave_observer_mode() - test_war_ending_auto_evacuation() - test_serialization() - - print("\n=======================================================") - print("Guild War Observer Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - - if _failed > 0: - quit(1) - else: - quit(0) - -func test_active_wars_and_display() -> void: - print("\n--- Test 1: Active Wars Registration & Town Guard List (40250 1:1) ---") - var system = GuildWarObserverArenaSystem.new() - - var wid = system.register_active_war(101, "王者之师", 202, "龙城铁骑", 1, 50) - _assert(wid > 0, "Registered active war") - - var list = system.get_active_wars_for_display() - _assert(list.size() == 1, "Listed 1 active war for guard display") - _assert(list[0]["guild1_name"] == "王者之师", "Guild 1 name correct") - _assert(list[0]["guild2_name"] == "龙城铁骑", "Guild 2 name correct") - _assert(list[0]["map_index"] == GuildWarObserverArenaSystem.MAP_GUILD_ARENA, "Map index is 110 (Arena)") - -func test_observer_entry_and_ghost_mode() -> void: - print("\n--- Test 2: Observer Entry & Ghost Mode (40250 1:1) ---") - var system = GuildWarObserverArenaSystem.new() - var wid = system.register_active_war(101, "王者之师", 202, "龙城铁骑", 1, 50) - - var orig_pos = Vector2(800.0, 900.0) - var res = system.enter_as_observer(999, wid, "主城辛秀", orig_pos) - _assert(res["ok"] == true, "Entered observer mode") - _assert(res["observer_pos"] == GuildWarObserverArenaSystem.OBSERVER_PLATFORM_POS, "Warps to high platform") - _assert(system.is_player_observing(999) == true, "Player 999 is in observer state") - # 幽灵模式免疫战斗与禁止出招 - _assert(system.can_player_attack_or_be_attacked(999) == false, "Player cannot attack or be attacked in observer mode") - -func test_manual_leave_observer_mode() -> void: - print("\n--- Test 3: Manual Leave Observer Mode (40250 1:1) ---") - var system = GuildWarObserverArenaSystem.new() - var wid = system.register_active_war(101, "王者之师", 202, "龙城铁骑", 1, 50) - - var orig_pos = Vector2(1200.0, 1500.0) - system.enter_as_observer(999, wid, "主城辛秀", orig_pos) - - # 主动离开 - var leave_res = system.leave_observer_mode(999) - _assert(leave_res["ok"] == true, "Left observer mode") - _assert(leave_res["return_map"] == "主城辛秀", "Returned to previous map") - _assert(leave_res["return_pos"] == orig_pos, "Returned to previous coordinates") - _assert(system.is_player_observing(999) == false, "No longer observer") - _assert(system.can_player_attack_or_be_attacked(999) == true, "Combat privileges restored") - -func test_war_ending_auto_evacuation() -> void: - print("\n--- Test 4: War Ending Auto Evacuation (40250 1:1) ---") - var system = GuildWarObserverArenaSystem.new() - var wid = system.register_active_war(101, "王者之师", 202, "龙城铁骑", 1, 50) - - var left_signals := [] - system.observer_left.connect(func(pid, map, pos): - left_signals.append({"pid": pid, "map": map, "pos": pos}) - ) - - # 2 位观战者入场 - system.enter_as_observer(801, wid, "主城辛秀", Vector2(100.0, 100.0)) - system.enter_as_observer(802, wid, "主城镇奴", Vector2(200.0, 200.0)) - - # 公会 1 战胜达成 50 分,终局自动结算 - system.update_war_score(wid, 50, 30) - - # 检查全部观战者被安全自动遣散 - _assert(left_signals.size() == 2, "Both observers evacuated") - _assert(system.is_player_observing(801) == false, "Observer 801 cleared") - _assert(system.is_player_observing(802) == false, "Observer 802 cleared") - _assert(system.get_active_wars_for_display().is_empty(), "War is marked ended and cleared from active list") - -func test_serialization() -> void: - print("\n--- Test 5: Serialization & Restoration ---") - var system = GuildWarObserverArenaSystem.new() - var wid = system.register_active_war(101, "公会A", 202, "公会B") - system.enter_as_observer(555, wid, "主城", Vector2(300.0, 400.0)) - - var data = system.serialize() - _assert(data.has("observers") and data["observers"].has(555), "Serialized observer state") - - var restored = GuildWarObserverArenaSystem.new() - restored.deserialize(data) - _assert(restored.is_player_observing(555) == true, "Restored observer state") - _assert(restored.can_player_attack_or_be_attacked(555) == false, "Restored combat immunity") diff --git a/project/test_guild_war_observer_arena_parity.gd.uid b/project/test_guild_war_observer_arena_parity.gd.uid deleted file mode 100644 index 3975dada..00000000 --- a/project/test_guild_war_observer_arena_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://yhu62x6s5yrx diff --git a/project/test_guild_war_parity.gd b/project/test_guild_war_parity.gd deleted file mode 100644 index 375f9d63..00000000 --- a/project/test_guild_war_parity.gd +++ /dev/null @@ -1,137 +0,0 @@ -# test_guild_war_parity.gd —— 40250 官方公会战备宣战与积分决斗 1:1 回归测试套件 -extends SceneTree - -const GuildWarSystem = preload("res://guild_war_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_guild_war_parity (Direction 3: Guild War 1:1) ===") - _test_war_declaration_and_start() - _test_score_tracking_and_target_win() - _test_war_timeout_resolution() - _test_serialization() - _finish() - -func _test_war_declaration_and_start() -> void: - print("\n--- Test 1: War Declaration & State Machine ---") - var sys := GuildWarSystem.new() - _assert(sys.war_state == GuildWarSystem.STATE_NONE, "Initially not at war") - - var dec_sig := {"fired": false, "opp": "", "score": 0} - sys.war_declared.connect(func(opp: String, _wt: int, s: int): - dec_sig["fired"] = true - dec_sig["opp"] = opp - dec_sig["score"] = s - ) - - # 1. Declare war against Shadow Assassins (101) - var dec_res = sys.declare_war(101, GuildWarSystem.WAR_TYPE_ARENA, 50, 1800.0) - _assert(dec_res["ok"] == true, "War declared") - _assert(sys.war_state == GuildWarSystem.STATE_DECLARED, "State is STATE_DECLARED") - _assert(dec_sig["fired"] == true, "war_declared signal emitted") - _assert(dec_sig["opp"] == "暗影刺客团", "Opponent is 暗影刺客团") - - # Cannot declare war while already declared - var dec_again = sys.declare_war(102) - _assert(dec_again["ok"] == true, "Replaced declaration before start") - - var start_sig := {"fired": false, "dur": 0.0} - sys.war_started.connect(func(_wt: int, d: float): - start_sig["fired"] = true - start_sig["dur"] = d - ) - - # 2. Start war - var start_res = sys.start_war() - _assert(start_res["ok"] == true, "War started") - _assert(sys.war_state == GuildWarSystem.STATE_FIGHTING, "State is STATE_FIGHTING") - _assert(start_sig["fired"] == true, "war_started signal emitted") - -func _test_score_tracking_and_target_win() -> void: - print("\n--- Test 2: Score Tracking & Target Score (50 Points) Victory ---") - var sys := GuildWarSystem.new() - sys.declare_war(102, GuildWarSystem.WAR_TYPE_ARENA, 50, 1800.0) - sys.start_war() - - var score_sig := {"fired": false, "sa": 0, "sb": 0} - sys.score_updated.connect(func(sa: int, sb: int): - score_sig["fired"] = true - score_sig["sa"] = sa - score_sig["sb"] = sb - ) - - var end_sig := {"fired": false, "winner": "", "reward": {}} - sys.war_ended.connect(func(w: String, _sa: int, _sb: int, rew: Dictionary): - end_sig["fired"] = true - end_sig["winner"] = w - end_sig["reward"] = rew - ) - - # Score increments - sys.record_score("A", 10) - sys.record_score("B", 5) - _assert(sys.score_a == 10 and sys.score_b == 5, "Score is 10 - 5") - _assert(score_sig["fired"] == true, "score_updated signal emitted") - - # Side A scores 40 more to hit 50 points (target score) - var win_res = sys.record_score("A", 40) - _assert(win_res["winner"] == "A", "Side A declared winner") - _assert(sys.war_state == GuildWarSystem.STATE_FINISHED, "State is STATE_FINISHED") - _assert(sys.player_guild_ladder == 1030, "Ladder points +30 (1000 -> 1030)") - _assert(sys.total_wars_won == 1, "Total wars won is 1") - _assert(end_sig["fired"] == true, "war_ended signal emitted") - _assert(end_sig["reward"]["guild_gold"] == 1000000, "Received 1,000,000 guild gold") - -func _test_war_timeout_resolution() -> void: - print("\n--- Test 3: War Timeout & Decisive Score Ruling ---") - var sys := GuildWarSystem.new() - sys.declare_war(103, GuildWarSystem.WAR_TYPE_FIELD, 100, 1800.0) - sys.start_war() - - sys.record_score("A", 25) - sys.record_score("B", 18) - - # Advance 1800s to timeout - sys.update(1800.0) - _assert(sys.war_state == GuildWarSystem.STATE_FINISHED, "War finished on timeout") - _assert(sys.score_a == 25 and sys.score_b == 18, "Final score is 25 - 18") - _assert(sys.player_guild_ladder == 1030, "Player won on higher score (+30 ladder)") - -func _test_serialization() -> void: - print("\n--- Test 4: Serialization & Restoration ---") - var sys1 := GuildWarSystem.new() - sys1.player_guild_ladder = 1450 - sys1.total_wars_won = 15 - sys1.declare_war(101, GuildWarSystem.WAR_TYPE_FLAG, 50, 1500.0) - sys1.start_war() - sys1.record_score("A", 30) - sys1.record_score("B", 20) - - var data = sys1.serialize() - var sys2 := GuildWarSystem.new() - sys2.deserialize(data) - - _assert(sys2.war_state == GuildWarSystem.STATE_FIGHTING, "Restored state FIGHTING") - _assert(sys2.active_war_type == GuildWarSystem.WAR_TYPE_FLAG, "Restored war type FLAG") - _assert(sys2.score_a == 30 and sys2.score_b == 20, "Restored scores") - _assert(sys2.player_guild_ladder == 1450, "Restored ladder rating 1450") - _assert(sys2.total_wars_won == 15, "Restored total wars won 15") - -func _finish() -> void: - print("\n=======================================================") - print("Guild War System Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_guild_war_parity.gd.uid b/project/test_guild_war_parity.gd.uid deleted file mode 100644 index aeeec519..00000000 --- a/project/test_guild_war_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dcmj1n5dx6ixg diff --git a/project/test_hair_dye_parity.gd b/project/test_hair_dye_parity.gd deleted file mode 100644 index 67e9c27e..00000000 --- a/project/test_hair_dye_parity.gd +++ /dev/null @@ -1,140 +0,0 @@ -# test_hair_dye_parity.gd —— 40250 发型染色与漂白系统 1:1 回归测试套件 -extends SceneTree - -const HairDyeSystem = preload("res://hair_dye_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_hair_dye_parity (Direction 3: Hair Dye System 1:1) ===") - _test_hair_dyeing_and_color_mapping() - _test_level_cooldown_restriction() - _test_bleach_clears_cooldown_and_resets() - _test_costume_hair_rejection() - _test_serialization() - _finish() - -func _test_hair_dyeing_and_color_mapping() -> void: - print("\n--- Test 1: Hair Dyeing & Color Mapping (40250 1:1) ---") - var sys := HairDyeSystem.new() - _assert(sys.current_hair_color == 0, "Default hair color is 0 (Default)") - - var inv: Array = [ - {"vnum": HairDyeSystem.VNUM_BLONDE_DYE, "name": "染发剂(金)", "count": 1} - ] - - var dye_sig := {"fired": false, "cid": 0, "cname": ""} - sys.hair_dyed.connect(func(cid: int, cn: String): - dye_sig["fired"] = true - dye_sig["cid"] = cid - dye_sig["cname"] = cn - ) - - # Dye Blonde at Lv 30 - var res_dye = sys.dye_hair(inv, 0, 30) - _assert(res_dye["ok"] == true, "Dyeing succeeded") - _assert(sys.current_hair_color == 2, "Color is 2 (Blonde)") - _assert(sys.last_dye_level == 30, "Last dye level recorded as 30") - _assert(inv[0] == null, "Dye consumed from slot 0") - _assert(dye_sig["fired"] == true, "hair_dyed signal emitted") - _assert(dye_sig["cid"] == 2, "Reported color ID 2") - -func _test_level_cooldown_restriction() -> void: - print("\n--- Test 2: Level Interval Cooldown Rule (last_dye_level + 3 <= Level) ---") - var sys := HairDyeSystem.new() - sys.current_hair_color = 2 - sys.last_dye_level = 30 # Dyed at Lv 30 - - var inv: Array = [ - {"vnum": HairDyeSystem.VNUM_RED_DYE, "name": "染发剂(红)", "count": 2} - ] - - # 1. Attempt at Lv 31 (< 33) -> rejected - var res_31 = sys.dye_hair(inv, 0, 31) - _assert(res_31["ok"] == false, "Rejected at Lv 31 (< 33)") - _assert(res_31["reason"] == "LEVEL_COOLDOWN_ACTIVE", "Reason is LEVEL_COOLDOWN_ACTIVE") - _assert(res_31["req_level"] == 33, "Required level is 33") - - # 2. Attempt at Lv 32 (< 33) -> rejected - var res_32 = sys.dye_hair(inv, 0, 32) - _assert(res_32["ok"] == false, "Rejected at Lv 32 (< 33)") - - # 3. Attempt at Lv 33 (>= 33) -> success! - var res_33 = sys.dye_hair(inv, 0, 33) - _assert(res_33["ok"] == true, "Succeeded at Lv 33 (30 + 3)") - _assert(sys.current_hair_color == 3, "Color is now 3 (Red)") - _assert(sys.last_dye_level == 33, "New last_dye_level is 33") - -func _test_bleach_clears_cooldown_and_resets() -> void: - print("\n--- Test 3: Bleach (70201) Bypasses Cooldown & Resets Color ---") - var sys := HairDyeSystem.new() - sys.current_hair_color = 3 # Red - sys.last_dye_level = 33 # Normally requires Lv 36 for next dye - - var inv: Array = [ - {"vnum": HairDyeSystem.VNUM_BLEACH, "name": "漂白水", "count": 1}, - {"vnum": HairDyeSystem.VNUM_WHITE_DYE, "name": "染发剂(白)", "count": 1} - ] - - var bleach_sig := {"fired": false} - sys.hair_bleached.connect(func(): - bleach_sig["fired"] = true - ) - - # Use Bleach at Lv 33 (same level) -> succeeds unconditionally - var res_bleach = sys.dye_hair(inv, 0, 33) - _assert(res_bleach["ok"] == true, "Bleach used successfully at same level") - _assert(sys.current_hair_color == 0, "Color reset to 0 (Default)") - _assert(sys.last_dye_level == 0, "Cooldown level reset to 0") - _assert(bleach_sig["fired"] == true, "hair_bleached signal emitted") - - # Immediately dye White at Lv 33 without cooldown! - var res_white = sys.dye_hair(inv, 1, 33) - _assert(res_white["ok"] == true, "White dye used immediately after bleach") - _assert(sys.current_hair_color == 1, "Color is now 1 (White)") - -func _test_costume_hair_rejection() -> void: - print("\n--- Test 4: Costume Hair (PART_HAIR >= 1001) Rejection ---") - var sys := HairDyeSystem.new() - sys.current_hair_style_id = 1005 # Equipped costume hair - - var inv: Array = [ - {"vnum": HairDyeSystem.VNUM_BLACK_DYE, "name": "染发剂(黑)", "count": 1} - ] - - var res_costume = sys.dye_hair(inv, 0, 50) - _assert(res_costume["ok"] == false, "Costume hair dyeing rejected") - _assert(res_costume["reason"] == "CANNOT_DYE_COSTUME_HAIR", "Reason is CANNOT_DYE_COSTUME_HAIR") - _assert(inv[0] != null, "Dye not consumed") - -func _test_serialization() -> void: - print("\n--- Test 5: Serialization & Restoration ---") - var sys1 := HairDyeSystem.new() - sys1.current_hair_color = 4 # Brown - sys1.last_dye_level = 65 - sys1.current_hair_style_id = 0 - - var data = sys1.serialize() - var sys2 := HairDyeSystem.new() - sys2.deserialize(data) - - _assert(sys2.current_hair_color == 4, "Restored color 4 (Brown)") - _assert(sys2.last_dye_level == 65, "Restored last dye level 65") - -func _finish() -> void: - print("\n=======================================================") - print("Hair Dye System Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_hair_dye_parity.gd.uid b/project/test_hair_dye_parity.gd.uid deleted file mode 100644 index 2366e1c8..00000000 --- a/project/test_hair_dye_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://o1jwdr4spxqo diff --git a/project/test_herbal_dew_parity.gd b/project/test_herbal_dew_parity.gd deleted file mode 100644 index 7ecb7f8b..00000000 --- a/project/test_herbal_dew_parity.gd +++ /dev/null @@ -1,113 +0,0 @@ -# test_herbal_dew_parity.gd —— 40250 草药炼丹与 6 色神圣露水 1:1 纯净回归测试套件 -extends SceneTree - -const HerbalDewSystem = preload("res://herbal_dew_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_herbal_dew_parity (Direction 2: Herbal Dews 1:1) ===") - _test_herb_to_water_extraction() - _test_dew_brewing_with_bottle() - _test_six_color_dew_drinking_and_stacking() - _test_dew_decay_and_expiration() - _finish() - -func _test_herb_to_water_extraction() -> void: - print("\n--- Test 1: Herb to Water Intermediate Extraction ---") - var inv: Array = [ - {"vnum": 50701, "count": 1}, # Peach Blossom - null, - null - ] - - # Extract Peach Blossom -> Sim-Water (50721) - var res = HerbalDewSystem.brew_water(inv, 50701) - _assert(res["ok"] == true, "Peach Blossom extracted successfully") - _assert(res["water_vnum"] == 50721, "Yielded Sim-Water (50721)") - _assert(inv[res["slot"]]["vnum"] == 50721, "Sim-Water stored in slot %d" % res["slot"]) - - # Missing herb fails - var empty_inv: Array = [null, null] - res = HerbalDewSystem.brew_water(empty_inv, 50701) - _assert(res["ok"] == false and res["reason"] == "MISSING_HERB", "Extraction fails without herb") - -func _test_dew_brewing_with_bottle() -> void: - print("\n--- Test 2: Dew Brewing with Empty Bottle (70030) ---") - var inv: Array = [ - {"vnum": 50723}, # Bo-Water (for Blue Dew 50823) - {"vnum": 70030}, # Empty Bottle - null - ] - - # Brew Blue Dew (50823) - var res = HerbalDewSystem.brew_dew(inv, 50823, true) - _assert(res["ok"] == true, "Blue Dew brewed successfully") - _assert(res["dew_vnum"] == 50823, "Vnum is 50823 (Blue Dew)") - _assert(res["value"] == 120, "Rolled max ATK +120") - _assert(inv[res["slot"] if res.has("slot") else 0]["vnum"] == 50823, "Blue Dew stored in inventory") - - # Missing bottle fails - var inv_no_bottle: Array = [{"vnum": 50723}, null] - res = HerbalDewSystem.brew_dew(inv_no_bottle, 50823) - _assert(res["ok"] == false and res["reason"] == "MISSING_BOTTLE", "Brewing fails without empty bottle") - -func _test_six_color_dew_drinking_and_stacking() -> void: - print("\n--- Test 3: Drinking Dews & Multi-Dew Stacking ---") - var hds := HerbalDewSystem.new() - - var blue_dew := {"vnum": 50823, "value": 100} # +100 ATK - var red_dew := {"vnum": 50822, "value": 15} # +15% Crit - var white_dew := {"vnum": 50824, "value": 80} # +80 DEF - - # Drink Blue Dew - var res = hds.drink_dew(blue_dew) - _assert(res["ok"] == true, "Blue Dew consumed") - _assert(hds.has_dew(50823) == true, "Blue Dew active") - _assert(hds.get_stat_bonus("att_grade") == 100, "ATK bonus is 100") - - # Drink Red Dew (stack together) - hds.drink_dew(red_dew) - _assert(hds.has_dew(50822) == true, "Red Dew active") - _assert(hds.get_stat_bonus("critical_pct") == 15, "Critical bonus is 15%") - _assert(hds.get_stat_bonus("att_grade") == 100, "ATK bonus remains active alongside Red Dew") - - # Drink White Dew - hds.drink_dew(white_dew) - _assert(hds.get_stat_bonus("def_grade") == 80, "DEF bonus is 80") - _assert(hds.active_dews.size() == 3, "3 distinct dews simultaneously active") - -func _test_dew_decay_and_expiration() -> void: - print("\n--- Test 4: Dew Duration Decay & Expiration ---") - var hds := HerbalDewSystem.new() - hds.drink_dew({"vnum": 50826, "value": 5}) # Yellow Dew (Attack Speed) - _assert(hds.has_dew(50826) == true, "Yellow Dew active") - - # Fast forward 500s -> still active - var expired = hds.update(500.0) - _assert(expired.is_empty(), "Not expired after 500s") - _assert(hds.has_dew(50826) == true, "Still active at 100s remaining") - - # Fast forward another 101s -> expired - expired = hds.update(101.0) - _assert(expired == [50826], "Yellow Dew expired") - _assert(hds.has_dew(50826) == false, "Yellow Dew no longer active") - _assert(hds.get_stat_bonus("att_speed") == 0, "Attack speed bonus reset to 0") - -func _finish() -> void: - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - quit(0) - else: - printerr("SOME TESTS FAILED!\n") - quit(1) diff --git a/project/test_herbal_dew_parity.gd.uid b/project/test_herbal_dew_parity.gd.uid deleted file mode 100644 index f974b3a4..00000000 --- a/project/test_herbal_dew_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dixr6sht8sodk diff --git a/project/test_horse_combat_parity.gd b/project/test_horse_combat_parity.gd deleted file mode 100644 index 270cbc4f..00000000 --- a/project/test_horse_combat_parity.gd +++ /dev/null @@ -1,160 +0,0 @@ -# test_horse_combat_parity.gd —— 40250 战马骑乘战斗与马术技能 1:1 纯净回归测试套件 -extends SceneTree - -const HorseCombatSystem = preload("res://horse_combat_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_horse_combat_parity (Direction 3: Mounted Combat & Horse Skills 1:1) ===") - _test_mounted_state_and_basic_attack() - _test_horse_skills_137_to_140() - _test_cooldowns_and_sp_validation() - _test_stamina_and_feeding() - _finish() - -func _test_mounted_state_and_basic_attack() -> void: - print("\n--- Test 1: Mounted State & 5.0m Attack Sweep Range ---") - var hcs := HorseCombatSystem.new() - - var targets: Array = [ - {"id": 1, "distance": 2.5}, # In range - {"id": 2, "distance": 4.8}, # In range (<= 5.0m) - {"id": 3, "distance": 5.2} # Out of range (> 5.0m) - ] - - # 1. Not riding - var r1 = hcs.perform_mounted_basic_attack(500, targets) - _assert(r1["ok"] == false and r1["reason"] == "NOT_RIDING", "Attack blocked when not riding") - - # 2. Beginner horse (Tier 1) cannot fight - hcs.set_mounted_state(true, HorseCombatSystem.HORSE_TIER_BEGINNER) - var r2 = hcs.perform_mounted_basic_attack(500, targets) - _assert(r2["ok"] == false and r2["reason"] == "HORSE_TIER_TOO_LOW", "Attack blocked on beginner horse") - - # 3. Combat horse (Tier 2) can fight - hcs.set_mounted_state(true, HorseCombatSystem.HORSE_TIER_COMBAT) - var attack_data := {"fired": false, "targets": 0, "dmg": 0} - hcs.horse_attack_performed.connect(func(cnt: int, dmg: int): - attack_data["fired"] = true - attack_data["targets"] = cnt - attack_data["dmg"] = dmg - ) - - var r3 = hcs.perform_mounted_basic_attack(500, targets) - _assert(r3["ok"] == true, "Attack succeeds on combat horse") - _assert(r3["attack_range"] == 5.0, "Mounted attack range is 5.0m") - _assert(r3["hit_count"] == 2, "Hit 2 targets within 5.0m (2.5m and 4.8m)") - _assert(r3["damage_per_hit"] == 600, "1.2x damage multiplier (500 * 1.2 = 600)") - _assert(r3["total_damage"] == 1200, "Total damage is 1200 for 2 targets") - _assert(attack_data["fired"] == true and attack_data["targets"] == 2, "horse_attack_performed signal fired") - -func _test_horse_skills_137_to_140() -> void: - print("\n--- Test 2: 4 Military Horse Skills (137-140) ---") - var hcs := HorseCombatSystem.new() - - # Combat horse (Tier 2) cannot use skills - hcs.set_mounted_state(true, HorseCombatSystem.HORSE_TIER_COMBAT) - var r1 = hcs.use_horse_skill(137, 100, 1000) - _assert(r1["ok"] == false and r1["reason"] == "REQUIRES_MILITARY_HORSE", "Skills blocked on tier 2 horse") - - # Switch to Military Horse (Tier 3) - hcs.set_mounted_state(true, HorseCombatSystem.HORSE_TIER_MILITARY) - - var skill_signal := {"fired": false, "id": 0, "name": ""} - hcs.horse_skill_used.connect(func(sid: int, sname: String, _sp: int): - skill_signal["fired"] = true - skill_signal["id"] = sid - skill_signal["name"] = sname - ) - - # 1. Skill 137: 战马砍击 (Horse Slash) - var s137 = hcs.use_horse_skill(137, 100, 1000) - _assert(s137["ok"] == true, "Skill 137 used successfully") - _assert(s137["damage"] == 2500, "Skill 137 deals 250% damage (2500)") - _assert(s137["effect"] == "KNOCKBACK", "Skill 137 inflicts KNOCKBACK") - _assert(skill_signal["fired"] == true and skill_signal["id"] == 137, "horse_skill_used signal fired for 137") - - # 2. Skill 138: 战马突刺 (Horse Charge) - var s138 = hcs.use_horse_skill(138, 100, 1000) - _assert(s138["ok"] == true, "Skill 138 used successfully") - _assert(s138["damage"] == 2000, "Skill 138 deals 200% damage (2000)") - _assert(s138["effect"] == "STUMBLE", "Skill 138 inflicts STUMBLE") - - # 3. Skill 139: 战马震击 (Horse Stomp) - var s139 = hcs.use_horse_skill(139, 100, 1000) - _assert(s139["ok"] == true, "Skill 139 used successfully") - _assert(s139["damage"] == 3000, "Skill 139 deals 300% damage (3000)") - _assert(s139["effect"] == "AOE_STUN", "Skill 139 inflicts AOE_STUN") - - # 4. Skill 140: 破浪击 (Power Wave) - var s140 = hcs.use_horse_skill(140, 100, 1000) - _assert(s140["ok"] == true, "Skill 140 used successfully") - _assert(s140["damage"] == 2200, "Skill 140 deals 220% damage (2200)") - _assert(s140["range"] == 12.0, "Skill 140 has 12.0m ranged reach") - -func _test_cooldowns_and_sp_validation() -> void: - print("\n--- Test 3: Skill Cooldowns & SP Verification ---") - var hcs := HorseCombatSystem.new() - hcs.set_mounted_state(true, HorseCombatSystem.HORSE_TIER_MILITARY) - - # Cast 137 - hcs.use_horse_skill(137, 100, 1000) - - # Cast again immediately -> on cooldown - var cd_res = hcs.use_horse_skill(137, 100, 1000) - _assert(cd_res["ok"] == false and cd_res["reason"] == "SKILL_ON_COOLDOWN", "Skill 137 is on cooldown") - - # Update 10s -> still on cooldown - hcs.update(10.0) - _assert(hcs.skill_cooldowns[137] == 5.0, "5.0s remaining cooldown on 137") - - # Update 6s -> ready - hcs.update(6.0) - _assert(hcs.skill_cooldowns[137] == 0.0, "Cooldown expired") - - # Try with insufficient SP (< 30) - var low_sp_res = hcs.use_horse_skill(137, 20, 1000) - _assert(low_sp_res["ok"] == false and low_sp_res["reason"] == "INSUFFICIENT_SP", "Skill blocked with low SP") - -func _test_stamina_and_feeding() -> void: - print("\n--- Test 4: Horse Stamina & Feeding (Carrot / Ginseng) ---") - var hcs := HorseCombatSystem.new() - hcs.set_mounted_state(true, HorseCombatSystem.HORSE_TIER_MILITARY) - - # Drain stamina manually to test exhaustion - hcs.horse_stamina = 2.0 - var ex_res = hcs.use_horse_skill(137, 100, 1000) - _assert(ex_res["ok"] == false and ex_res["reason"] == "HORSE_EXHAUSTED", "Skill blocked when stamina < 5") - - # Feed invalid food - var r_bad = hcs.feed_horse(99999) - _assert(r_bad["ok"] == false and r_bad["reason"] == "WRONG_FOOD", "Invalid food rejected") - - # Feed Carrot (50054) -> +40 stamina (2 + 40 = 42) - var r_carrot = hcs.feed_horse(50054) - _assert(r_carrot["ok"] == true, "Fed carrot successfully") - _assert(hcs.horse_stamina == 42.0, "Stamina increased by 40 to 42.0") - - # Feed Ginseng (50058) -> +80 stamina (42 + 80 = 122 -> capped at 100.0) - var r_ginseng = hcs.feed_horse(50058) - _assert(r_ginseng["ok"] == true, "Fed ginseng successfully") - _assert(hcs.horse_stamina == 100.0, "Stamina capped at 100.0") - -func _finish() -> void: - print("\n==========================================") - print("Horse Combat Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_horse_combat_parity.gd.uid b/project/test_horse_combat_parity.gd.uid deleted file mode 100644 index 60495758..00000000 --- a/project/test_horse_combat_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bijpyvqynjl6 diff --git a/project/test_horse_level_growth_parity.gd b/project/test_horse_level_growth_parity.gd deleted file mode 100644 index 72d60b8f..00000000 --- a/project/test_horse_level_growth_parity.gd +++ /dev/null @@ -1,179 +0,0 @@ -# test_horse_level_growth_parity.gd —— 40250 官方马匹等级进化与养殖喂食系统 1:1 回归测试套件 -extends SceneTree - -const HorseLevelGrowthSystem = preload("res://horse_level_growth_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_horse_level_growth_parity (Direction 2: Horse Level Growth 1:1) ===") - _test_horse_tiers_and_leveling() - _test_feeding_food_tier_matching() - _test_horse_death_and_herb_revive() - _test_mount_dismount_and_stamina() - _test_serialization() - _finish() - -func _test_horse_tiers_and_leveling() -> void: - print("\n--- Test 1: Horse Tiers & Level Progression ---") - var sys := HorseLevelGrowthSystem.new() - sys.initialize_horse(1) - _assert(sys.horse_level == 1, "Initialized at level 1") - _assert(sys.get_tier() == HorseLevelGrowthSystem.TIER_BEGINNER, "Tier is Beginner") - _assert(sys.get_tier_name() == "初级小马", "Tier name is 初级小马") - - # Upgrade to Lv 11 with char level 25 (Combat horse requires 35) - sys.horse_level = 10 - var res_low = sys.upgrade_horse_level(25) - _assert(res_low["ok"] == false, "Upgrade rejected: char level 25 < 35") - _assert(res_low["reason"] == "CHAR_LEVEL_TOO_LOW", "Reason is CHAR_LEVEL_TOO_LOW") - - # Upgrade with char level 35 - var res_combat = sys.upgrade_horse_level(35) - _assert(res_combat["ok"] == true, "Upgraded to Lv 11") - _assert(sys.horse_level == 11, "Horse level is 11") - _assert(sys.get_tier() == HorseLevelGrowthSystem.TIER_COMBAT, "Tier is Combat") - _assert(sys.get_tier_name() == "战斗装甲马", "Tier name is 战斗装甲马") - - # Upgrade to Lv 21 with char level 45 (Military horse requires 50) - sys.horse_level = 20 - var res_low2 = sys.upgrade_horse_level(45) - _assert(res_low2["ok"] == false, "Upgrade rejected: char level 45 < 50") - - # Upgrade with char level 50 - var res_mil = sys.upgrade_horse_level(50) - _assert(res_mil["ok"] == true, "Upgraded to Lv 21") - _assert(sys.horse_level == 21, "Horse level is 21") - _assert(sys.get_tier() == HorseLevelGrowthSystem.TIER_MILITARY, "Tier is Military") - _assert(sys.get_tier_name() == "军阶战马", "Tier name is 军阶战马") - -func _test_feeding_food_tier_matching() -> void: - print("\n--- Test 2: Feeding Food Tier Matching (Hay, Carrot, Ginseng) ---") - var sys := HorseLevelGrowthSystem.new() - sys.initialize_horse(1) # Beginner horse - sys.current_health = 1 # Lower health so we can feed - - var inv: Array = [ - {"vnum": HorseLevelGrowthSystem.VNUM_HAY, "name": "干草", "count": 2}, - {"vnum": HorseLevelGrowthSystem.VNUM_CARROT, "name": "胡萝卜", "count": 2}, - {"vnum": HorseLevelGrowthSystem.VNUM_RED_GINSENG, "name": "红参", "count": 2} - ] - - # 1. Beginner horse fed Carrot -> mismatch - var feed_mismatch = sys.feed_horse(inv, 1) - _assert(feed_mismatch["ok"] == false, "Feeding Carrot to Beginner horse rejected") - _assert(feed_mismatch["reason"] == "FOOD_TIER_MISMATCH", "Reason is FOOD_TIER_MISMATCH") - - # 2. Beginner horse fed Hay -> success - var feed_succ = sys.feed_horse(inv, 0) - _assert(feed_succ["ok"] == true, "Feeding Hay to Beginner horse succeeded") - _assert(sys.current_health == 3, "Health increased to 3") - _assert(inv[0]["count"] == 1, "Hay count decremented to 1") - - # 3. Combat horse fed Carrot - sys.initialize_horse(15) - sys.current_health = 10 - var feed_combat = sys.feed_horse(inv, 1) - _assert(feed_combat["ok"] == true, "Feeding Carrot to Combat horse succeeded") - - # 4. Military horse fed Red Ginseng - sys.initialize_horse(25) - sys.current_health = 20 - var feed_mil = sys.feed_horse(inv, 2) - _assert(feed_mil["ok"] == true, "Feeding Red Ginseng to Military horse succeeded") - -func _test_horse_death_and_herb_revive() -> void: - print("\n--- Test 3: Horse Death & Ape Herb Revive ---") - var sys := HorseLevelGrowthSystem.new() - sys.initialize_horse(1) # Beginner horse - sys.current_health = 0 # Dead/Fainted - - var inv: Array = [ - {"vnum": HorseLevelGrowthSystem.VNUM_HERB_NORMAL, "name": "中级猴草", "count": 1}, - {"vnum": HorseLevelGrowthSystem.VNUM_HERB_EASY, "name": "初级猴草", "count": 1} - ] - - # Cannot mount dead horse - var player := {"attack_power": 100, "defense": 100} - var mount_dead = sys.mount_horse(player) - _assert(mount_dead["ok"] == false, "Cannot mount dead horse") - _assert(mount_dead["reason"] == "HORSE_IS_DEAD", "Reason is HORSE_IS_DEAD") - - # Revive with wrong herb (Normal herb on Beginner horse) - var revive_wrong = sys.revive_horse(inv, 0) - _assert(revive_wrong["ok"] == false, "Wrong herb tier rejected") - _assert(revive_wrong["reason"] == "HERB_TIER_MISMATCH", "Reason is HERB_TIER_MISMATCH") - - # Revive with correct Easy Ape herb - var revive_sig := {"fired": false, "hp": 0} - sys.horse_revived.connect(func(h: int): - revive_sig["fired"] = true - revive_sig["hp"] = h - ) - - var revive_ok = sys.revive_horse(inv, 1) - _assert(revive_ok["ok"] == true, "Horse successfully revived with Easy Ape herb") - _assert(sys.current_health > 0, "Current health > 0") - _assert(inv[1] == null, "Herb consumed") - _assert(revive_sig["fired"] == true, "horse_revived signal emitted") - -func _test_mount_dismount_and_stamina() -> void: - print("\n--- Test 4: Mount, Dismount, & Stamina Depletion ---") - var sys := HorseLevelGrowthSystem.new() - sys.initialize_horse(21) # Military horse Lv 21: atk 84, def 50 - var player := {"attack_power": 500, "defense": 200} - - # Mount - var mount_res = sys.mount_horse(player) - _assert(mount_res["ok"] == true, "Mounted military horse") - _assert(sys.is_mounted == true, "is_mounted is true") - _assert(player["attack_power"] == 584, "Attack power increased 500 -> 584 (+84)") - _assert(player["defense"] == 250, "Defense increased 200 -> 250 (+50)") - - # Consume stamina until exhausted - var exhaust_sig := {"fired": false} - sys.stamina_exhausted.connect(func(): - exhaust_sig["fired"] = true - ) - - sys.consume_stamina(999, player) - _assert(exhaust_sig["fired"] == true, "stamina_exhausted signal emitted") - _assert(sys.is_mounted == false, "Auto-dismounted on exhaustion") - _assert(player["attack_power"] == 500, "Attack power reversed back to 500") - _assert(player["defense"] == 200, "Defense reversed back to 200") - -func _test_serialization() -> void: - print("\n--- Test 5: Serialization & Restoration ---") - var sys1 := HorseLevelGrowthSystem.new() - sys1.initialize_horse(25) - sys1.current_health = 30 - sys1.current_stamina = 80 - sys1.is_mounted = true - - var data = sys1.serialize() - var sys2 := HorseLevelGrowthSystem.new() - sys2.deserialize(data) - - _assert(sys2.horse_level == 25, "Restored horse level 25") - _assert(sys2.get_tier_name() == "军阶战马", "Restored tier military") - _assert(sys2.current_health == 30, "Restored health 30") - _assert(sys2.current_stamina == 80, "Restored stamina 80") - _assert(sys2.is_mounted == true, "Restored mounted state") - -func _finish() -> void: - print("\n=======================================================") - print("Horse Level Growth Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_horse_level_growth_parity.gd.uid b/project/test_horse_level_growth_parity.gd.uid deleted file mode 100644 index 5107e335..00000000 --- a/project/test_horse_level_growth_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b6vad1nro7ocu diff --git a/project/test_inventory_expansion_parity.gd b/project/test_inventory_expansion_parity.gd deleted file mode 100644 index 34d1360c..00000000 --- a/project/test_inventory_expansion_parity.gd +++ /dev/null @@ -1,147 +0,0 @@ -# test_inventory_expansion_parity.gd —— 40250 背包扩展凭证与阶梯解锁系统回归测试 -extends SceneTree - -const SystemClass = preload("res://inventory_expansion_system.gd") - -var _passed: int = 0 -var _failed: int = 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_inventory_expansion_parity (Batch 26 Direction 4) ===") - _test_default_unlocked_baseline() - _test_key_cost_curve() - _test_stage_by_stage_unlocking() - _test_locked_slot_item_placement_rejection() - _test_multislot_item_boundary_and_lock_check() - _test_full_180_slots_unlock_completion() - - print("\n=== Result: %d Passed, %d Failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - else: - printerr("SOME TESTS FAILED!\n") - quit(0 if _failed == 0 else 1) - -func _test_default_unlocked_baseline() -> void: - print("\n--- Test 1: Default Baseline (Pages 1 & 2 = 90 Slots Unlocked) ---") - var inv = SystemClass.new() - _assert(inv.get_unlocked_slot_count() == 90, "Baseline has 90 unlocked slots (2 pages)") - _assert(inv.is_slot_unlocked(0) == true, "Slot 0 (Page 1) is unlocked") - _assert(inv.is_slot_unlocked(44) == true, "Slot 44 (Page 1 end) is unlocked") - _assert(inv.is_slot_unlocked(45) == true, "Slot 45 (Page 2 start) is unlocked") - _assert(inv.is_slot_unlocked(89) == true, "Slot 89 (Page 2 end) is unlocked") - - # Locked slots on Pages 3 & 4 - _assert(inv.is_slot_unlocked(90) == false, "Slot 90 (Page 3 start) is locked") - _assert(inv.is_slot_unlocked(134) == false, "Slot 134 (Page 3 end) is locked") - _assert(inv.is_slot_unlocked(135) == false, "Slot 135 (Page 4 start) is locked") - _assert(inv.is_slot_unlocked(179) == false, "Slot 179 (Page 4 end) is locked") - -func _test_key_cost_curve() -> void: - print("\n--- Test 2: 18-Stage Key Cost Curve (Official 40250) ---") - var inv = SystemClass.new() - _assert(inv.get_next_stage_key_cost() == 1, "Stage 1 costs 1 key") - - inv.unlocked_stages = 5 - _assert(inv.get_next_stage_key_cost() == 1, "Stage 6 costs 1 key") - - inv.unlocked_stages = 6 - _assert(inv.get_next_stage_key_cost() == 2, "Stage 7 costs 2 keys") - - inv.unlocked_stages = 11 - _assert(inv.get_next_stage_key_cost() == 2, "Stage 12 costs 2 keys") - - inv.unlocked_stages = 12 - _assert(inv.get_next_stage_key_cost() == 3, "Stage 13 costs 3 keys") - - inv.unlocked_stages = 17 - _assert(inv.get_next_stage_key_cost() == 4, "Stage 18 costs 4 keys") - - # Sum of all stages - var total_keys := 0 - for cost in SystemClass.STAGE_KEY_COSTS: - total_keys += cost - _assert(total_keys == 37, "Total keys for all 18 stages is exactly 37 keys") - -func _test_stage_by_stage_unlocking() -> void: - print("\n--- Test 3: Stage-by-Stage Unlocking and Key Deduction ---") - var inv = SystemClass.new() - - # Insufficient keys - var r_fail = inv.unlock_next_stage(0) - _assert(r_fail["error_code"] == "INSUFFICIENT_KEYS", "Reject unlock if keys < required") - - # Unlock stage 1 (costs 1 key) - var r_stage1 = inv.unlock_next_stage(10) - _assert(r_stage1["success"] == true, "Stage 1 unlocked") - _assert(r_stage1["keys_deducted"] == 1, "1 key deducted") - _assert(r_stage1["remaining_keys"] == 9, "9 keys remain") - _assert(r_stage1["unlocked_slots"] == 95, "Unlocked slots increased to 95") - _assert(inv.is_slot_unlocked(90) == true, "Slot 90 now unlocked") - _assert(inv.is_slot_unlocked(94) == true, "Slot 94 now unlocked") - _assert(inv.is_slot_unlocked(95) == false, "Slot 95 still locked") - -func _test_locked_slot_item_placement_rejection() -> void: - print("\n--- Test 4: Locked Slot Item Placement Rejection ---") - var inv = SystemClass.new() - var sword = { "vnum": 10, "name": "Sword+0" } - - # Put into unlocked slot 0 - var r_ok = inv.put_item(0, sword, 1) - _assert(r_ok["success"] == true, "Placed item into unlocked slot 0") - - # Put into locked slot 90 - var r_lock = inv.put_item(90, sword, 1) - _assert(r_lock["error_code"] == "SLOT_LOCKED", "Reject putting item into locked slot 90") - -func _test_multislot_item_boundary_and_lock_check() -> void: - print("\n--- Test 5: Multi-Slot Item Boundary & Partial Lock Detection ---") - var inv = SystemClass.new() - var greatsword = { "vnum": 3000, "name": "Glaive+0" } # Height = 3 - - # Slot 80 is Page 2, row 7 (cells 80, 85, 90). - # But row 7 + 3 = 10 > 9 -> PAGE_OVERFLOW! - var r_overflow = inv.can_place_item_at(80, 3) - _assert(r_overflow["error_code"] == "PAGE_OVERFLOW", "Reject multi-slot item crossing page bottom") - - # Slot 70 is Page 2, row 5 (cells 70, 75, 80). row 5 + 3 = 8 <= 9 -> OK! - var r_valid = inv.can_place_item_at(70, 3) - _assert(r_valid["can_place"] == true, "Can place 3-slot item safely inside page 2") - - # Now unlock Stage 1 (slots 90..94 unlocked) - inv.unlock_next_stage(1) - # Try to place 2-slot item at slot 91 (needs 91 and 96; 91 is unlocked, but 96 is locked!) - var r_part_locked = inv.can_place_item_at(91, 2) - _assert(r_part_locked["error_code"] == "SLOT_LOCKED", "Reject multi-slot item when lower cell (96) is locked") - - # But 1-slot item at 91 is fine - var r_single_ok = inv.can_place_item_at(91, 1) - _assert(r_single_ok["can_place"] == true, "Single slot item at 91 can be placed") - -func _test_full_180_slots_unlock_completion() -> void: - print("\n--- Test 6: Full 180 Slots Unlocking (All 18 Stages) ---") - var inv = SystemClass.new() - var total_keys_available := 50 - - # Unlock all 18 stages - for s in range(18): - var res = inv.unlock_next_stage(total_keys_available) - _assert(res["success"] == true, "Stage %d unlocked successfully" % (s + 1)) - total_keys_available = res["remaining_keys"] - - _assert(inv.unlocked_stages == 18, "All 18 stages unlocked") - _assert(inv.get_unlocked_slot_count() == 180, "All 180 slots unlocked") - _assert(inv.is_slot_unlocked(179) == true, "Final slot 179 is unlocked") - _assert(total_keys_available == 50 - 37, "Remaining keys is 13 (50 - 37)") - - # Attempting 19th stage - var r_max = inv.unlock_next_stage(total_keys_available) - _assert(r_max["error_code"] == "ALREADY_MAX_EXPANDED", "Reject unlocking beyond 18 stages") diff --git a/project/test_inventory_expansion_parity.gd.uid b/project/test_inventory_expansion_parity.gd.uid deleted file mode 100644 index a66841a5..00000000 --- a/project/test_inventory_expansion_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bk2w3st68coxe diff --git a/project/test_inventory_sort_parity.gd b/project/test_inventory_sort_parity.gd deleted file mode 100644 index 7ba58806..00000000 --- a/project/test_inventory_sort_parity.gd +++ /dev/null @@ -1,119 +0,0 @@ -# test_inventory_sort_parity.gd —— 40250 多页背包与智能自动整理 1:1 纯净回归测试套件 -extends SceneTree - -const InventorySortSystem = preload("res://inventory_sort_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_inventory_sort_parity (Direction 2: Inventory Sort & Stacking 1:1) ===") - _test_page_switching() - _test_stack_merging() - _test_category_sorting_and_compaction() - _finish() - -func _test_page_switching() -> void: - print("\n--- Test 1: Multi-Page Switching (Page 0 & Page 1, 90 Slots) ---") - var iss := InventorySortSystem.new() - - var page_signal := {"fired": false, "page": -1} - iss.page_switched.connect(func(p: int): - page_signal["fired"] = true - page_signal["page"] = p - ) - - # Invalid page - var r_bad = iss.switch_page(2) - _assert(r_bad["ok"] == false and r_bad["reason"] == "INVALID_PAGE", "Page 2 rejected (> max page 1)") - - # Page 0 (Slots 0..44) - var r0 = iss.switch_page(0) - _assert(r0["ok"] == true, "Page 0 switched successfully") - _assert(r0["slot_start"] == 0 and r0["slot_end"] == 44, "Page 0 covers slots 0 to 44") - _assert(page_signal["fired"] == true and page_signal["page"] == 0, "page_switched signal fired for 0") - - # Page 1 (Slots 45..89) - var r1 = iss.switch_page(1) - _assert(r1["ok"] == true, "Page 1 switched successfully") - _assert(r1["slot_start"] == 45 and r1["slot_end"] == 89, "Page 1 covers slots 45 to 89") - _assert(page_signal["page"] == 1, "page_switched signal fired for 1") - -func _test_stack_merging() -> void: - print("\n--- Test 2: Auto-Stack Merging (Max 200 Limit) ---") - var iss := InventorySortSystem.new() - var inv: Array = [ - {"vnum": 27003, "name": "红药水(大)", "item_type": "potion", "count": 50}, # Slot 0 - null, # Slot 1 - {"vnum": 27003, "name": "红药水(大)", "item_type": "potion", "count": 80}, # Slot 2 - {"vnum": 27003, "name": "红药水(大)", "item_type": "potion", "count": 100} # Slot 3 - ] - - var merged = iss.merge_stacks(inv) - _assert(merged == 2, "2 stack merges performed") - - # Slot 0 should have 50 + 80 + 70 = 200 (max cap) - _assert(inv[0]["count"] == 200, "Slot 0 merged to max 200") - # Slot 2 fully consumed into slot 0 - _assert(inv[2] == null, "Slot 2 consumed and set to null") - # Slot 3 had 100, gave 70 to slot 0 -> 30 remaining - _assert(inv[3] != null and inv[3]["count"] == 30, "Slot 3 has 30 remaining count") - -func _test_category_sorting_and_compaction() -> void: - print("\n--- Test 3: Category Sorting (Weapons -> Armors -> Consumables -> Materials -> Chests) ---") - var iss := InventorySortSystem.new() - - var sort_signal := {"fired": false, "items": 0, "merged": 0} - iss.inventory_sorted.connect(func(cnt: int, m: int): - sort_signal["fired"] = true - sort_signal["items"] = cnt - sort_signal["merged"] = m - ) - - # Fragmented messy inventory - var inv: Array = [ - {"vnum": 71084, "name": "高级附魔石", "item_type": "material", "count": 5}, # Mat - null, - {"vnum": 169, "name": "战神之剑+9", "item_type": "weapon"}, # Weapon - {"vnum": 27003, "name": "红药水(大)", "item_type": "potion", "count": 10}, # Potion - null, - {"vnum": 11299, "name": "黑钢甲+9", "item_type": "armor"}, # Armor - {"vnum": 50006, "name": "九尾宝箱", "item_type": "chest"} # Chest - ] - - var res = iss.sort_inventory(inv) - _assert(res["ok"] == true, "sort_inventory succeeded") - _assert(res["total_items"] == 5, "5 total non-empty items sorted") - _assert(sort_signal["fired"] == true and sort_signal["items"] == 5, "inventory_sorted signal fired") - - # Check exact order: - # 1. Weapon (169) - # 2. Armor (11299) - # 3. Consumable (27003) - # 4. Material (71084) - # 5. Chest (50006) - _assert(inv[0] != null and inv[0]["vnum"] == 169, "Slot 0 is Weapon (169)") - _assert(inv[1] != null and inv[1]["vnum"] == 11299, "Slot 1 is Armor (11299)") - _assert(inv[2] != null and inv[2]["vnum"] == 27003, "Slot 2 is Potion (27003)") - _assert(inv[3] != null and inv[3]["vnum"] == 71084, "Slot 3 is Material (71084)") - _assert(inv[4] != null and inv[4]["vnum"] == 50006, "Slot 4 is Chest (50006)") - - # Remaining slots compacted to null - _assert(inv[5] == null and inv[6] == null, "Trailing slots compacted to null") - -func _finish() -> void: - print("\n==========================================") - print("Inventory Sort Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_inventory_sort_parity.gd.uid b/project/test_inventory_sort_parity.gd.uid deleted file mode 100644 index 5a9dfda2..00000000 --- a/project/test_inventory_sort_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ck1abvqshw10w diff --git a/project/test_item_enchant_manager_parity.gd b/project/test_item_enchant_manager_parity.gd deleted file mode 100644 index b80575a8..00000000 --- a/project/test_item_enchant_manager_parity.gd +++ /dev/null @@ -1,173 +0,0 @@ -# test_item_enchant_manager_parity.gd —— 40250 装备附魔洗炼与稀有词条重铸 1:1 回归测试套件 -extends SceneTree - -const ItemEnchantSystem = preload("res://item_enchant_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_item_enchant_manager_parity (Direction 3: Item Enchant & Rare Reforge 1:1) ===") - _test_scroll_and_item_compatibility() - _test_add_1_to_4_attributes() - _test_blessing_marble_5th_attribute() - _test_change_attributes() - _test_rare_6th_and_7th_attributes() - _finish() - -func _test_scroll_and_item_compatibility() -> void: - print("\n--- Test 1: Scroll & Equipment Compatibility ---") - _assert(ItemEnchantSystem.is_enchant_scroll(71085), "71085 is enchant scroll") - _assert(ItemEnchantSystem.is_enchant_scroll(71084), "71084 is enchant scroll") - _assert(ItemEnchantSystem.is_enchant_scroll(70024), "70024 is enchant scroll") - _assert(ItemEnchantSystem.is_enchant_scroll(71151), "71151 is enchant scroll") - _assert(ItemEnchantSystem.is_enchant_scroll(71152), "71152 is enchant scroll") - _assert(not ItemEnchantSystem.is_enchant_scroll(27001), "Potion is not enchant scroll") - - var sword := {"vnum": 10, "type": 1, "attrs": []} - var arrow := {"vnum": 8000, "type": 1, "attrs": []} - var locked_armor := {"vnum": 11299, "type": 2, "is_locked": true, "attrs": []} - - _assert(ItemEnchantSystem.is_enchantable_item(sword), "Sword is enchantable") - _assert(not ItemEnchantSystem.is_enchantable_item(arrow), "Arrow (8000) cannot be enchanted") - _assert(not ItemEnchantSystem.is_enchantable_item(locked_armor), "Locked armor is protected from enchanting") - -func _test_add_1_to_4_attributes() -> void: - print("\n--- Test 2: Add 1st~4th Attributes (71085) ---") - var sys := ItemEnchantSystem.new() - var inv: Array = [ - {"vnum": ItemEnchantSystem.VNUM_ATTR_ADD, "name": "添加属性秘籍", "count": 10}, - {"vnum": 10, "name": "巨剑+9", "type": 1, "attrs": []} - ] - - var sig_data := {"fired": false, "slot": -1, "success": false} - sys.enchant_applied.connect(func(slot: int, _svnum: int, succ: bool, _attrs: Array): - sig_data["fired"] = true - sig_data["slot"] = slot - sig_data["success"] = succ - ) - - # Add 1st attribute (force_success = true) - var res1 = sys.apply_scroll(0, 1, inv, true) - _assert(res1["ok"] == true and res1["success"] == true, "1st attribute added") - _assert(inv[1]["attrs"].size() == 1, "Sword has 1 attribute") - _assert(inv[0]["count"] == 9, "Scroll count decreased 10 -> 9") - _assert(sig_data["fired"] == true and sig_data["slot"] == 1, "enchant_applied signal emitted") - - # Add 2nd, 3rd, 4th attributes - sys.apply_scroll(0, 1, inv, true) - sys.apply_scroll(0, 1, inv, true) - sys.apply_scroll(0, 1, inv, true) - _assert(inv[1]["attrs"].size() == 4, "Sword has 4 attributes") - _assert(inv[0]["count"] == 6, "Scroll count is now 6") - - # 5th attempt with 71085 must be rejected - var res5 = sys.apply_scroll(0, 1, inv, true) - _assert(res5["ok"] == false and res5["reason"] == "MAX_BASE_ATTRS_REACHED", "5th attempt rejected by 71085") - _assert(inv[0]["count"] == 6, "Scroll count NOT consumed on failure") - -func _test_blessing_marble_5th_attribute() -> void: - print("\n--- Test 3: Blessing Marble 5th Attribute (70024) ---") - var sys := ItemEnchantSystem.new() - var inv: Array = [ - {"vnum": ItemEnchantSystem.VNUM_BLESSING_MARBLE, "name": "祝福宝珠", "count": 2}, - {"vnum": 10, "name": "巨剑+9", "type": 1, "attrs": [{"type": 1, "name": "最大生命", "value": 2000}]} # only 1 attr - ] - - # Using on item with < 4 attrs must fail - var fail_res = sys.apply_scroll(0, 1, inv, true) - _assert(fail_res["ok"] == false and fail_res["reason"] == "NEED_4_ATTRS_FIRST", "Blessing marble requires 4 attrs first") - - # Fill up to 4 attrs - inv[1]["attrs"] = [ - {"type": 1, "name": "HP", "value": 2000}, - {"type": 5, "name": "STR", "value": 12}, - {"type": 15, "name": "Crit", "value": 10}, - {"type": 16, "name": "Pen", "value": 10} - ] - - # Now use blessing marble - var res5 = sys.apply_scroll(0, 1, inv, true) - _assert(res5["ok"] == true and res5["success"] == true, "5th attribute added by Blessing Marble") - _assert(inv[1]["attrs"].size() == 5, "Item now has exactly 5 attributes") - _assert(inv[0]["count"] == 1, "Marble consumed (2 -> 1)") - - # 6th attempt with blessing marble must be rejected - var res6 = sys.apply_scroll(0, 1, inv, true) - _assert(res6["ok"] == false and res6["reason"] == "ALREADY_HAS_5_ATTRS", "Blessing marble rejected when 5 attrs exist") - -func _test_change_attributes() -> void: - print("\n--- Test 4: Change All Attributes (71084) ---") - var sys := ItemEnchantSystem.new() - var old_attrs: Array = [ - {"type": 1, "name": "HP", "value": 500}, - {"type": 2, "name": "SP", "value": 100}, - {"type": 7, "name": "AtkSpeed", "value": 2} - ] - var inv: Array = [ - {"vnum": ItemEnchantSystem.VNUM_ATTR_CHANGE, "name": "属性改变秘籍", "count": 1}, - {"vnum": 10, "name": "巨剑+9", "type": 1, "attrs": old_attrs.duplicate(true)} - ] - - var res = sys.apply_scroll(0, 1, inv) - _assert(res["ok"] == true and res["success"] == true, "Attributes rerolled") - _assert(inv[1]["attrs"].size() == 3, "Still has exactly 3 attributes") - _assert(inv[0] == null, "Single scroll consumed to null") - -func _test_rare_6th_and_7th_attributes() -> void: - print("\n--- Test 5: 6th & 7th Rare Attributes (71151 / 71152) ---") - var sys := ItemEnchantSystem.new() - var inv: Array = [ - {"vnum": ItemEnchantSystem.VNUM_RARE_ADD, "name": "稀有属性秘籍", "count": 3}, - {"vnum": ItemEnchantSystem.VNUM_RARE_CHANGE, "name": "稀有属性改变", "count": 1}, - {"vnum": 10, "name": "巨剑+9", "type": 1, "attrs": [{"type": 1, "name": "HP", "value": 2000}], "rare_attrs": []} - ] - - var rare_sig := {"fired": false, "rares": []} - sys.rare_enchant_applied.connect(func(_slot: int, _succ: bool, rares: Array): - rare_sig["fired"] = true - rare_sig["rares"] = rares - ) - - # Add 6th attribute (1st rare) - var r1 = sys.apply_scroll(0, 2, inv, true) - _assert(r1["ok"] == true and r1["success"] == true, "6th attribute (1st rare) added") - _assert(inv[2]["rare_attrs"].size() == 1, "1 rare attribute present") - _assert(inv[2]["attrs"].size() == 1, "Base attrs untouched") - _assert(rare_sig["fired"] == true, "rare_enchant_applied signal emitted") - - # Add 7th attribute (2nd rare) - var r2 = sys.apply_scroll(0, 2, inv, true) - _assert(r2["ok"] == true and r2["success"] == true, "7th attribute (2nd rare) added") - _assert(inv[2]["rare_attrs"].size() == 2, "2 rare attributes present") - - # 3rd rare must be rejected - var r3 = sys.apply_scroll(0, 2, inv, true) - _assert(r3["ok"] == false and r3["reason"] == "MAX_RARE_ATTRS", "3rd rare rejected") - - # Change rare attributes (71152) - var rc = sys.apply_scroll(1, 2, inv) - _assert(rc["ok"] == true and rc["success"] == true, "Rare attributes rerolled") - _assert(inv[2]["rare_attrs"].size() == 2, "2 rare attributes preserved after reroll") - _assert(inv[2]["attrs"].size() == 1, "Base attrs remain intact") - _assert(inv[1] == null, "71152 scroll consumed") - -func _finish() -> void: - print("\n==========================================") - print("Item Enchant Manager Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Item Enchant Manager parity tests passed!") - quit(0) diff --git a/project/test_item_enchant_manager_parity.gd.uid b/project/test_item_enchant_manager_parity.gd.uid deleted file mode 100644 index 01dfd139..00000000 --- a/project/test_item_enchant_manager_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dqxojn342f2r3 diff --git a/project/test_item_split_parity.gd b/project/test_item_split_parity.gd deleted file mode 100644 index 717e2a75..00000000 --- a/project/test_item_split_parity.gd +++ /dev/null @@ -1,150 +0,0 @@ -# test_item_split_parity.gd —— 40250 物品精准拆分与快捷操作 1:1 纯净回归测试套件 -extends SceneTree - -const ItemSplitSystem = preload("res://item_split_system.gd") - -var _passed := 0 -var _failed := 0 - -class FakeClient extends Node: - var move_calls: Array = [] - var sell_calls: Array = [] - var move_ok := true - var sell_ok := true - - func move_item(fw: int, fc: int, tw: int, tc: int, count: int) -> bool: - move_calls.append([fw, fc, tw, tc, count]) - return move_ok - - func shop_sell(cell: int, count: int) -> bool: - sell_calls.append([cell, count]) - return sell_ok - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_item_split_parity (Direction 3: Item Split & Quick Sell 1:1) ===") - _test_item_stack_splitting() - _test_quick_sell_to_shop() - _test_drop_safety_confirmation() - _finish() - -func _test_item_stack_splitting() -> void: - print("\n--- Test 1: Stack Splitting (Shift + Left Click) ---") - var iss := ItemSplitSystem.new() - var client := FakeClient.new() - var inv: Array = [ - {"vnum": 27003, "name": "红药水(大)", "count": 200}, # Slot 0 - {"vnum": 10, "name": "木剑+0", "count": 1}, # Slot 1 (single) - null, # Slot 2 (empty) - null # Slot 3 (empty) - ] - - # 1. Split single count item - var r_single = iss.split_item(1, 1, 2, inv) - _assert(r_single["ok"] == false and r_single["reason"] == "NOT_SPLITTABLE", "Single count item rejected for split") - - # 2. Split count >= total count - var r_overflow = iss.split_item(0, 200, 2, inv) - _assert(r_overflow["ok"] == false and r_overflow["reason"] == "INVALID_SPLIT_COUNT", "Split count >= total rejected") - - # 3. Valid split: Split 50 from 200 into slot 2 - var split_signal := {"fired": false, "src": -1, "dst": -1, "cnt": 0} - iss.item_split.connect(func(s: int, d: int, c: int): - split_signal["fired"] = true - split_signal["src"] = s - split_signal["dst"] = d - split_signal["cnt"] = c - ) - - var r_split = iss.split_item(0, 50, 2, inv, client) - _assert(r_split["ok"] == true, "Split 50 potions succeeded") - _assert(r_split["pending"] == true and client.move_calls == [[1, 0, 1, 2, 50]], - "Split sends CG_ITEM_MOVE with source, destination and count") - _assert(inv[0]["count"] == 200 and inv[2] == null, - "Split request does not mutate local inventory before GC confirmation") - _assert(not split_signal["fired"], "legacy item_split is not emitted before server confirmation") - - # 4. Auto-find empty slot when target_slot is -1 - # Simulate the first request's destination after a server GC confirmation; - # the helper itself must still not mutate this snapshot. - inv[2] = {"vnum": 27003, "count": 50} - var r_auto = iss.split_item(0, 30, -1, inv, client) - _assert(r_auto["ok"] == true, "Auto-find split succeeded") - _assert(r_auto["target_slot"] == 3, "Auto-placed into slot 3") - _assert(client.move_calls[-1] == [1, 0, 1, 3, 30], - "Auto split sends the selected empty destination") - _assert(inv[3] == null and inv[0]["count"] == 200, - "Auto split also waits for server confirmation") - client.free() - -func _test_quick_sell_to_shop() -> void: - print("\n--- Test 2: Quick Sell to NPC Shop (Ctrl + Right Click) ---") - var iss := ItemSplitSystem.new() - var client := FakeClient.new() - var player_data := {"gold": 1000} - var inv: Array = [ - {"vnum": 10, "name": "木剑+0", "count": 1, "sale_price": 50, "is_locked": true}, # Locked - {"vnum": 27001, "name": "小红药水", "count": 20, "sale_price": 100} # 20 x 100 = 2000 - ] - - # 1. Locked item cannot be sold - var r_lock = iss.quick_sell_item(0, inv, player_data) - _assert(r_lock["ok"] == false and r_lock["reason"] == "ITEM_LOCKED", "Locked item quick sell blocked") - - # 2. Valid quick sell - var sell_signal := {"fired": false, "count": 0} - iss.item_quick_sell_requested.connect(func(_s: int, c: int, _n: String): - sell_signal["fired"] = true - sell_signal["count"] = c - ) - - var r_sell = iss.quick_sell_item(1, inv, player_data, client) - _assert(r_sell["ok"] == true, "Quick sell succeeded") - _assert(r_sell["pending"] == true and client.sell_calls == [[1, 20]], - "Quick sell sends CG_SHOP SELL2 with cell and count") - _assert(inv[1] != null and inv[1]["count"] == 20, - "Quick sell does not clear local item before server confirmation") - _assert(player_data["gold"] == 1000, "Quick sell does not add local gold before confirmation") - _assert(sell_signal["fired"] and sell_signal["count"] == 20, - "item_quick_sell_requested signal records pending request") - client.free() - -func _test_drop_safety_confirmation() -> void: - print("\n--- Test 3: Drop Safety Check for Valuable Equipment ---") - var iss := ItemSplitSystem.new() - - # 1. Locked item - var locked_item := {"vnum": 169, "name": "战神之剑+9", "is_locked": true} - var r1 = iss.check_drop_safety(locked_item) - _assert(r1["can_drop"] == false and r1["reason"] == "ITEM_LOCKED", "Locked item drop blocked") - - # 2. +9 equipment requires confirmation - var high_refine := {"vnum": 169, "name": "战神之剑+9", "refine_level": 9, "is_locked": false} - var r2 = iss.check_drop_safety(high_refine) - _assert(r2["can_drop"] == true and r2["require_confirm"] == true, "+9 weapon requires confirmation") - - # 3. Item with bonus attributes requires confirmation - var bonus_item := {"vnum": 10, "name": "木剑+0", "attributes": [{"type": 1, "value": 500}], "is_locked": false} - var r3 = iss.check_drop_safety(bonus_item) - _assert(r3["can_drop"] == true and r3["require_confirm"] == true, "Item with bonuses requires confirmation") - - # 4. Plain +0 item requires no confirmation - var plain_item := {"vnum": 10, "name": "木剑+0", "refine_level": 0, "is_locked": false} - var r4 = iss.check_drop_safety(plain_item) - _assert(r4["can_drop"] == true and r4["require_confirm"] == false, "Plain item drops without confirmation") - -func _finish() -> void: - print("\n==========================================") - print("Item Split Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_item_split_parity.gd.uid b/project/test_item_split_parity.gd.uid deleted file mode 100644 index 83833c50..00000000 --- a/project/test_item_split_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://e50i1puey42f diff --git a/project/test_jewelry_socket_parity.gd b/project/test_jewelry_socket_parity.gd deleted file mode 100644 index dfa06f2f..00000000 --- a/project/test_jewelry_socket_parity.gd +++ /dev/null @@ -1,130 +0,0 @@ -# test_jewelry_socket_parity.gd —— 40250 首饰打孔与精炼宝石镶嵌 1:1 纯净回归测试套件 -extends SceneTree - -const JewelrySocketSystem = preload("res://jewelry_socket_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_jewelry_socket_parity (Direction 3: Jewelry Sockets 1:1) ===") - _test_accessory_validation_and_diamond() - _test_socket_drilling_progression() - _test_refined_gem_insertion_and_bonuses() - _test_endurance_decay_and_expiration() - _finish() - -func _test_accessory_validation_and_diamond() -> void: - print("\n--- Test 1: Accessory Validation & Diamond Check ---") - var sword := {"vnum": 10} # Weapon - var armor := {"vnum": 11200} # Armor - var ebony_earrings := {"vnum": 17109} # Earrings (17xxx) - var diamond := {"vnum": 50621} # Refined Diamond - var fake_stone := {"vnum": 28030} # Metin stone - - # Non-accessory rejected - _assert(JewelrySocketSystem.is_accessory(sword) == false, "Sword is not an accessory") - _assert(JewelrySocketSystem.is_accessory(armor) == false, "Armor is not an accessory") - _assert(JewelrySocketSystem.is_accessory(ebony_earrings) == true, "Ebony Earrings is an accessory") - - var res = JewelrySocketSystem.drill_socket(sword, diamond) - _assert(res["ok"] == false and res["reason"] == "NOT_ACCESSORY", "Cannot drill sockets on weapons") - - # Non diamond rejected - res = JewelrySocketSystem.drill_socket(ebony_earrings, fake_stone) - _assert(res["ok"] == false and res["reason"] == "NOT_DIAMOND", "Cannot drill without refined diamond (50621)") - -func _test_socket_drilling_progression() -> void: - print("\n--- Test 2: Socket Drilling Progression (1~3 Sockets) ---") - var necklace := {"vnum": 16109, "socket_count": 0} - var diamond := {"vnum": 50621} - - # Drill 1st socket - var res = JewelrySocketSystem.drill_socket(necklace, diamond, true) - _assert(res["ok"] == true and res["socket_count"] == 1, "1st socket drilled successfully") - - # Drill 2nd socket - res = JewelrySocketSystem.drill_socket(necklace, diamond, true) - _assert(res["ok"] == true and res["socket_count"] == 2, "2nd socket drilled successfully") - - # Drill 3rd socket - res = JewelrySocketSystem.drill_socket(necklace, diamond, true) - _assert(res["ok"] == true and res["socket_count"] == 3, "3rd socket drilled successfully") - - # 4th socket attempt rejected (MAX_SOCKETS = 3) - res = JewelrySocketSystem.drill_socket(necklace, diamond, true) - _assert(res["ok"] == false and res["reason"] == "SOCKETS_FULL", "Cannot drill more than 3 sockets") - -func _test_refined_gem_insertion_and_bonuses() -> void: - print("\n--- Test 3: Refined Gem Insertion & Tiered Bonuses ---") - var bracelet := { - "vnum": 14109, # White Gold Bracelet (14xxx) - "socket_count": 3, - "sockets": [0, 0, 0], - "gem_times": [0.0, 0.0, 0.0] - } - - var white_gold_gem := {"vnum": 50630} # White Gold (50630) - var wrong_gem := {"vnum": 50628} # Ebony (50628) - - # Mismatched gem rejected when required - var res = JewelrySocketSystem.insert_gem(bracelet, wrong_gem, 50630) - _assert(res["ok"] == false and res["reason"] == "GEM_MISMATCH", "Mismatched gem rejected") - - # Insert 1st gem (+10%) - res = JewelrySocketSystem.insert_gem(bracelet, white_gold_gem, 50630) - _assert(res["ok"] == true and res["slot"] == 0, "1st gem inserted into socket 0") - _assert(is_equal_approx(float(res["total_bonus_pct"]), 0.10), "1st gem bonus: +10%") - - # Insert 2nd gem (+10%, total +20%) - res = JewelrySocketSystem.insert_gem(bracelet, white_gold_gem, 50630) - _assert(res["ok"] == true and res["slot"] == 1, "2nd gem inserted into socket 1") - _assert(is_equal_approx(float(res["total_bonus_pct"]), 0.20), "2nd gem bonus: total +20%") - - # Insert 3rd gem (+20%, total +40%) - res = JewelrySocketSystem.insert_gem(bracelet, white_gold_gem, 50630) - _assert(res["ok"] == true and res["slot"] == 2, "3rd gem inserted into socket 2") - _assert(is_equal_approx(float(res["total_bonus_pct"]), 0.40), "3rd gem bonus: total +40%") - - # Full sockets reject further insertion - res = JewelrySocketSystem.insert_gem(bracelet, white_gold_gem, 50630) - _assert(res["ok"] == false and res["reason"] == "NO_EMPTY_SOCKET", "Cannot insert into full sockets") - -func _test_endurance_decay_and_expiration() -> void: - print("\n--- Test 4: Endurance Decay & Gem Expiration ---") - var item := { - "vnum": 17109, - "socket_count": 1, - "sockets": [50628, 0, 0], - "gem_times": [10.0, 0.0, 0.0] # 10 seconds remaining - } - - _assert(JewelrySocketSystem.count_active_gems(item) == 1, "Initially 1 active gem") - - # Decay by 5 seconds - var res = JewelrySocketSystem.update_gem_endurance(item, 5.0) - _assert(res["active_gems"] == 1, "Still 1 active gem after 5s decay") - _assert(res["expired"].is_empty(), "No gems expired yet") - - # Decay by another 6 seconds -> should expire - res = JewelrySocketSystem.update_gem_endurance(item, 6.0) - _assert(res["active_gems"] == 0, "0 active gems after expiration") - _assert(res["expired"] == [50628], "Gem 50628 expired and recorded") - _assert(item["sockets"][0] == 0, "Socket 0 cleared and available for re-insertion") - -func _finish() -> void: - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - quit(0) - else: - printerr("SOME TESTS FAILED!\n") - quit(1) diff --git a/project/test_jewelry_socket_parity.gd.uid b/project/test_jewelry_socket_parity.gd.uid deleted file mode 100644 index 64630cb3..00000000 --- a/project/test_jewelry_socket_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dyjtapy34gqbl diff --git a/project/test_lucky_lottery_fortune_parity.gd b/project/test_lucky_lottery_fortune_parity.gd deleted file mode 100644 index 9f428176..00000000 --- a/project/test_lucky_lottery_fortune_parity.gd +++ /dev/null @@ -1,142 +0,0 @@ -# test_lucky_lottery_fortune_parity.gd —— Metin2 40250 幸运乐透与老妪占卜转运系统 1:1 测试套件 -extends SceneTree - -const LuckyLotteryFortuneSystem = preload("res://lucky_lottery_fortune_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_lucky_lottery_fortune_parity (Direction 4: Lucky Lottery & Fortune Telling 1:1) ===") - - test_fortune_telling_rules() - test_bad_luck_and_talisman() - test_lottery_scratch_tickets() - test_fortune_update_and_expiry() - test_history_and_serialization() - - print("\n=======================================================") - print("Lucky Lottery Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - - if _failed > 0: - quit(1) - else: - quit(0) - -func test_fortune_telling_rules() -> void: - print("\n--- Test 1: Old Woman Fortune Telling (40250 1:1) ---") - var system = LuckyLotteryFortuneSystem.new() - - # 等级不足 20 级拦截 - var res = system.tell_fortune(18, 50000) - _assert(res["ok"] == false, "Rejected Lv 18 character") - _assert(res["reason"] == "LEVEL_TOO_LOW", "Reason is LEVEL_TOO_LOW") - - # 金币不足 7777 拦截 - res = system.tell_fortune(25, 3000) - _assert(res["ok"] == false, "Rejected insufficient gold") - _assert(res["reason"] == "NOT_ENOUGH_GOLD", "Reason is NOT_ENOUGH_GOLD") - - # 占卜成功:大吉 - res = system.tell_fortune(25, 10000, "great_luck") - _assert(res["ok"] == true, "Fortune told successfully") - _assert(res["tier"] == "great_luck", "Tier is great_luck") - _assert(res["exp_bonus"] == 0.10, "+10% EXP bonus") - _assert(res["gold_bonus"] == 0.10, "+10% Gold drop bonus") - _assert(res["remaining_gold"] == 2223, "Deducted 7777 gold (10000 - 7777 = 2223)") - -func test_bad_luck_and_talisman() -> void: - print("\n--- Test 2: Bad Luck & Karma Talisman (40250 1:1) ---") - var system = LuckyLotteryFortuneSystem.new() - - # 占卜得到大凶 - var res = system.tell_fortune(30, 20000, "bad_luck") - _assert(res["ok"] == true, "Told bad luck") - _assert(res["tier"] == "bad_luck", "Current tier is bad_luck") - _assert(res["can_buy_talisman"] == true, "Old woman offers talisman") - - # 购买辟邪转运符 (70052) - var inv: Array = [] - var buy_res = system.buy_karma_talisman(inv, 20000) - _assert(buy_res["ok"] == true, "Purchased talisman") - _assert(buy_res["talisman_vnum"] == 70052, "Talisman item vnum is 70052") - _assert(inv.size() == 1 and inv[0]["vnum"] == 70052, "Talisman added to inventory") - - # 运势转危为安,大凶转化为中吉 - var active = system.get_active_fortune_bonus() - _assert(active["tier"] == "good_luck", "Tier converted to good_luck") - _assert(active["gold_bonus"] > 0.0, "Gold penalty cleared into positive bonus") - -func test_lottery_scratch_tickets() -> void: - print("\n--- Test 3: Scratch Lottery Tickets (40250 1:1) ---") - var system = LuckyLotteryFortuneSystem.new() - - var inv: Array = [ - {"vnum": 50027, "count": 2}, # 彩票 x2 (slot 0) - {"vnum": 50027, "count": 1}, # 彩票 x1 (slot 1) - {"vnum": 50027, "count": 1} # 彩票 x1 (slot 2) - ] - - # 1. 刮出三同号 [7, 7, 7] -> 特等大奖 (1,000,000 金币 + 祝福卷轴 25040) - var res = system.play_scratch_lottery(inv, 0, [7, 7, 7]) - _assert(res["ok"] == true, "Scratched lottery ticket") - _assert(res["prize_tier"] == "grand_jackpot", "Won grand jackpot!") - _assert(res["prize_gold"] == 1000000, "Prize gold is 1,000,000") - _assert(res["prize_item"] == 25040, "Prize item is Blessing Scroll (25040)") - _assert(inv[0]["count"] == 1, "Count decremented from 2 to 1") - # 检查背包是否发放了祝福卷轴 - var has_scroll := false - for it in inv: - if it != null and it.get("vnum", 0) == 25040: - has_scroll = true - _assert(has_scroll == true, "Blessing Scroll awarded into inventory") - - # 2. 刮出二同号 [8, 8, 3] -> 二等奖 (200,000 金币) - res = system.play_scratch_lottery(inv, 1, [8, 8, 3]) - _assert(res["prize_tier"] == "second_prize", "Won second prize") - _assert(res["prize_gold"] == 200000, "Prize gold is 200,000") - - # 3. 刮出单号 [1, 5, 9] -> 安慰奖 (30,000 金币) - res = system.play_scratch_lottery(inv, 2, [1, 5, 9]) - _assert(res["prize_tier"] == "consolation_prize", "Won consolation prize") - _assert(res["prize_gold"] == 30000, "Prize gold is 30,000") - -func test_fortune_update_and_expiry() -> void: - print("\n--- Test 4: Fortune Duration & Expiry ---") - var system = LuckyLotteryFortuneSystem.new() - system.tell_fortune(30, 10000, "great_luck") - - var active = system.get_active_fortune_bonus() - _assert(active["tier"] == "great_luck", "Active fortune exists") - - # 时间流逝 20000 秒 - system.update(20000.0) - _assert(system.fortune_time_remaining <= 8800.0, "Time remaining decremented") - - # 时间流逝剩余时间 (到期) - system.update(10000.0) - active = system.get_active_fortune_bonus() - _assert(active["tier"] == "", "Fortune expired and cleared") - -func test_history_and_serialization() -> void: - print("\n--- Test 5: History & Serialization ---") - var system = LuckyLotteryFortuneSystem.new() - var inv: Array = [{"vnum": 50027, "count": 1}] - system.play_scratch_lottery(inv, 0, [3, 3, 3]) - - _assert(system.lotto_history.size() == 1, "Recorded 1 lotto history") - var data = system.serialize() - - var restored = LuckyLotteryFortuneSystem.new() - restored.deserialize(data) - _assert(restored.lotto_history.size() == 1, "Restored 1 lotto history") - _assert(restored.lotto_history[0]["prize_tier"] == "grand_jackpot", "Restored grand jackpot tier") diff --git a/project/test_lucky_lottery_fortune_parity.gd.uid b/project/test_lucky_lottery_fortune_parity.gd.uid deleted file mode 100644 index 5f258b68..00000000 --- a/project/test_lucky_lottery_fortune_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://borlpaaoki1j4 diff --git a/project/test_mail_system_parity.gd b/project/test_mail_system_parity.gd deleted file mode 100644 index a809f0a9..00000000 --- a/project/test_mail_system_parity.gd +++ /dev/null @@ -1,179 +0,0 @@ -# test_mail_system_parity.gd —— 40250 游戏内邮箱系统 1:1 纯净回归测试套件 -extends SceneTree - -const MailSystem = preload("res://mail_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_mail_system_parity (Direction 4: In-Game Mailbox 1:1) ===") - _test_mail_sending_validation() - _test_mail_reading_and_unread_count() - _test_attachment_claiming() - _test_delete_protection() - _test_mail_expiry_cleanup() - _finish() - -func _test_mail_sending_validation() -> void: - print("\n--- Test 1: Mail Sending Validation & Postage (1000 Yang) ---") - var ms := MailSystem.new() - var player_data := {"gold": 500} # Less than postage 1000 - var inv: Array = [null] - - # Empty recipient - var r1 = ms.send_mail("PlayerA", "", "Title", "Body", player_data, inv) - _assert(r1["ok"] == false and r1["reason"] == "EMPTY_RECIPIENT", "Empty recipient rejected") - - # Insufficient Yang for postage - var r2 = ms.send_mail("PlayerA", "PlayerB", "Title", "Body", player_data, inv) - _assert(r2["ok"] == false and r2["reason"] == "INSUFFICIENT_YANG", "Blocked when gold < postage fee") - - # Locked item cannot be mailed - player_data["gold"] = 10000 - inv[0] = {"vnum": 10, "name": "木剑", "is_locked": true} - var r3 = ms.send_mail("PlayerA", "PlayerB", "Title", "Body", player_data, inv, 0) - _assert(r3["ok"] == false and r3["reason"] == "ITEM_LOCKED", "Locked item rejected for mailing") - - # Valid sending without attachment - var send_data := {"fired": false, "id": 0} - ms.mail_sent.connect(func(mid: int, _s: String, _r: String): - send_data["fired"] = true - send_data["id"] = mid - ) - - var r4 = ms.send_mail("PlayerA", "PlayerB", "问候信", "你好,这是测试邮件!", player_data, inv) - _assert(r4["ok"] == true, "Mail sent successfully") - _assert(player_data["gold"] == 9000, "1000 Yang postage deducted (10000 -> 9000)") - _assert(send_data["fired"] == true and send_data["id"] == 1, "mail_sent signal fired with id 1") - _assert(ms.get_unread_count("PlayerB") == 1, "PlayerB has 1 unread mail") - -func _test_mail_reading_and_unread_count() -> void: - print("\n--- Test 2: Mail Reading & Unread Counter ---") - var ms := MailSystem.new() - var player_data := {"gold": 10000} - var inv: Array = [null] - - ms.send_mail("Alice", "Bob", "Hello", "World", player_data, inv) - _assert(ms.get_unread_count("Bob") == 1, "Initial unread count is 1") - - var unread_signal := {"fired": false, "count": -1} - ms.unread_count_changed.connect(func(_rec: String, cnt: int): - unread_signal["fired"] = true - unread_signal["count"] = cnt - ) - - # Read mail - var res = ms.read_mail(1, "Bob") - _assert(res["ok"] == true, "Mail read successfully") - _assert(res["mail"]["is_read"] == true, "is_read is true") - _assert(ms.get_unread_count("Bob") == 0, "Unread count updated to 0") - _assert(unread_signal["fired"] == true and unread_signal["count"] == 0, "unread_count_changed signal fired with 0") - - # Reading non-existent mail - var r_fail = ms.read_mail(999, "Bob") - _assert(r_fail["ok"] == false and r_fail["reason"] == "MAIL_NOT_FOUND", "Non-existent mail returns error") - -func _test_attachment_claiming() -> void: - print("\n--- Test 3: Gold & Item Attachment Sealing and Claiming ---") - var ms := MailSystem.new() - var sender_data := {"gold": 100000} - var sender_inv: Array = [ - {"vnum": 11299, "name": "黑钢甲+9", "count": 1}, - null - ] - - # Send mail with 50,000 Yang and armor in slot 0 - var send_res = ms.send_mail( - "Sender", - "Receiver", - "珍贵赠礼", - "请收下这件黑钢甲和金币!", - sender_data, - sender_inv, - 0, # attach slot 0 - 50000 # attach 50,000 gold - ) - _assert(send_res["ok"] == true, "Mail with attachments sent") - _assert(sender_data["gold"] == 49000, "51,000 Yang deducted (50k gold + 1k postage)") - _assert(sender_inv[0] == null, "Attached item removed from sender inventory") - - # Receiver inbox - var receiver_data := {"gold": 10000} - var receiver_inv: Array = [null, null] - - var claim_signal := {"fired": false, "id": 0} - ms.attachment_claimed.connect(func(mid: int): - claim_signal["fired"] = true - claim_signal["id"] = mid - ) - - # Claim attachments - var claim_res = ms.claim_attachments(1, "Receiver", receiver_data, receiver_inv) - _assert(claim_res["ok"] == true, "Attachments claimed successfully") - _assert(claim_signal["fired"] == true and claim_signal["id"] == 1, "attachment_claimed signal fired") - _assert(receiver_data["gold"] == 60000, "Receiver gold increased by 50,000 (10k -> 60k)") - _assert(receiver_inv[0] != null and receiver_inv[0]["vnum"] == 11299, "Attached armor received into inventory") - - # Double claiming yields nothing - var claim_again = ms.claim_attachments(1, "Receiver", receiver_data, receiver_inv) - _assert(claim_again["claimed_gold"] == 0 and claim_again["claimed_item"] == null, "No attachments left to claim") - -func _test_delete_protection() -> void: - print("\n--- Test 4: Delete Protection on Unclaimed Attachments ---") - var ms := MailSystem.new() - var s_data := {"gold": 50000} - var s_inv: Array = [{"vnum": 10, "name": "木剑", "count": 1}] - - ms.send_mail("A", "B", "Item Mail", "Body", s_data, s_inv, 0, 10000) - - # Trying to delete before claiming attachments - var del_blocked = ms.delete_mail(1, "B") - _assert(del_blocked["ok"] == false and del_blocked["reason"] == "HAS_UNCLAIMED_ATTACHMENTS", "Deletion blocked while attachments unclaimed") - - # Claim attachments - var b_data := {"gold": 0} - var b_inv: Array = [null] - ms.claim_attachments(1, "B", b_data, b_inv) - - # Now deletion succeeds - var del_ok = ms.delete_mail(1, "B") - _assert(del_ok["ok"] == true, "Mail deleted after attachments claimed") - _assert(ms.get_inbox("B").is_empty(), "B inbox is now empty") - -func _test_mail_expiry_cleanup() -> void: - print("\n--- Test 5: 30-Day Expiry Cleanup ---") - var ms := MailSystem.new() - var s_data := {"gold": 10000} - var s_inv: Array = [null] - - # Send mail at time 0 - ms.send_mail("A", "B", "Old Mail", "Body", s_data, s_inv, -1, 0, 0.0) - _assert(ms.get_inbox("B").size() == 1, "1 mail exists") - - # Clean up at day 15 (15 * 86400 = 1296000) -> not expired - var c1 = ms.cleanup_expired_mails(1296000.0) - _assert(c1 == 0, "0 mails cleaned at 15 days") - _assert(ms.get_inbox("B").size() == 1, "Mail still intact") - - # Clean up at day 31 (31 * 86400 = 2678400) -> expired! - var c2 = ms.cleanup_expired_mails(2678400.0) - _assert(c2 == 1, "1 mail cleaned after 30 days") - _assert(ms.get_inbox("B").size() == 0, "Expired mail was removed") - -func _finish() -> void: - print("\n==========================================") - print("Mail System Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_mail_system_parity.gd.uid b/project/test_mail_system_parity.gd.uid deleted file mode 100644 index 8cd103ed..00000000 --- a/project/test_mail_system_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://7qtp70l3r41l diff --git a/project/test_mall_affect_parity.gd b/project/test_mall_affect_parity.gd deleted file mode 100644 index c96b2884..00000000 --- a/project/test_mall_affect_parity.gd +++ /dev/null @@ -1,118 +0,0 @@ -# test_mall_affect_parity.gd —— 40250 商城常驻 Affect 增益与双倍特权 1:1 纯净回归测试套件 -extends SceneTree - -const MallAffectSystem = preload("res://mall_affect_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_mall_affect_parity (Direction 4: Mall Affects 1:1) ===") - _test_mall_item_activation() - _test_multipliers_and_privileges() - _test_hud_icon_formatting() - _test_decay_and_expiration() - _finish() - -func _test_mall_item_activation() -> void: - print("\n--- Test 1: Mall Item Activation & Affect Table ---") - var mas := MallAffectSystem.new() - - var non_mall := {"vnum": 10} - var res = mas.use_mall_item(non_mall) - _assert(res["ok"] == false and res["reason"] == "NOT_MALL_ITEM", "Non-mall item rejected") - - # EXP Ring 1 hour (70005) - var exp_ring_1h := {"vnum": 70005} - res = mas.use_mall_item(exp_ring_1h) - _assert(res["ok"] == true, "EXP Ring 70005 activated") - _assert(mas.has_affect(MallAffectSystem.AFFECT_EXP_BONUS), "AFFECT_EXP_BONUS is active") - _assert(mas.active_affects[MallAffectSystem.AFFECT_EXP_BONUS]["duration"] == 3600.0, "Duration is 3600s (1h)") - - # Thief Glove (70043) - var thief_glove := {"vnum": 70043} - res = mas.use_mall_item(thief_glove) - _assert(res["ok"] == true, "Thief Glove 70043 activated") - _assert(mas.has_affect(MallAffectSystem.AFFECT_ITEM_BONUS), "AFFECT_ITEM_BONUS is active") - - # Lucky Gold Coin (70044) - var gold_coin := {"vnum": 70044} - res = mas.use_mall_item(gold_coin) - _assert(res["ok"] == true, "Lucky Gold Coin 70044 activated") - _assert(mas.has_affect(MallAffectSystem.AFFECT_GOLD_BONUS), "AFFECT_GOLD_BONUS is active") - - # Safebox expansion ticket (72006) - var safebox_ticket := {"vnum": 72006} - res = mas.use_mall_item(safebox_ticket) - _assert(res["ok"] == true, "Safebox ticket 72006 activated") - _assert(mas.has_safebox_expansion() == true, "Safebox expansion granted") - -func _test_multipliers_and_privileges() -> void: - print("\n--- Test 2: Game Multipliers (EXP, Drop, Gold) ---") - var mas := MallAffectSystem.new() - - # Defaults - _assert(is_equal_approx(mas.get_exp_multiplier(), 1.0), "Default EXP multiplier is 1.0x") - _assert(is_equal_approx(mas.get_drop_multiplier(), 1.0), "Default Drop multiplier is 1.0x") - _assert(is_equal_approx(mas.get_gold_multiplier(), 1.0), "Default Gold multiplier is 1.0x") - - # Activate 50% EXP ring - mas.use_mall_item({"vnum": 70005}) # value = 50 - _assert(is_equal_approx(mas.get_exp_multiplier(), 1.5), "50% EXP Ring gives 1.5x EXP") - - # Activate 100% EXP ring (72001) - mas.use_mall_item({"vnum": 72001}) # value = 100 - _assert(is_equal_approx(mas.get_exp_multiplier(), 2.0), "100% EXP Ring gives 2.0x EXP") - - # Activate Thief Glove (70043) - mas.use_mall_item({"vnum": 70043}) - _assert(is_equal_approx(mas.get_drop_multiplier(), 2.0), "Thief Glove gives 2.0x Drop Rate") - - # Activate Gold Coin (70044) - mas.use_mall_item({"vnum": 70044}) - _assert(is_equal_approx(mas.get_gold_multiplier(), 2.0), "Lucky Gold Coin gives 2.0x Gold Amount") - -func _test_hud_icon_formatting() -> void: - print("\n--- Test 3: HUD Icon & Official Sub Path Formatting ---") - var mas := MallAffectSystem.new() - mas.use_mall_item({"vnum": 70005}) # 1 hour = 3600s -> "01:00:00" - - var icons: Array[Dictionary] = mas.get_hud_icons() - _assert(icons.size() == 1, "1 icon in HUD list") - var ic: Dictionary = icons[0] - _assert(ic["affect_type"] == MallAffectSystem.AFFECT_EXP_BONUS, "Affect type is EXP_BONUS") - _assert(ic["icon_sub"] == "d:/ymir work/ui/skill/common/affect/exp_bonus.sub", "Official 40250 sub texture matched") - _assert(ic["time_remaining_str"] == "01:00:00", "Remaining time formatted as 01:00:00") - -func _test_decay_and_expiration() -> void: - print("\n--- Test 4: Duration Decay & Expiration ---") - var mas := MallAffectSystem.new() - mas.add_affect(MallAffectSystem.AFFECT_AUTOLOOT, 10.0) # 10 seconds - _assert(mas.has_affect(MallAffectSystem.AFFECT_AUTOLOOT), "Autoloot active") - - # Decay 5s - var expired = mas.update(5.0) - _assert(expired.is_empty(), "No expiration after 5s") - _assert(mas.has_affect(MallAffectSystem.AFFECT_AUTOLOOT), "Still active") - - # Decay 6s -> expire - expired = mas.update(6.0) - _assert(expired == [MallAffectSystem.AFFECT_AUTOLOOT], "Autoloot expired") - _assert(mas.has_affect(MallAffectSystem.AFFECT_AUTOLOOT) == false, "Autoloot no longer active") - -func _finish() -> void: - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - quit(0) - else: - printerr("SOME TESTS FAILED!\n") - quit(1) diff --git a/project/test_mall_affect_parity.gd.uid b/project/test_mall_affect_parity.gd.uid deleted file mode 100644 index 97a3ee38..00000000 --- a/project/test_mall_affect_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d20mb502elamt diff --git a/project/test_map_name_shower_parity.gd b/project/test_map_name_shower_parity.gd deleted file mode 100644 index 5e85cf7d..00000000 --- a/project/test_map_name_shower_parity.gd +++ /dev/null @@ -1,121 +0,0 @@ -# test_map_name_shower_parity.gd -# 检验 40250 地图横幅与魔塔/恶魔陵墓楼层识别及渐显时间轴 -extends SceneTree - -const MapNameShowerSystem = preload("res://map_name_shower_system.gd") - -func _init() -> void: - print("--- 开始测试 40250 地图横幅与魔塔/陵墓楼层识别系统 (test_map_name_shower_parity) ---") - var shower = MapNameShowerSystem.new() - var fails: int = 0 - - # 1. 普通野外/王城地图加载 - var ok = shower.show_map_name("metin2_map_a1", 1000.0, 2000.0, 0.0) - if not ok: - printerr("FAIL: metin2_map_a1 横幅未成功触发!") - fails += 1 - if shower.current_image_path != "locale/ui/mapname/a1.tga": - printerr("FAIL: a1 横幅路径不符: ", shower.current_image_path) - fails += 1 - if shower.current_floor_number != 0 or shower.pos_x != 0: - printerr("FAIL: 普通地图不应有楼层号偏移: ", shower.current_floor_number, " pos_x: ", shower.pos_x) - fails += 1 - - # 2. 魔塔 (metin2_map_deviltower1) 9 层空间识别检验 - # 40250 官方坐标: - # 1F: x(10000..25000), y(58000..72000) -> 选取 (15000, 65000) - # 3F: x(10000..25000), y(10000..25000) -> 选取 (18000, 18000) - # 6F: x(14000..43500), y(14000..24500) -> 选取 (30000, 20000) - # 9F: x(56000..68000), y(13000..23000) -> 选取 (60000, 18000) - shower.show_map_name("metin2_map_deviltower1", 15000.0, 65000.0, 0.0) - if shower.current_floor_number != 1: - printerr("FAIL: 恶魔之塔 1 层坐标判定错误,得到: ", shower.current_floor_number) - fails += 1 - if shower.current_floor_image != "locale/ui/mapname/devil1_1f.tga": - printerr("FAIL: 1层楼层横幅图片不符: ", shower.current_floor_image) - fails += 1 - if shower.pos_x != -60: - printerr("FAIL: 恶魔之塔主横幅 X 轴偏移应为 -60,实际: ", shower.pos_x) - fails += 1 - - var f3 = shower.get_devil_tower_floor(18000.0, 18000.0) - if f3 != 3: - printerr("FAIL: 恶魔之塔 3 层空间判定错误: ", f3) - fails += 1 - - var f6 = shower.get_devil_tower_floor(30000.0, 20000.0) - if f6 != 6: - printerr("FAIL: 恶魔之塔 6 层空间判定错误: ", f6) - fails += 1 - - var f9 = shower.get_devil_tower_floor(60000.0, 18000.0) - if f9 != 9: - printerr("FAIL: 恶魔之塔 9 层空间判定错误: ", f9) - fails += 1 - - # 3. 恶魔陵墓 (metin2_map_devilsCatacomb) 7 层空间识别检验 - # 1F: x(3000..45000), y(4500..45000) -> (20000, 20000) - # 4F: x(3100..56400), y(54100..105800) -> (10000, 80000) - # 7F: x(5000..15000), y(104900..122000) -> (10000, 115000) - shower.show_map_name("metin2_map_devilsCatacomb", 20000.0, 20000.0, 0.0) - if shower.current_floor_number != 1 or shower.pos_x != -75: - printerr("FAIL: 恶魔地下陵墓 1 层判定错误,层: ", shower.current_floor_number, " pos_x: ", shower.pos_x) - fails += 1 - - var b4 = shower.get_devil_catacomb_base(10000.0, 80000.0) - if b4 != 4: - printerr("FAIL: 恶魔地下陵墓 4 层判定错误: ", b4) - fails += 1 - - var b7 = shower.get_devil_catacomb_base(10000.0, 115000.0) - if b7 != 7: - printerr("FAIL: 恶魔地下陵墓 7 层判定错误: ", b7) - fails += 1 - - # 4. 渐变状态机时间轴演进验证 - shower.show_map_name("metin2_map_b1", 500.0, 500.0, 10.0) - if shower.state != MapNameShowerSystem.State.STATE_FADE_IN: - printerr("FAIL: 初始状态应为 STATE_FADE_IN!") - fails += 1 - # 10.5 秒 (处于 1.0s 延迟内),alpha 保持 0 - shower.update_step(10.5) - if shower.cur_alpha != 0.0: - printerr("FAIL: 延迟期间 alpha 应为 0: ", shower.cur_alpha) - fails += 1 - - # 11.0 秒以后开始渐显,每次增加 0.05 - for i in range(18): - shower.update_step(11.0 + i * 0.05) - if shower.cur_alpha < 0.9: - printerr("FAIL: 渐变后 alpha 应达到或超过 0.9,实际: ", shower.cur_alpha) - fails += 1 - if shower.state != MapNameShowerSystem.State.STATE_SHOW: - printerr("FAIL: 达到 0.9 后应进入 STATE_SHOW: ", shower.state) - fails += 1 - - # 停留 5.0 秒检验 - var hold_start = 11.0 + 18 * 0.05 - shower.update_step(hold_start + 2.0) - if shower.state != MapNameShowerSystem.State.STATE_SHOW: - printerr("FAIL: 驻留期间应保持 STATE_SHOW: ", shower.state) - fails += 1 - - # 停留超时后转入 FADE_OUT - shower.update_step(hold_start + 5.1) - if shower.state != MapNameShowerSystem.State.STATE_FADE_OUT: - printerr("FAIL: 驻留超时后应转入 STATE_FADE_OUT: ", shower.state) - fails += 1 - - # 模拟淡出至 0 - for i in range(25): - shower.update_step(hold_start + 5.1 + (i + 1) * 0.1) - if shower.state != MapNameShowerSystem.State.STATE_HIDE or shower.cur_alpha != 0.0: - printerr("FAIL: 淡出完成后应转入 STATE_HIDE,实际: ", shower.state, " alpha: ", shower.cur_alpha) - fails += 1 - - if fails == 0: - print("PASS: test_map_name_shower_parity (10/10 全部通过)") - quit(0) - else: - printerr("FAILED: test_map_name_shower_parity (", fails, " 项失败)") - quit(1) diff --git a/project/test_map_name_shower_parity.gd.uid b/project/test_map_name_shower_parity.gd.uid deleted file mode 100644 index 15fe13e2..00000000 --- a/project/test_map_name_shower_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bt6sgoolw7ypl diff --git a/project/test_marriage_parity.gd b/project/test_marriage_parity.gd deleted file mode 100644 index 1073e610..00000000 --- a/project/test_marriage_parity.gd +++ /dev/null @@ -1,137 +0,0 @@ -# test_marriage_parity.gd —— 40250 结婚戒指与伴侣瞬移传送 1:1 纯净回归测试套件 -extends SceneTree - -const MarriageSystem = preload("res://marriage_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_marriage_parity (Direction 4: Marriage & Couple Warp 1:1) ===") - _test_marriage_qualifications_and_ring() - _test_love_points_growth() - _test_couple_warp_restrictions() - _test_successful_warp_and_cooldown() - _test_divorce() - _finish() - -func _test_marriage_qualifications_and_ring() -> void: - print("\n--- Test 1: Marriage Prerequisites & Ring Inscription ---") - var ms := MarriageSystem.new() - var ring := {"vnum": 70301} - var non_ring := {"vnum": 10} - - # Level 20 rejected (requires 25) - var res = ms.marry("Hero", "Alice", ring, 20) - _assert(res["ok"] == false and res["reason"] == "LEVEL_TOO_LOW", "Level < 25 rejected") - - # Non wedding ring rejected - res = ms.marry("Hero", "Alice", non_ring, 30) - _assert(res["ok"] == false and res["reason"] == "NO_WEDDING_RING", "Non-wedding ring rejected") - - # Valid marriage succeeds - res = ms.marry("Hero", "Alice", ring, 30) - _assert(res["ok"] == true, "Marriage ceremony succeeds") - _assert(ms.is_married == true, "Player is now married") - _assert(ms.spouse_name == "Alice", "Spouse name is Alice") - _assert(ring["inscribed_name"] == "Alice", "Ring inscribed with partner's name") - _assert(ms.love_points == 50, "Initial love points is 50%") - -func _test_love_points_growth() -> void: - print("\n--- Test 2: Love Points Growth ---") - var ms := MarriageSystem.new() - var ring := {"vnum": 70301} - ms.marry("Hero", "Alice", ring, 30) - - ms.add_love_points(30) - _assert(ms.love_points == 80, "Love points increased to 80%") - - # Clamped to 100% - ms.add_love_points(50) - _assert(ms.love_points == 100, "Love points clamped at 100%") - -func _test_couple_warp_restrictions() -> void: - print("\n--- Test 3: Couple Warp Restrictions ---") - var ms := MarriageSystem.new() - var spouse_loc := {"is_online": true, "map": "metin2_map_a1", "pos": Vector3(100, 0, 100)} - - # Unmarried player rejected - var res = ms.warp_to_spouse(100, spouse_loc, false) - _assert(res["ok"] == false and res["reason"] == "NOT_MARRIED", "Unmarried player cannot warp") - - # Marry - var ring := {"vnum": 70301} - ms.marry("Hero", "Alice", ring, 30) - - # Dead player rejected - res = ms.warp_to_spouse(100, spouse_loc, true) - _assert(res["ok"] == false and res["reason"] == "PLAYER_DEAD", "Dead player cannot warp") - - # Not enough SP (< 50) - res = ms.warp_to_spouse(30, spouse_loc, false) - _assert(res["ok"] == false and res["reason"] == "NOT_ENOUGH_SP", "Fails when SP < 50") - - # Offline spouse rejected - res = ms.warp_to_spouse(100, {"is_online": false}, false) - _assert(res["ok"] == false and res["reason"] == "SPOUSE_OFFLINE", "Offline spouse rejected") - - # Forbidden zone (Devil Tower F9) rejected - var forbidden_loc := {"is_online": true, "map": "deviltower_f9", "pos": Vector3(50, 0, 50)} - res = ms.warp_to_spouse(100, forbidden_loc, false) - _assert(res["ok"] == false and res["reason"] == "FORBIDDEN_ZONE", "Warp into Devil Tower F9 blocked") - -func _test_successful_warp_and_cooldown() -> void: - print("\n--- Test 4: Successful Couple Warp & Cooldown ---") - var ms := MarriageSystem.new() - var ring := {"vnum": 70301} - ms.marry("Hero", "Alice", ring, 30) - - var spouse_loc := {"is_online": true, "map": "metin2_map_b1", "pos": Vector3(250, 10, 320)} - var warped := [false] - ms.couple_warped.connect(func(m: String, p: Vector3): warped[0] = true) - - var res = ms.warp_to_spouse(80, spouse_loc, false) - _assert(res["ok"] == true, "Warp request succeeded") - _assert(warped[0] == true, "couple_warped signal emitted") - _assert(res["sp_cost"] == 50, "50 SP consumed") - _assert(res["target_map"] == "metin2_map_b1", "Target map is metin2_map_b1") - _assert(res["target_pos"] == Vector3(250, 10, 320), "Target position accurately resolved") - _assert(ms.warp_cooldown == 180.0, "Warp entered 180s cooldown") - - # Second warp immediately blocked by cooldown - res = ms.warp_to_spouse(80, spouse_loc, false) - _assert(res["ok"] == false and res["reason"] == "COOLDOWN_ACTIVE", "Second warp blocked by 180s cooldown") - - # Fast forward 181 seconds - ms.update(181.0) - _assert(ms.warp_cooldown == 0.0, "Cooldown expired after 181s") - -func _test_divorce() -> void: - print("\n--- Test 5: Divorce ---") - var ms := MarriageSystem.new() - var ring := {"vnum": 70301} - ms.marry("Hero", "Alice", ring, 30) - _assert(ms.is_married == true, "Married initially") - - var res = ms.divorce() - _assert(res["ok"] == true, "Divorce completed") - _assert(ms.is_married == false, "No longer married") - _assert(ms.spouse_name.is_empty(), "Spouse name cleared") - _assert(ms.love_points == 0, "Love points reset to 0") - -func _finish() -> void: - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - quit(0) - else: - printerr("SOME TESTS FAILED!\n") - quit(1) diff --git a/project/test_marriage_parity.gd.uid b/project/test_marriage_parity.gd.uid deleted file mode 100644 index 3e6260c3..00000000 --- a/project/test_marriage_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cws8pneyu1d86 diff --git a/project/test_marriage_wedding_love_parity.gd b/project/test_marriage_wedding_love_parity.gd deleted file mode 100644 index d50dfaeb..00000000 --- a/project/test_marriage_wedding_love_parity.gd +++ /dev/null @@ -1,124 +0,0 @@ -# test_marriage_wedding_love_parity.gd —— Metin2 40250 夫妻同心传送与爱情点数婚戒系统 1:1 测试套件 -extends SceneTree - -const MarriageWeddingLoveSystem = preload("res://marriage_wedding_love_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_marriage_wedding_love_parity (Direction 2: Marriage & Wedding Ring 1:1) ===") - - test_marriage_and_partner_query() - test_love_points_and_items_bonus() - test_wedding_ring_teleport() - test_divorce_and_cleanup() - test_serialization() - - print("\n=======================================================") - print("Marriage & Wedding Ring Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - - if _failed > 0: - quit(1) - else: - quit(0) - -func test_marriage_and_partner_query() -> void: - print("\n--- Test 1: Marriage Creation & Querying (40250 1:1) ---") - var system = MarriageWeddingLoveSystem.new() - - _assert(system.is_married(1001) == false, "Not married initially") - - # 缔结婚姻 - var cid = system.marry(1001, "剑魂", 2002, "雪儿") - _assert(cid > 0, "Created marriage couple") - _assert(system.is_married(1001) == true, "Player 1 is married") - _assert(system.is_married(2002) == true, "Player 2 is married") - _assert(system.get_partner_pid(1001) == 2002, "Player 1 partner is 2002") - _assert(system.get_partner_pid(2002) == 1001, "Player 2 partner is 1001") - _assert(system.get_partner_name(1001) == "雪儿", "Player 1 partner name is 雪儿") - _assert(system.get_partner_name(2002) == "剑魂", "Player 2 partner name is 剑魂") - -func test_love_points_and_items_bonus() -> void: - print("\n--- Test 2: Love Points & Jewelry Scaling (40250 1:1) ---") - var system = MarriageWeddingLoveSystem.new() - system.marry(1001, "剑魂", 2002, "雪儿") - - _assert(system.get_love_points(1001) == 50, "Initial love points 50%") - - # 增加爱情点数至 100% - system.add_love_points(1001, 50) - _assert(system.get_love_points(1001) == 100, "Love points maxed to 100%") - - # 爱情饰品 100% 加成 - var b_atk = system.get_love_item_bonus(1001, MarriageWeddingLoveSystem.VNUM_BRACELET_OF_LOVE) - _assert(b_atk["bonus_type"] == "att_grade" and b_atk["value"] == 40, "Bracelet of Love gives +40 ATK at 100% love") - - var b_def = system.get_love_item_bonus(1001, MarriageWeddingLoveSystem.VNUM_EARRINGS_OF_LOVE) - _assert(b_def["bonus_type"] == "def_grade" and b_def["value"] == 30, "Earrings of Love give +30 DEF at 100% love") - - var b_crit = system.get_love_item_bonus(1001, MarriageWeddingLoveSystem.VNUM_NECKLACE_OF_LOVE) - _assert(b_crit["bonus_type"] == "crit_pct" and b_crit["value"] == 20, "Necklace of Love gives +20% Crit at 100% love") - -func test_wedding_ring_teleport() -> void: - print("\n--- Test 3: Wedding Ring (70302) Teleportation (40250 1:1) ---") - var system = MarriageWeddingLoveSystem.new() - system.marry(1001, "剑魂", 2002, "雪儿") - system.add_love_points(1001, 50) # 100% 爱情点数 -> 消耗 50 SP - - var target_pos = Vector2(1500.0, 3200.0) - - # 1. 伴侣离线 -> 拦截 - var res = system.use_wedding_ring(1001, 500, false, "谷地", target_pos) - _assert(res["ok"] == false, "Rejected when partner offline") - _assert(res["reason"] == "PARTNER_OFFLINE", "Reason is PARTNER_OFFLINE") - - # 2. 伴侣处于竞技场 (IsArenaMap) -> 拦截 - res = system.use_wedding_ring(1001, 500, true, "竞技场", target_pos, true) - _assert(res["ok"] == false, "Rejected when partner in arena") - _assert(res["reason"] == "PARTNER_IN_ARENA", "Reason is PARTNER_IN_ARENA") - - # 3. SP 不足 -> 拦截 (需要 50 SP,玩家只有 30) - res = system.use_wedding_ring(1001, 30, true, "首阳山", target_pos, false) - _assert(res["ok"] == false, "Rejected insufficient SP") - _assert(res["reason"] == "NOT_ENOUGH_SP", "Reason is NOT_ENOUGH_SP") - - # 4. 传送成功 - res = system.use_wedding_ring(1001, 200, true, "首阳山", target_pos, false) - _assert(res["ok"] == true, "Teleport succeeded") - _assert(res["target_pos"] == target_pos, "Target position matches partner position") - _assert(res["sp_consumed"] == 50, "Consumed 50 SP at 100% love points") - _assert(res["remaining_sp"] == 150, "Remaining SP is 150") - -func test_divorce_and_cleanup() -> void: - print("\n--- Test 4: Divorce & State Cleanup (40250 1:1) ---") - var system = MarriageWeddingLoveSystem.new() - system.marry(1001, "剑魂", 2002, "雪儿") - - var div_ok = system.divorce(1001) - _assert(div_ok == true, "Divorce succeeded") - _assert(system.is_married(1001) == false, "Player 1 is no longer married") - _assert(system.is_married(2002) == false, "Player 2 is no longer married") - -func test_serialization() -> void: - print("\n--- Test 5: Serialization & Restoration ---") - var system = MarriageWeddingLoveSystem.new() - system.marry(1001, "剑魂", 2002, "雪儿") - system.add_love_points(1001, 30) - - var data = system.serialize() - _assert(data.has("marriages") and data.has("pid_to_couple"), "Serialized data") - - var restored = MarriageWeddingLoveSystem.new() - restored.deserialize(data) - _assert(restored.is_married(1001) == true, "Restored marriage state") - _assert(restored.get_love_points(1001) == 80, "Restored love points 80") diff --git a/project/test_marriage_wedding_love_parity.gd.uid b/project/test_marriage_wedding_love_parity.gd.uid deleted file mode 100644 index 42700465..00000000 --- a/project/test_marriage_wedding_love_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://i5ky01gkki3k diff --git a/project/test_mercenary_companion_parity.gd b/project/test_mercenary_companion_parity.gd deleted file mode 100644 index 449743f6..00000000 --- a/project/test_mercenary_companion_parity.gd +++ /dev/null @@ -1,156 +0,0 @@ -# test_mercenary_companion_parity.gd —— 40250 单人单机假人/陪练佣兵助战 1:1 回归测试套件 -extends SceneTree - -const MercenaryCompanionSystem = preload("res://mercenary_companion_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_mercenary_companion_parity (Direction 2: Mercenary Companion 1:1) ===") - _test_summon_and_stats() - _test_buff_and_healing() - _test_assist_combat_and_stances() - _test_teleport_and_movement() - _test_downed_and_revival() - _finish() - -func _test_summon_and_stats() -> void: - print("\n--- Test 1: Summoning & Level Scaling ---") - var merc := MercenaryCompanionSystem.new() - - var summon_sig := {"fired": false, "type": 0, "name": ""} - merc.mercenary_summoned.connect(func(t: int, n: String): - summon_sig["fired"] = true - summon_sig["type"] = t - summon_sig["name"] = n - ) - - # Level 50 Shaman - var res = merc.summon_mercenary(MercenaryCompanionSystem.MercenaryType.SHAMAN_HEALER, Vector3(10, 0, 10), 50) - _assert(res["ok"] == true, "Shaman summoned") - _assert(summon_sig["fired"] == true and summon_sig["type"] == 1, "mercenary_summoned signal emitted") - _assert(merc.is_active == true and merc.is_alive == true, "Merc is active and alive") - _assert(merc.level == 50, "Mercenary level is 50") - # Level 50 growth: 1 + 49 * 0.05 = 3.45x base_hp (8000 * 3.45 = 27600) - _assert(merc.max_hp == int(8000 * 3.45), "HP properly scaled by level") - - # Dismiss - var dis_sig := {"fired": false} - merc.mercenary_dismissed.connect(func(): dis_sig["fired"] = true) - var dis_res = merc.dismiss_mercenary() - _assert(dis_res["ok"] == true, "Merc dismissed") - _assert(dis_sig["fired"] == true, "mercenary_dismissed signal emitted") - _assert(merc.is_active == false, "Merc is inactive") - -func _test_buff_and_healing() -> void: - print("\n--- Test 2: Shaman Buffs & Smart Emergency Healing ---") - var merc := MercenaryCompanionSystem.new() - merc.summon_mercenary(MercenaryCompanionSystem.MercenaryType.SHAMAN_HEALER, Vector3.ZERO, 30) - - var buffs_received := [] - merc.buff_applied_to_player.connect(func(bname: String, dur: float, val: float): - buffs_received.append({"name": bname, "duration": dur, "value": val}) - ) - - var heal_received := {"fired": false, "amount": 0} - merc.heal_applied_to_player.connect(func(amt: int): - heal_received["fired"] = true - heal_received["amount"] = amt - ) - - # Update when player is healthy (10000 / 10000) - var up1 = merc.update_mercenary(0.1, Vector3.ZERO, 10000, 10000) - _assert(buffs_received.size() == 2, "Shaman applied 2 buffs (Blessing & Dragon's Aid)") - _assert(heal_received["fired"] == false, "No heal cast when player is healthy") - - # Player HP drops to 40% (4000 / 10000 < 60%) - var up2 = merc.update_mercenary(0.1, Vector3.ZERO, 4000, 10000) - _assert(up2["status"] == "cast_heal", "Shaman triggered emergency heal") - _assert(heal_received["fired"] == true, "heal_applied_to_player signal fired") - _assert(heal_received["amount"] == 3500, "Healed 35% of max HP (3500)") - - # Second hit while heal is on cooldown should not retrigger heal - heal_received["fired"] = false - var up3 = merc.update_mercenary(0.1, Vector3.ZERO, 3000, 10000) - _assert(heal_received["fired"] == false, "Heal respects cooldown") - -func _test_assist_combat_and_stances() -> void: - print("\n--- Test 3: Stances & Warrior Assist Combat ---") - var merc := MercenaryCompanionSystem.new() - merc.summon_mercenary(MercenaryCompanionSystem.MercenaryType.WARRIOR_TANK, Vector3.ZERO, 50) - - # Check stance change - var st_res = merc.set_stance(MercenaryCompanionSystem.Stance.FOLLOW) - _assert(st_res["ok"] == true and merc.current_stance == MercenaryCompanionSystem.Stance.FOLLOW, "Stance set to FOLLOW") - - # Change to ASSIST - merc.set_stance(MercenaryCompanionSystem.Stance.ASSIST) - _assert(merc.current_stance == MercenaryCompanionSystem.Stance.ASSIST, "Stance set to ASSIST") - - var atk_sig := {"fired": false, "tgt": "", "dmg": 0} - merc.mercenary_attacked.connect(func(tgt: String, dmg: int): - atk_sig["fired"] = true - atk_sig["tgt"] = tgt - atk_sig["dmg"] = dmg - ) - - # Target at (1.5, 0, 0) within melee range - merc.pos = Vector3.ZERO - var act = merc.update_mercenary(0.1, Vector3.ZERO, 10000, 10000, "mob_101", Vector3(1.5, 0, 0)) - _assert(act["status"] == "attacked_target", "Warrior attacked target") - _assert(atk_sig["fired"] == true and atk_sig["tgt"] == "mob_101", "mercenary_attacked signal fired") - _assert(atk_sig["dmg"] == merc.atk, "Damage matches warrior's ATK") - -func _test_teleport_and_movement() -> void: - print("\n--- Test 4: Distance Teleport Catch-up ---") - var merc := MercenaryCompanionSystem.new() - merc.summon_mercenary(MercenaryCompanionSystem.MercenaryType.SHAMAN_HEALER, Vector3.ZERO, 10) - - # Place mercenary 30 meters away (> 25m TELEPORT_DISTANCE) - merc.pos = Vector3(30, 0, 0) - var tp_act = merc.update_mercenary(0.1, Vector3.ZERO, 5000, 5000) - _assert(tp_act["status"] == "teleported_to_player", "Mercenary teleported back to player") - _assert(merc.pos.distance_to(Vector3.ZERO) < 3.0, "Position is now close to player") - -func _test_downed_and_revival() -> void: - print("\n--- Test 5: Downed State & Revival ---") - var merc := MercenaryCompanionSystem.new() - merc.summon_mercenary(MercenaryCompanionSystem.MercenaryType.WARRIOR_TANK, Vector3.ZERO, 10) - - var down_sig := {"fired": false} - var rev_sig := {"fired": false} - merc.mercenary_downed.connect(func(): down_sig["fired"] = true) - merc.mercenary_revived.connect(func(): rev_sig["fired"] = true) - - # Take fatal damage - var hit = merc.receive_damage(999999) - _assert(hit["ok"] == true and hit["downed"] == true, "Mercenary downed") - _assert(down_sig["fired"] == true, "mercenary_downed signal fired") - _assert(merc.is_alive == false, "is_alive is false") - - # Instant revive - var rev_res = merc.revive(true) - _assert(rev_res["ok"] == true, "Instant revive succeeded") - _assert(rev_sig["fired"] == true, "mercenary_revived signal fired") - _assert(merc.is_alive == true and merc.current_hp == merc.max_hp, "HP restored to 100% on instant revive") - -func _finish() -> void: - print("\n==========================================") - print("Mercenary Companion Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Mercenary Companion parity tests passed!") - quit(0) diff --git a/project/test_mercenary_companion_parity.gd.uid b/project/test_mercenary_companion_parity.gd.uid deleted file mode 100644 index 9c05d9e4..00000000 --- a/project/test_mercenary_companion_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://brku4b2nfqkg2 diff --git a/project/test_messenger_parity.gd b/project/test_messenger_parity.gd deleted file mode 100644 index a3ca00d2..00000000 --- a/project/test_messenger_parity.gd +++ /dev/null @@ -1,134 +0,0 @@ -# test_messenger_parity.gd —— 40250 好友/行会/家族联络人系统回归测试 -extends SceneTree - -const SystemClass = preload("res://messenger_system.gd") - -var _passed: int = 0 -var _failed: int = 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_messenger_parity (Batch 27 Direction 1) ===") - _test_friend_addition_and_validation() - _test_friend_status_transitions() - _test_guild_member_management() - _test_family_partner_mechanics() - _test_filtering_and_counts() - - print("\n=== Result: %d Passed, %d Failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - else: - printerr("SOME TESTS FAILED!\n") - quit(0 if _failed == 0 else 1) - -func _test_friend_addition_and_validation() -> void: - print("\n--- Test 1: Friend Addition & Constraints ---") - var m = SystemClass.new("Hero") - - # Reject empty name - var r_empty = m.add_friend("") - _assert(r_empty["error_code"] == "EMPTY_NAME", "Reject empty friend name") - - # Reject adding self - var r_self = m.add_friend("Hero") - _assert(r_self["error_code"] == "CANNOT_ADD_SELF", "Reject adding self as friend") - - # Add friend successfully - var r_ok = m.add_friend("Alice", SystemClass.STATE_ONLINE) - _assert(r_ok["success"] == true, "Add Alice as friend successfully") - _assert(m.is_friend("Alice") == true, "Alice is recognized as friend") - _assert(m.get_friend_state("Alice") == SystemClass.STATE_ONLINE, "Alice initial state is ONLINE") - - # Reject duplicate friend - var r_dup = m.add_friend("Alice") - _assert(r_dup["error_code"] == "ALREADY_FRIEND", "Reject duplicate friend addition") - - # Remove friend - var rm_ok = m.remove_friend("Alice") - _assert(rm_ok == true, "Alice removed from friends") - _assert(m.is_friend("Alice") == false, "Alice is no longer friend") - -func _test_friend_status_transitions() -> void: - print("\n--- Test 2: Friend Status Transitions (Online/Offline/Mobile) ---") - var m = SystemClass.new("Hero") - m.add_friend("Bob", SystemClass.STATE_OFFLINE) - - # Transition to ONLINE - m.set_friend_state("Bob", SystemClass.STATE_ONLINE) - _assert(m.get_friend_state("Bob") == SystemClass.STATE_ONLINE, "Bob transitioned to ONLINE") - var last_event = m.event_logs[m.event_logs.size() - 1] - _assert(last_event["type"] == "FRIEND_LOGIN", "Emitted FRIEND_LOGIN event") - - # Transition to MOBILE - m.set_friend_state("Bob", SystemClass.STATE_MOBILE) - _assert(m.get_friend_state("Bob") == SystemClass.STATE_MOBILE, "Bob transitioned to MOBILE") - last_event = m.event_logs[m.event_logs.size() - 1] - _assert(last_event["type"] == "FRIEND_MOBILE", "Emitted FRIEND_MOBILE event") - - # Transition to OFFLINE - m.set_friend_state("Bob", SystemClass.STATE_OFFLINE) - _assert(m.get_friend_state("Bob") == SystemClass.STATE_OFFLINE, "Bob transitioned to OFFLINE") - last_event = m.event_logs[m.event_logs.size() - 1] - _assert(last_event["type"] == "FRIEND_LOGOUT", "Emitted FRIEND_LOGOUT event") - -func _test_guild_member_management() -> void: - print("\n--- Test 3: Guild Member Synchronization ---") - var m = SystemClass.new("Hero") - - m.append_guild_member("Leader", "Guild Master", SystemClass.STATE_ONLINE) - m.append_guild_member("Knight", "General", SystemClass.STATE_ONLINE) - m.append_guild_member("Novice", "Member", SystemClass.STATE_OFFLINE) - - _assert(m.guild_members.size() == 3, "Guild has 3 members") - _assert(m.guild_members["Leader"].rank == "Guild Master", "Leader rank is Guild Master") - - # Remove single member - var rm_ok = m.remove_guild_member("Novice") - _assert(rm_ok == true, "Novice removed from guild list") - _assert(m.guild_members.size() == 2, "Guild has 2 members remaining") - - # Clear all guild members - m.clear_all_guild_members() - _assert(m.guild_members.is_empty(), "All guild members cleared on guild leave") - -func _test_family_partner_mechanics() -> void: - print("\n--- Test 4: Family / Spouse Partner Mechanics ---") - var m = SystemClass.new("Hero") - - # Bind partner - m.set_family_partner("Eva", 100, SystemClass.STATE_ONLINE) - _assert(m.family_partner != null, "Family partner bound") - _assert(m.family_partner.name == "Eva", "Partner name is Eva") - _assert(m.family_partner.love_point == 100, "Initial Love Points is 100") - - # Update love points - m.update_partner_love_points(85) - _assert(m.family_partner.love_point == 85, "Love Points updated to 85") - - # Remove partner (Divorce) - m.remove_family_partner() - _assert(m.family_partner == null, "Partner cleared on divorce") - -func _test_filtering_and_counts() -> void: - print("\n--- Test 5: Filtering and Online Counts ---") - var m = SystemClass.new("Hero") - - m.add_friend("User1", SystemClass.STATE_ONLINE) - m.add_friend("User2", SystemClass.STATE_MOBILE) - m.add_friend("User3", SystemClass.STATE_OFFLINE) - - var friend_list = m.get_group_members(SystemClass.GROUP_FRIEND) - _assert(friend_list.size() == 3, "Total 3 friends in group") - _assert(m.get_online_count(SystemClass.GROUP_FRIEND) == 2, "2 friends currently online (ONLINE + MOBILE)") - - m.append_guild_member("Guildie1", "Member", SystemClass.STATE_ONLINE) - m.append_guild_member("Guildie2", "Member", SystemClass.STATE_OFFLINE) - _assert(m.get_online_count(SystemClass.GROUP_GUILD) == 1, "1 guild member online") diff --git a/project/test_messenger_parity.gd.uid b/project/test_messenger_parity.gd.uid deleted file mode 100644 index 92811b0c..00000000 --- a/project/test_messenger_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://chpivlvtxa30l diff --git a/project/test_metin_spawn_event_parity.gd b/project/test_metin_spawn_event_parity.gd deleted file mode 100644 index 2f0f9e7f..00000000 --- a/project/test_metin_spawn_event_parity.gd +++ /dev/null @@ -1,168 +0,0 @@ -# test_metin_spawn_event_parity.gd —— 40250 野外 8 档魔石与怪潮暴动 1:1 回归测试套件 -extends SceneTree - -const MetinSpawnEventSystem = preload("res://metin_spawn_event_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_metin_spawn_event_parity (Direction 3: Field Metin Spawn & Waves 1:1) ===") - _test_table_configuration() - _test_spawn_metin() - _test_damage_and_phases() - _test_destruction_and_drops() - _test_invalid_inputs() - _finish() - -func _test_table_configuration() -> void: - print("\n--- Test 1: Metin Table Configuration (8008~8014, 8027) ---") - var expected_vnums = [8008, 8009, 8010, 8011, 8012, 8013, 8014, 8027] - _assert(MetinSpawnEventSystem.METIN_TABLE.size() == 8, "Table has exactly 8 Metin stone types") - - for vnum in expected_vnums: - _assert(MetinSpawnEventSystem.METIN_TABLE.has(vnum), "Has Metin stone %d" % vnum) - var entry = MetinSpawnEventSystem.METIN_TABLE[vnum] - _assert(entry.has("name") and entry.has("level") and entry.has("max_hp") and entry.has("def") and entry.has("wave_mob") and entry.has("wave_count"), - "Metin %d has all required fields" % vnum) - _assert(entry["max_hp"] > 0 and entry["def"] > 0 and entry["wave_count"] > 0, - "Metin %d has positive stats" % vnum) - -func _test_spawn_metin() -> void: - print("\n--- Test 2: Spawning Metin Stones ---") - var sys := MetinSpawnEventSystem.new() - var spawn_sig := {"fired": false, "vnum": 0, "name": "", "pos": Vector3.ZERO} - sys.metin_spawned.connect(func(v: int, n: String, p: Vector3): - spawn_sig["fired"] = true - spawn_sig["vnum"] = v - spawn_sig["name"] = n - spawn_sig["pos"] = p - ) - - var spawn_pos := Vector3(100.5, 0.0, 200.5) - var res = sys.spawn_metin(8008, spawn_pos) - _assert(res["ok"] == true, "Spawned 8008 successfully") - _assert(res["id"] == 1, "First instance ID is 1") - _assert(sys.active_metins.has(1), "Instance 1 in active_metins") - _assert(spawn_sig["fired"] == true and spawn_sig["vnum"] == 8008 and spawn_sig["pos"] == spawn_pos, - "metin_spawned signal emitted correctly") - - var res2 = sys.spawn_metin(8027, Vector3(50.0, 0.0, 50.0)) - _assert(res2["ok"] == true and res2["id"] == 2, "Second stone instance ID is 2") - _assert(sys.active_metins.size() == 2, "2 stones active") - -func _test_damage_and_phases() -> void: - print("\n--- Test 3: Damage Calculation & 75%, 50%, 25% Mob Waves ---") - var sys := MetinSpawnEventSystem.new() - var res = sys.spawn_metin(8008, Vector3.ZERO) # 8008: HP 70000, Def 120, Mob 391, count 4 - var id: int = res["id"] - - var waves := [] - sys.mob_wave_erupted.connect(func(vnum: int, count: int, minion_vnum: int): - waves.append({"vnum": vnum, "count": count, "minion_vnum": minion_vnum}) - ) - - # Raw damage 200 - 120 def = 80 net damage - var atk1 = sys.attack_metin(id, 200) - _assert(atk1["ok"] == true, "Attack successful") - _assert(atk1["net_dmg"] == 80, "Net damage = 200 - 120 = 80") - _assert(atk1["remaining_hp"] == 70000 - 80, "Remaining HP correct") - _assert(atk1["wave_triggered"] == false, "No wave at > 75% HP") - _assert(waves.size() == 0, "No wave signal fired") - - # Damage to trigger <= 75% HP: 70000 * 0.75 = 52500. We deal raw dmg to bring it below 52500. - # Remaining is 69920. Need to deal at least 69920 - 52500 = 17420 net dmg. - # Raw dmg = 17420 + 120 = 17540 - var atk_phase_75 = sys.attack_metin(id, 17540) - _assert(atk_phase_75["wave_triggered"] == true, "Phase 75% wave triggered") - _assert(atk_phase_75["spawned_count"] == 4, "Spawned 4 minions") - _assert(waves.size() == 1 and waves[0]["minion_vnum"] == 391, "Wave signal received for 391") - - # Another hit while still in 50%~75% bracket should NOT trigger wave again - var atk_repeat = sys.attack_metin(id, 1000) - _assert(atk_repeat["wave_triggered"] == false, "Subsequent hit doesn't retrigger 75% wave") - _assert(waves.size() == 1, "Wave count still 1") - - # Damage to trigger <= 50% HP (35000) - # Current HP is around 51620. Need 17000 net dmg. - var atk_phase_50 = sys.attack_metin(id, 20000) - _assert(atk_phase_50["wave_triggered"] == true, "Phase 50% wave triggered") - _assert(waves.size() == 2, "Wave count now 2") - - # Damage to trigger <= 25% HP (17500) - var atk_phase_25 = sys.attack_metin(id, 20000) - _assert(atk_phase_25["wave_triggered"] == true, "Phase 25% wave triggered") - _assert(waves.size() == 3, "Wave count now 3") - -func _test_destruction_and_drops() -> void: - print("\n--- Test 4: Metin Destruction & Drops ---") - var sys := MetinSpawnEventSystem.new() - var res = sys.spawn_metin(8014, Vector3(10, 0, 10)) # 8014: 死亡之石, lvl 65, HP 260000 - var id: int = res["id"] - - var destroy_sig := {"fired": false, "vnum": 0, "drops": []} - sys.metin_destroyed.connect(func(vnum: int, pos: Vector3, drops: Array): - destroy_sig["fired"] = true - destroy_sig["vnum"] = vnum - destroy_sig["drops"] = drops - ) - - # Overkill blow - var kill_atk = sys.attack_metin(id, 1000000) - _assert(kill_atk["ok"] == true and kill_atk["destroyed"] == true, "Metin destroyed") - _assert(sys.active_metins.has(id) == false, "Stone removed from active_metins") - _assert(destroy_sig["fired"] == true and destroy_sig["vnum"] == 8014, "metin_destroyed signal emitted") - - var drops: Array = kill_atk["drops"] - _assert(drops.size() == 4, "4 drops generated") - - var has_skill_book := false - var has_soul_stone := false - var has_spirit_stone := false - var has_gold := false - - for d in drops: - if d["vnum"] == 50300: - has_skill_book = true - elif d["vnum"] == 50513: - has_soul_stone = true - elif d["vnum"] == 28430: - has_spirit_stone = true - elif d["vnum"] == 1 and d["count"] >= 65 * 500: - has_gold = true - - _assert(has_skill_book, "Dropped Skill Book 50300") - _assert(has_soul_stone, "Dropped Soul Stone 50513") - _assert(has_spirit_stone, "Dropped Spirit Stone 28430") - _assert(has_gold, "Dropped Gold scaled with level 65") - -func _test_invalid_inputs() -> void: - print("\n--- Test 5: Invalid Metin Inputs ---") - var sys := MetinSpawnEventSystem.new() - var spawn_fail = sys.spawn_metin(99999) - _assert(spawn_fail["ok"] == false and spawn_fail["reason"] == "INVALID_METIN_VNUM", - "Correctly rejected invalid vnum 99999") - - var atk_fail = sys.attack_metin(8888, 100) - _assert(atk_fail["ok"] == false and atk_fail["reason"] == "METIN_NOT_FOUND", - "Correctly rejected attack on missing instance 8888") - -func _finish() -> void: - print("\n==========================================") - print("Metin Spawn Event Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Metin Spawn Event parity tests passed!") - quit(0) diff --git a/project/test_metin_spawn_event_parity.gd.uid b/project/test_metin_spawn_event_parity.gd.uid deleted file mode 100644 index 6cbaca6a..00000000 --- a/project/test_metin_spawn_event_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://n5dxvwk5n3lb diff --git a/project/test_mining_refinery_smelt_parity.gd b/project/test_mining_refinery_smelt_parity.gd deleted file mode 100644 index cea78e2c..00000000 --- a/project/test_mining_refinery_smelt_parity.gd +++ /dev/null @@ -1,144 +0,0 @@ -# test_mining_refinery_smelt_parity.gd —— 40250 矿石采掘熔炼与首饰宝石镶嵌 1:1 回归测试套件 -extends SceneTree - -const MiningRefinerySmeltSystem = preload("res://mining_refinery_smelt_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_mining_refinery_smelt_parity (Direction 1: Mining Refinery & Smelt 1:1) ===") - _test_smelt_ore_success() - _test_smelt_validations() - _test_open_jewelry_sockets() - _test_embed_gems() - _finish() - -func _test_smelt_ore_success() -> void: - print("\n--- Test 1: Smelting Ore into Refined Gems ---") - var sys := MiningRefinerySmeltSystem.new() - var player := {"gold": 50000} - var inv: Array = [ - {"vnum": 50621, "name": "钻石原石", "count": 100}, - {"vnum": 28030, "name": "战士灵石+0", "count": 1} - ] - - var smelt_sig := {"fired": false, "succ": false, "gem": 0} - sys.ore_smelted.connect(func(succ: bool, gvnum: int, _gn: String): - smelt_sig["fired"] = true - smelt_sig["succ"] = succ - smelt_sig["gem"] = gvnum - ) - - # Smelt Diamond Ore (force_success = true) - var res = sys.smelt_ore(0, 1, inv, player, false, true) - _assert(res["ok"] == true and res["success"] == true, "Smelt succeeded") - _assert(res["gem_vnum"] == 50634, "Produced Diamond Gem (50634)") - _assert(player["gold"] == 40000, "10,000 gold deducted (50000 -> 40000)") - _assert(smelt_sig["fired"] == true and smelt_sig["gem"] == 50634, "ore_smelted signal emitted") - - var has_gem := false - for it in inv: - if it != null and it.get("vnum", 0) == 50634: - has_gem = true - break - _assert(has_gem, "Diamond Gem found in inventory") - -func _test_smelt_validations() -> void: - print("\n--- Test 2: Smelting Input Validations ---") - var sys := MiningRefinerySmeltSystem.new() - var player := {"gold": 5000} # Less than 10,000 - var inv: Array = [ - {"vnum": 50623, "name": "乌木原石", "count": 50}, # Less than 100 - {"vnum": 28030, "count": 1} - ] - - # Fail on not enough ore - var fail_ore = sys.smelt_ore(0, 1, inv, player) - _assert(fail_ore["ok"] == false and fail_ore["reason"] == "NOT_ENOUGH_ORE", "Rejected: ore count < 100") - - # Fail on not enough gold - inv[0]["count"] = 100 - var fail_gold = sys.smelt_ore(0, 1, inv, player) - _assert(fail_gold["ok"] == false and fail_gold["reason"] == "NOT_ENOUGH_GOLD", "Rejected: gold < 10000") - -func _test_open_jewelry_sockets() -> void: - print("\n--- Test 3: Opening Jewelry Sockets with Diamond (50634) ---") - var sys := MiningRefinerySmeltSystem.new() - var inv: Array = [ - {"vnum": 50634, "name": "纯净钻石", "count": 5}, - {"vnum": 14009, "name": "白金手镯+9", "jewelry_sockets": []} - ] - - var sock_sig := {"fired": false, "idx": 0} - sys.jewelry_socket_opened.connect(func(_slot: int, idx: int): - sock_sig["fired"] = true - sock_sig["idx"] = idx - ) - - # Open 1st socket (100% success) - var s1 = sys.open_jewelry_socket(0, 1, inv, true) - _assert(s1["ok"] == true and s1["success"] == true, "1st socket opened") - _assert(inv[1]["jewelry_sockets"].size() == 1, "Bracelet has 1 socket") - _assert(sock_sig["fired"] == true and sock_sig["idx"] == 1, "jewelry_socket_opened emitted") - - # Open 2nd and 3rd sockets - sys.open_jewelry_socket(0, 1, inv, true) - sys.open_jewelry_socket(0, 1, inv, true) - _assert(inv[1]["jewelry_sockets"].size() == 3, "Bracelet has 3 sockets (Max)") - _assert(inv[0]["count"] == 2, "3 diamonds consumed (5 -> 2)") - - # 4th socket attempt must be rejected - var fail_4th = sys.open_jewelry_socket(0, 1, inv, true) - _assert(fail_4th["ok"] == false and fail_4th["reason"] == "MAX_SOCKETS_REACHED", "Max 3 sockets enforced") - -func _test_embed_gems() -> void: - print("\n--- Test 4: Embedding Refined Gems into Sockets ---") - var sys := MiningRefinerySmeltSystem.new() - var inv: Array = [ - {"vnum": 50641, "name": "白金精炼石", "count": 3}, - {"vnum": 14009, "name": "白金手镯+9", "jewelry_sockets": [null, null]} # 2 open sockets - ] - - var embed_sig := {"fired": false, "name": ""} - sys.gem_embedded.connect(func(_slot: int, gname: String, _b: float): - embed_sig["fired"] = true - embed_sig["name"] = gname - ) - - # Embed into socket 0 - var em1 = sys.embed_gem_into_socket(0, 1, inv) - _assert(em1["ok"] == true, "1st gem embedded") - _assert(inv[1]["jewelry_sockets"][0] != null, "Socket 0 occupied") - _assert(inv[1]["jewelry_sockets"][0]["vnum"] == 50641, "Socket 0 has White Gold Gem") - _assert(embed_sig["fired"] == true and embed_sig["name"] == "白金精炼石", "gem_embedded signal fired") - - # Embed into socket 1 - var em2 = sys.embed_gem_into_socket(0, 1, inv) - _assert(em2["ok"] == true, "2nd gem embedded") - _assert(inv[1]["jewelry_sockets"][1] != null, "Socket 1 occupied") - _assert(inv[0]["count"] == 1, "Gem count decreased 3 -> 1") - - # 3rd embed when all sockets occupied must fail - var fail_em = sys.embed_gem_into_socket(0, 1, inv) - _assert(fail_em["ok"] == false and fail_em["reason"] == "ALL_SOCKETS_OCCUPIED", "Rejected when sockets are full") - -func _finish() -> void: - print("\n==========================================") - print("Mining Refinery Smelt Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Mining Refinery Smelt parity tests passed!") - quit(0) diff --git a/project/test_mining_refinery_smelt_parity.gd.uid b/project/test_mining_refinery_smelt_parity.gd.uid deleted file mode 100644 index 5adddc4b..00000000 --- a/project/test_mining_refinery_smelt_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bqs2uw6qn6k36 diff --git a/project/test_mob_combat_reaction_parity.gd b/project/test_mob_combat_reaction_parity.gd deleted file mode 100644 index 83419715..00000000 --- a/project/test_mob_combat_reaction_parity.gd +++ /dev/null @@ -1,97 +0,0 @@ -# test_mob_combat_reaction_parity.gd —— 验证方向 2:怪物主动仇恨视野、追击脱战与连击击退硬直 40250 1:1 对齐 -extends SceneTree - -const MobCombatReactionSystem = preload("res://mob_combat_reaction_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_mob_combat_reaction_parity (Direction 2: Mob Aggro & Knockback 1:1) ===") - test_aggro_radius_detection() - test_leash_distance_and_reset() - test_combo_reaction_steps() - test_knockback_direction_vector() - - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed > 0: - printerr("TEST FAILED!") - quit(1) - else: - print("ALL TESTS PASSED!") - quit(0) - -func test_aggro_radius_detection() -> void: - print("\n--- Test 1: Aggro Radius (18m) Detection ---") - var mob_pos := Vector3(0, 0, 0) - var player_in_range := Vector3(10, 0, 10) # distance = ~14.14m <= 18m - var player_out_range := Vector3(20, 0, 20) # distance = ~28.28m > 18m - - # Active aggressive mob - _assert(MobCombatReactionSystem.check_aggro(mob_pos, player_in_range, true, 18.0), - "Aggressive mob detects player within 18m") - _assert(not MobCombatReactionSystem.check_aggro(mob_pos, player_out_range, true, 18.0), - "Aggressive mob ignores player outside 18m") - - # Passive mob ignores player even close - _assert(not MobCombatReactionSystem.check_aggro(mob_pos, player_in_range, false, 18.0), - "Passive mob does not aggro even when close") - -func test_leash_distance_and_reset() -> void: - print("\n--- Test 2: Leash Distance (40m) & Full HP Reset ---") - var spawn_pos := Vector3(100, 0, 100) - var mob_chasing := Vector3(120, 0, 120) # distance = 28.28m <= 40m - var mob_leashed := Vector3(150, 0, 150) # distance = 70.71m > 40m - - _assert(not MobCombatReactionSystem.check_leash_exceeded(mob_chasing, spawn_pos, 40.0), - "Mob within 40m remains in chase") - _assert(MobCombatReactionSystem.check_leash_exceeded(mob_leashed, spawn_pos, 40.0), - "Mob exceeding 40m triggers leash exceeded") - - # Leash reset - var damaged_mob := {"hp": 200, "max_hp": 2000, "target_vid": 1, "state": MobCombatReactionSystem.MobAIState.CHASE} - var reset_res := MobCombatReactionSystem.perform_leash_reset(damaged_mob, spawn_pos) - _assert(reset_res.leashed, "Leash reset executed") - _assert(damaged_mob.hp == 2000, "HP fully restored to 2000") - _assert(damaged_mob.target_vid == 0, "Target cleared") - _assert(damaged_mob.state == MobCombatReactionSystem.MobAIState.RETREAT, "State set to RETREAT") - -func test_combo_reaction_steps() -> void: - print("\n--- Test 3: 40250 Combo Step Reactions (1-2 Damage vs 3-4 Knockdown) ---") - var mob_pos := Vector3(0, 0, 0) - - # Step 1: Hit 1 -> normal damage stun - var hit1 := MobCombatReactionSystem.calculate_combo_reaction(1, mob_pos, 0.0) - _assert(hit1.reaction_type == "DAMAGE_STUN" and not hit1.is_knockback, "Combo 1 is DAMAGE_STUN (no knockback)") - _assert(hit1.anim == "damage", "Plays damage animation") - - # Step 2: Hit 2 -> normal damage stun - var hit2 := MobCombatReactionSystem.calculate_combo_reaction(2, mob_pos, 0.0) - _assert(hit2.reaction_type == "DAMAGE_STUN" and not hit2.is_knockback, "Combo 2 is DAMAGE_STUN") - - # Step 3: Hit 3 -> heavy knockback & knockdown - var hit3 := MobCombatReactionSystem.calculate_combo_reaction(3, mob_pos, 0.0) - _assert(hit3.reaction_type == "KNOCKBACK_FLYING" and hit3.is_knockback, "Combo 3 triggers KNOCKBACK_FLYING") - _assert(hit3.anim == "knockdown" and hit3.standup_anim == "standup", "Triggers knockdown and standup") - - # Step 4: Hit 4 -> heavy knockback & knockdown - var hit4 := MobCombatReactionSystem.calculate_combo_reaction(4, mob_pos, 0.0) - _assert(hit4.reaction_type == "KNOCKBACK_FLYING" and hit4.is_knockback, "Combo 4 triggers KNOCKBACK_FLYING") - -func test_knockback_direction_vector() -> void: - print("\n--- Test 4: Knockback Push Vector Along Attack Heading ---") - var mob_pos := Vector3(10, 0, 10) - - # Attack heading = 0 radians (North / +Z) - var hit_north := MobCombatReactionSystem.calculate_combo_reaction(3, mob_pos, 0.0) - var offset_z: float = hit_north.push_offset.z - _assert(is_equal_approx(offset_z, 1.5), "Knockback pushed 1.5m along attack heading") - _assert(is_equal_approx(hit_north.target_pos.z, 11.5), "Target position updated to 11.5m") diff --git a/project/test_mob_combat_reaction_parity.gd.uid b/project/test_mob_combat_reaction_parity.gd.uid deleted file mode 100644 index bb361410..00000000 --- a/project/test_mob_combat_reaction_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c6a8fvd6qbd1h diff --git a/project/test_monarch_election_ui_parity.gd b/project/test_monarch_election_ui_parity.gd deleted file mode 100644 index 6869b4a8..00000000 --- a/project/test_monarch_election_ui_parity.gd +++ /dev/null @@ -1,126 +0,0 @@ -# test_monarch_election_ui_parity.gd —— 40250 帝国君主大选投票公示系统回归测试 -extends SceneTree - -const SystemClass = preload("res://monarch_election_ui_system.gd") - -var _passed: int = 0 -var _failed: int = 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_monarch_election_ui_parity (Batch 28 Direction 3) ===") - _test_candidacy_registration_rules() - _test_voting_mechanics() - _test_election_conclusion_and_monarch_inauguration() - _test_monarch_privileges() - - print("\n=== Result: %d Passed, %d Failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - else: - printerr("SOME TESTS FAILED!\n") - quit(0 if _failed == 0 else 1) - -func _test_candidacy_registration_rules() -> void: - print("\n--- Test 1: Candidacy Registration Rules (Lv 50+, 1M Gold) ---") - var m = SystemClass.new() - - # 1. Reject when stage is CLOSED - var r1 = m.register_candidacy(1, "PlayerA", SystemClass.EMPIRE_SHINSOO, 60, 2000000, "For the Empire!") - _assert(r1["error_code"] == "CANDIDACY_NOT_OPEN", "Reject registration when stage is CLOSED") - - # Set stage to CANDIDACY - m.set_election_stage(SystemClass.EMPIRE_SHINSOO, SystemClass.STATUS_CANDIDACY) - - # 2. Reject level < 50 - var r2 = m.register_candidacy(2, "PlayerB", SystemClass.EMPIRE_SHINSOO, 49, 2000000, "I am young") - _assert(r2["error_code"] == "LEVEL_TOO_LOW", "Reject candidacy for level < 50") - - # 3. Reject insufficient gold (< 1M) - var r3 = m.register_candidacy(3, "PlayerC", SystemClass.EMPIRE_SHINSOO, 55, 500000, "I am poor") - _assert(r3["error_code"] == "INSUFFICIENT_GOLD", "Reject candidacy when gold < 1,000,000") - - # 4. Valid registration - var r4 = m.register_candidacy(1, "CandidateAlpha", SystemClass.EMPIRE_SHINSOO, 70, 2000000, "Glory to Shinsoo!") - _assert(r4["success"] == true, "Registered CandidateAlpha successfully") - _assert(r4["gold_cost"] == 1000000, "Deducted 1,000,000 gold deposit") - _assert(m.candidates[SystemClass.EMPIRE_SHINSOO].size() == 1, "Candidates count is 1") - - # 5. Reject duplicate registration - var r5 = m.register_candidacy(1, "CandidateAlpha", SystemClass.EMPIRE_SHINSOO, 70, 2000000, "Duplicate") - _assert(r5["error_code"] == "ALREADY_REGISTERED", "Reject duplicate candidacy registration") - -func _test_voting_mechanics() -> void: - print("\n--- Test 2: Voting Eligibility & One-Vote-Per-Citizen Limit ---") - var m = SystemClass.new() - m.set_election_stage(SystemClass.EMPIRE_SHINSOO, SystemClass.STATUS_CANDIDACY) - m.register_candidacy(101, "CandidateOne", SystemClass.EMPIRE_SHINSOO, 65, 2000000, "Manifesto 1") - m.register_candidacy(102, "CandidateTwo", SystemClass.EMPIRE_SHINSOO, 75, 2000000, "Manifesto 2") - - # Reject voting when still in CANDIDACY stage - var r_fail_stage = m.cast_vote(501, SystemClass.EMPIRE_SHINSOO, 60, 101) - _assert(r_fail_stage["error_code"] == "VOTING_NOT_OPEN", "Reject voting when stage != STATUS_VOTING") - - # Advance to VOTING stage - m.set_election_stage(SystemClass.EMPIRE_SHINSOO, SystemClass.STATUS_VOTING) - - # Reject voter level < 50 - var r_low_voter = m.cast_vote(502, SystemClass.EMPIRE_SHINSOO, 45, 101) - _assert(r_low_voter["error_code"] == "VOTER_LEVEL_TOO_LOW", "Reject voter with level < 50") - - # Valid vote - var r_vote1 = m.cast_vote(501, SystemClass.EMPIRE_SHINSOO, 60, 101) - _assert(r_vote1["success"] == true, "Citizen 501 successfully voted for CandidateOne") - _assert(m.candidates[SystemClass.EMPIRE_SHINSOO][0].votes == 1, "CandidateOne has 1 vote") - - # Reject duplicate vote - var r_dup_vote = m.cast_vote(501, SystemClass.EMPIRE_SHINSOO, 60, 102) - _assert(r_dup_vote["error_code"] == "ALREADY_VOTED", "Reject duplicate vote from same citizen") - - # Another citizen votes for CandidateTwo - m.cast_vote(503, SystemClass.EMPIRE_SHINSOO, 60, 102) - m.cast_vote(504, SystemClass.EMPIRE_SHINSOO, 60, 102) - _assert(m.candidates[SystemClass.EMPIRE_SHINSOO][1].votes == 2, "CandidateTwo has 2 votes") - -func _test_election_conclusion_and_monarch_inauguration() -> void: - print("\n--- Test 3: Election Conclusion & Winner Inauguration ---") - var m = SystemClass.new() - m.set_election_stage(SystemClass.EMPIRE_CHUNJO, SystemClass.STATUS_CANDIDACY) - m.register_candidacy(201, "ChunjoHero", SystemClass.EMPIRE_CHUNJO, 80, 2000000, "Pledge") - m.register_candidacy(202, "ChunjoLord", SystemClass.EMPIRE_CHUNJO, 85, 2000000, "Pledge") - - m.set_election_stage(SystemClass.EMPIRE_CHUNJO, SystemClass.STATUS_VOTING) - m.cast_vote(601, SystemClass.EMPIRE_CHUNJO, 55, 202) # 202 gets 1 vote - - var r_win = m.conclude_election(SystemClass.EMPIRE_CHUNJO) - _assert(r_win["success"] == true, "Election concluded successfully") - _assert(r_win["winner_pid"] == 202, "Candidate 202 won with majority votes") - _assert(m.current_monarchs[SystemClass.EMPIRE_CHUNJO]["pid"] == 202, "Candidate 202 inaugurated as Monarch") - -func _test_monarch_privileges() -> void: - print("\n--- Test 4: Monarch Privileges (Tax & Empire Blessing) ---") - var m = SystemClass.new() - m.current_monarchs[SystemClass.EMPIRE_JINNO]["pid"] = 301 - m.current_monarchs[SystemClass.EMPIRE_JINNO]["name"] = "EmperorJinno" - - # 1. Non-monarch cannot adjust tax - var r_tax_fail = m.set_tax_rate(999, SystemClass.EMPIRE_JINNO, 5) - _assert(r_tax_fail["error_code"] == "NOT_MONARCH", "Non-monarch rejected from changing tax") - - # 2. Monarch sets tax to 8% - var r_tax_ok = m.set_tax_rate(301, SystemClass.EMPIRE_JINNO, 8) - _assert(r_tax_ok["success"] == true, "Monarch successfully updated tax rate") - _assert(m.current_monarchs[SystemClass.EMPIRE_JINNO]["tax_rate"] == 8, "Tax rate is 8%") - - # 3. Monarch casts empire blessing - var r_bless = m.activate_monarch_blessing(301, SystemClass.EMPIRE_JINNO) - _assert(r_bless["success"] == true, "Monarch activated empire blessing") - _assert(r_bless["blessing_buff"]["exp_bonus"] == 10, "Granted +10% EXP bonus") - _assert(r_bless["blessing_buff"]["att_bonus"] == 10, "Granted +10% Attack bonus") diff --git a/project/test_monarch_election_ui_parity.gd.uid b/project/test_monarch_election_ui_parity.gd.uid deleted file mode 100644 index a9096842..00000000 --- a/project/test_monarch_election_ui_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bsf03mqm3i5l0 diff --git a/project/test_monarch_empire_treasury_parity.gd b/project/test_monarch_empire_treasury_parity.gd deleted file mode 100644 index aa8749c7..00000000 --- a/project/test_monarch_empire_treasury_parity.gd +++ /dev/null @@ -1,151 +0,0 @@ -# test_monarch_empire_treasury_parity.gd —— Metin2 40250 君主特权与国库祈愿系统 1:1 测试套件 -extends SceneTree - -const MonarchEmpireTreasurySystem = preload("res://monarch_empire_treasury_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_monarch_empire_treasury_parity (Direction 1: Monarch & Treasury 1:1) ===") - - test_monarch_appointment() - test_treasury_management_and_tax() - test_heal_my_empire() - test_power_up_and_defense_up() - test_serialization() - - print("\n=======================================================") - print("Monarch Treasury Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - - if _failed > 0: - quit(1) - else: - quit(0) - -func test_monarch_appointment() -> void: - print("\n--- Test 1: Monarch Appointment & Verification (40250 1:1) ---") - var system = MonarchEmpireTreasurySystem.new() - - _assert(system.is_monarch(1001, 1) == false, "No monarch initially") - - # 任命辛秀帝国 (红国 1) 君主 - var ok = system.appoint_monarch(1, 1001, "赤霄狂帝") - _assert(ok == true, "Appointed Shinsoo monarch") - _assert(system.is_monarch(1001, 1) == true, "Player 1001 is monarch of Shinsoo") - _assert(system.is_monarch(1002, 1) == false, "Player 1002 is not monarch") - _assert(system.is_monarch(1001, 2) == false, "Player 1001 is not Chunjo monarch") - - var info = system.get_monarch_info(1) - _assert(info["player_name"] == "赤霄狂帝", "Monarch name recorded") - _assert(info["player_id"] == 1001, "Monarch PID recorded") - -func test_treasury_management_and_tax() -> void: - print("\n--- Test 2: Treasury Gold & Monster Micro-Tax (40250 1:1) ---") - var system = MonarchEmpireTreasurySystem.new() - - _assert(system.get_treasury_gold(1) == 0, "Treasury starts at 0") - - # 国库注资 1,000,000 - system.add_treasury_gold(1, 1000000) - _assert(system.get_treasury_gold(1) == 1000000, "Treasury has 1,000,000 gold") - - # 征缴打怪微税 (2% 进国库) - var tax = system.collect_monster_tax(1, 50000, 0.02) - _assert(tax == 1000, "Collected 1,000 gold tax from 50,000 drop") - _assert(system.get_treasury_gold(1) == 1001000, "Treasury increased to 1,001,000") - - # 扣除资金 - var dec_ok = system.dec_treasury_gold(1, 500000) - _assert(dec_ok == true, "Dec treasury 500,000 succeeded") - _assert(system.get_treasury_gold(1) == 501000, "Treasury balance is 501,000") - - # 超额扣除拦截 - dec_ok = system.dec_treasury_gold(1, 9999999) - _assert(dec_ok == false, "Rejected overdraft") - -func test_heal_my_empire() -> void: - print("\n--- Test 3: HealMyEmpire Grace (40250 1:1) ---") - var system = MonarchEmpireTreasurySystem.new() - system.appoint_monarch(1, 1001, "赤霄狂帝") - system.add_treasury_gold(1, 300000) # 存入 300,000 - - # 模拟在线国民 - var citizens: Array[Dictionary] = [ - {"pid": 1001, "empire": 1, "hp": 200, "max_hp": 2000, "sp": 50, "max_sp": 500}, - {"pid": 1002, "empire": 1, "hp": 50, "max_hp": 1500, "sp": 10, "max_sp": 400}, - {"pid": 2001, "empire": 2, "hp": 100, "max_hp": 1000, "sp": 20, "max_sp": 300} # 敌国国民 - ] - - # 非君主尝试施展 -> 拦截 - var res = system.heal_my_empire(1002, 1, citizens) - _assert(res["ok"] == false, "Rejected non-monarch cast") - _assert(res["reason"] == "NOT_MONARCH", "Reason is NOT_MONARCH") - - # 君主施展恩泽圣愈 - res = system.heal_my_empire(1001, 1, citizens) - _assert(res["ok"] == true, "HealMyEmpire succeeded") - _assert(res["healed_count"] == 2, "Healed 2 Shinsoo citizens") - _assert(citizens[0]["hp"] == 2000 and citizens[0]["sp"] == 500, "Citizen 1 fully restored") - _assert(citizens[1]["hp"] == 1500 and citizens[1]["sp"] == 400, "Citizen 2 fully restored") - _assert(citizens[2]["hp"] == 100, "Enemy empire citizen not healed") - _assert(res["remaining_treasury"] == 200000, "Cost 100,000 deducted from treasury") - - # 冷却期连续施放 -> 拦截 - res = system.heal_my_empire(1001, 1, citizens) - _assert(res["ok"] == false, "Rejected during cooldown") - _assert(res["reason"] == "COOLDOWN_ACTIVE", "Reason is COOLDOWN_ACTIVE") - -func test_power_up_and_defense_up() -> void: - print("\n--- Test 4: PowerUp & DefenseUp Empire Auras (40250 1:1) ---") - var system = MonarchEmpireTreasurySystem.new() - system.appoint_monarch(1, 1001, "赤霄狂帝") - system.add_treasury_gold(1, 2000000) # 200万国库金币 - - # 激活战力觉醒 (PowerUp, +10% 攻击力) - var res = system.activate_power_up(1001, 1) - _assert(res["ok"] == true, "Activated PowerUp") - _assert(res["att_grade_bonus"] == 0.10, "+10% attack bonus") - _assert(res["remaining_treasury"] == 1500000, "Deducted 500,000 cost") - - # 激活铁壁守护 (DefenseUp, +10% 防御力) - res = system.activate_defense_up(1001, 1) - _assert(res["ok"] == true, "Activated DefenseUp") - _assert(res["def_grade_bonus"] == 0.10, "+10% defense bonus") - - # 检查国民享受的增益 - var buffs = system.get_citizen_empire_buffs(1) - _assert(buffs["att_bonus"] == 0.10, "Shinsoo citizen enjoys +10% attack") - _assert(buffs["def_bonus"] == 0.10, "Shinsoo citizen enjoys +10% defense") - - # 敌国无增益 - var enemy_buffs = system.get_citizen_empire_buffs(2) - _assert(enemy_buffs["att_bonus"] == 0.0, "Enemy has 0 bonus") - - # 时间推移 1900 秒 (增益持续 1800 秒结束) - system.update(1900.0) - buffs = system.get_citizen_empire_buffs(1) - _assert(buffs["att_bonus"] == 0.0 and buffs["def_bonus"] == 0.0, "Auras expired naturally") - -func test_serialization() -> void: - print("\n--- Test 5: Serialization & Restoration ---") - var system = MonarchEmpireTreasurySystem.new() - system.appoint_monarch(1, 1001, "赤霄狂帝") - system.add_treasury_gold(1, 888888) - - var data = system.serialize() - _assert(data.has("monarch_pids") and data["treasury"][1] == 888888, "Serialized state") - - var restored = MonarchEmpireTreasurySystem.new() - restored.deserialize(data) - _assert(restored.is_monarch(1001, 1) == true, "Restored monarch PID") - _assert(restored.get_treasury_gold(1) == 888888, "Restored treasury gold") diff --git a/project/test_monarch_empire_treasury_parity.gd.uid b/project/test_monarch_empire_treasury_parity.gd.uid deleted file mode 100644 index 52c74784..00000000 --- a/project/test_monarch_empire_treasury_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bcgfxr58e656e diff --git a/project/test_monster_card_parity.gd b/project/test_monster_card_parity.gd deleted file mode 100644 index 9b9b75af..00000000 --- a/project/test_monster_card_parity.gd +++ /dev/null @@ -1,110 +0,0 @@ -# test_monster_card_parity.gd —— 40250 怪物图鉴与百科系统 1:1 纯净回归测试套件 -extends SceneTree - -const MonsterCardSystem = preload("res://monster_card_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_monster_card_parity (Direction 4: Monster Card Bestiary 1:1) ===") - _test_card_registration_and_discovery() - _test_zone_completion_and_permanent_bonuses() - _test_multiple_zone_bonuses_stacking() - _finish() - -func _test_card_registration_and_discovery() -> void: - print("\n--- Test 1: Card Registration & Bestiary Entry Discovery ---") - var mcs := MonsterCardSystem.new() - - # 1. Invalid card vnum - var r_bad = mcs.register_card(99999) - _assert(r_bad["ok"] == false and r_bad["reason"] == "INVALID_CARD", "Invalid card vnum rejected") - - # 2. Query undiscovered monster 101 (野狼) - var undiscovered = mcs.get_monster_entry(101) - _assert(undiscovered["is_unlocked"] == false, "Entry 101 initially locked") - _assert(undiscovered["name"] == "???", "Name is hidden as '???'") - - # 3. Register Card 70201 (野狼卡片) - var card_signal := {"fired": false, "vnum": 0, "name": ""} - mcs.card_collected.connect(func(v: int, n: String, _c: int): - card_signal["fired"] = true - card_signal["vnum"] = v - card_signal["name"] = n - ) - - var r_reg = mcs.register_card(70201) - _assert(r_reg["ok"] == true, "Card 70201 registered successfully") - _assert(card_signal["fired"] == true and card_signal["vnum"] == 101, "card_collected signal fired for 101") - _assert(mcs.collected_cards[101] == 1, "Collected 1 Wolf card") - - # 4. Query discovered monster 101 - var discovered = mcs.get_monster_entry(101) - _assert(discovered["is_unlocked"] == true, "Entry 101 is now unlocked") - _assert(discovered["name"] == "野狼", "Name revealed as '野狼'") - _assert(discovered["level"] == 1, "Level revealed as 1") - _assert(discovered["drops"].size() >= 2, "Drops revealed") - -func _test_zone_completion_and_permanent_bonuses() -> void: - print("\n--- Test 2: Zone Completion & Passive Stat Activation ---") - var mcs := MonsterCardSystem.new() - - var zone_signal := {"fired": false, "zname": "", "bonus": {}} - mcs.zone_completed.connect(func(zn: String, b: Dictionary): - zone_signal["fired"] = true - zone_signal["zname"] = zn - zone_signal["bonus"] = b - ) - - # Register Wolf (101) -> Village has 2 monsters, not complete yet - mcs.register_card(70201) - _assert(zone_signal["fired"] == false, "Village not complete with only 1/2 monsters") - _assert(mcs.get_active_collection_bonuses().is_empty(), "No bonuses active yet") - - # Register White Oath (391, card 70202) -> Completes Village! - var r2 = mcs.register_card(70202) - _assert(r2["zone_completed"] == true, "Village zone completed") - _assert(zone_signal["fired"] == true and zone_signal["zname"] == "新手原野", "zone_completed signal fired") - _assert(zone_signal["bonus"]["max_hp"] == 300, "Village grants +300 Max HP") - - var bonuses = mcs.get_active_collection_bonuses() - _assert(bonuses["max_hp"] == 300, "Active collection bonuses contain +300 Max HP") - -func _test_multiple_zone_bonuses_stacking() -> void: - print("\n--- Test 3: Multi-Zone Stacking (Village + Desert + Catacomb) ---") - var mcs := MonsterCardSystem.new() - - # Complete Village (+300 HP) - mcs.register_card(70201) - mcs.register_card(70202) - - # Complete Desert (+3% Monster Dmg) - mcs.register_card(70203) # 2108 沙漠强盗 - mcs.register_card(70204) # 2191 巨型沙漠龟 - - # Complete Catacomb (+50 DEF) - mcs.register_card(70205) # 2597 塔耳塔洛斯 - mcs.register_card(70206) # 2598 冥王阿兹瑞尔 - - var all_bonuses = mcs.get_active_collection_bonuses() - _assert(all_bonuses["max_hp"] == 300, "Max HP +300 stacked") - _assert(all_bonuses["monster_dmg_pct"] == 3, "Monster Dmg +3% stacked") - _assert(all_bonuses["def"] == 50, "DEF +50 stacked") - -func _finish() -> void: - print("\n==========================================") - print("Monster Card Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_monster_card_parity.gd.uid b/project/test_monster_card_parity.gd.uid deleted file mode 100644 index bba5b2b6..00000000 --- a/project/test_monster_card_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cfdv1dlg0028g diff --git a/project/test_mount_horse_parity.gd b/project/test_mount_horse_parity.gd deleted file mode 100644 index 48ea0215..00000000 --- a/project/test_mount_horse_parity.gd +++ /dev/null @@ -1,123 +0,0 @@ -# test_mount_horse_parity.gd —— 战马与骑乘系统 1:1 回归测试套件 -# 运行方式: -# /opt/homebrew/bin/godot --headless --path metin2-client/project --script test_mount_horse_parity.gd -extends SceneTree - -const HorseSystem = preload("res://horse_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_mount_horse_parity (Direction 3: Horse & Mount System 1:1) ===") - _test_horse_tiers_and_food() - _test_summoning() - _test_ctrl_h_ride() - _test_ctrl_b_back() - _test_ctrl_g_ride() - _test_ctrl_f_feed() - _test_mounted_combat_ranges() - _finish() - -func _test_horse_tiers_and_food() -> void: - print("\n--- Test 1: Horse Tiers & Foods ---") - _assert(HorseSystem.get_horse_tier_from_item(50051) == HorseSystem.HORSE_TIER_BEGINNER, "50051 is Beginner Horse") - _assert(HorseSystem.get_horse_tier_from_item(50052) == HorseSystem.HORSE_TIER_COMBAT, "50052 is Combat Horse") - _assert(HorseSystem.get_horse_tier_from_item(50053) == HorseSystem.HORSE_TIER_MILITARY, "50053 is Military Horse") - - _assert(HorseSystem.get_required_food(HorseSystem.HORSE_TIER_BEGINNER) == HorseSystem.ITEM_FOOD_HAY, "Beginner eats Hay (50057)") - _assert(HorseSystem.get_required_food(HorseSystem.HORSE_TIER_COMBAT) == HorseSystem.ITEM_FOOD_CARROT, "Combat horse eats Carrot (50054)") - _assert(HorseSystem.get_required_food(HorseSystem.HORSE_TIER_MILITARY) == HorseSystem.ITEM_FOOD_GINSENG, "Military horse eats Ginseng (50058)") - -func _test_summoning() -> void: - print("\n--- Test 2: Summoning & SP Costs ---") - # Rejects if insufficient SP - var r1 := HorseSystem.try_summon_horse(HorseSystem.HORSE_TIER_BEGINNER, 50, 0) - _assert(r1["success"] == false and r1["err"] == "insufficient_sp", "Rejects summon when SP < 100") - - # Roll test - var r_fail := HorseSystem.try_summon_horse(HorseSystem.HORSE_TIER_BEGINNER, 150, 0, 50) - _assert(r_fail["success"] == false, "Roll 50 fails for 10% base chance") - - var r_succ := HorseSystem.try_summon_horse(HorseSystem.HORSE_TIER_BEGINNER, 150, 0, 5) - _assert(r_succ["success"] == true, "Roll 5 succeeds for 10% base chance") - - # Higher summon skill adds +10% per level - var chance_lv5 := HorseSystem.calculate_summon_chance(HorseSystem.HORSE_TIER_BEGINNER, 5) - _assert(chance_lv5 == 60, "Skill Lv 5 adds +50% chance (10 + 50 = 60%)") - -func _test_ctrl_h_ride() -> void: - print("\n--- Test 3: Ctrl+H (/user_horse_ride) State Machine ---") - # 1. Horse not summoned -> reject - var r1 := HorseSystem.handle_user_horse_ride(false, false, false, false) - _assert(r1["action"] == "none" and r1["err"] == "horse_not_summoned", "Cannot ride if horse is not summoned") - - # 2. Horse summoned -> mount - var r2 := HorseSystem.handle_user_horse_ride(false, true, false, false) - _assert(r2["action"] == "start_riding" and r2["is_riding"] == true, "Starts riding when horse is summoned") - - # 3. Already riding -> dismount - var r3 := HorseSystem.handle_user_horse_ride(true, true, false, false) - _assert(r3["action"] == "stop_riding" and r3["is_riding"] == false, "Stops riding when already mounted") - - # 4. Dead/stun -> no action - var r4 := HorseSystem.handle_user_horse_ride(false, true, false, true) - _assert(r4["action"] == "none" and r4["err"] == "dead_or_stun", "No action while dead or stunned") - -func _test_ctrl_b_back() -> void: - print("\n--- Test 4: Ctrl+B (/user_horse_back) State Machine ---") - # 1. Currently riding -> must dismount first - var r1 := HorseSystem.handle_user_horse_back(true, true) - _assert(r1["action"] == "none" and r1["err"] == "must_dismount_first", "Must dismount before sending horse back") - - # 2. Horse not summoned -> cannot send back - var r2 := HorseSystem.handle_user_horse_back(false, false) - _assert(r2["action"] == "none" and r2["err"] == "horse_not_summoned", "Cannot send back if not summoned") - - # 3. Summoned & unmounted -> unsummon - var r3 := HorseSystem.handle_user_horse_back(false, true) - _assert(r3["action"] == "unsummon" and r3["is_horse_summoned"] == false, "Successfully unsummons horse") - -func _test_ctrl_g_ride() -> void: - print("\n--- Test 5: Ctrl+G (/ride) State Machine ---") - # 1. Dismount if already riding - var r1 := HorseSystem.handle_ride(true, true, false) - _assert(r1["action"] == "stop_riding", "/ride dismounts if riding") - - # 2. Mount summoned horse - var r2 := HorseSystem.handle_ride(false, true, false) - _assert(r2["action"] == "start_riding", "/ride mounts summoned horse") - - # 3. Mount item from inventory if horse not summoned - var r3 := HorseSystem.handle_ride(false, false, true) - _assert(r3["action"] == "mount_item", "/ride mounts ride item from inventory") - -func _test_ctrl_f_feed() -> void: - print("\n--- Test 6: Ctrl+F (/user_horse_feed) ---") - var f1 := HorseSystem.handle_user_horse_feed(HorseSystem.HORSE_TIER_COMBAT, HorseSystem.ITEM_FOOD_HAY) - _assert(f1["success"] == false, "Combat horse rejects Hay") - - var f2 := HorseSystem.handle_user_horse_feed(HorseSystem.HORSE_TIER_COMBAT, HorseSystem.ITEM_FOOD_CARROT) - _assert(f2["success"] == true, "Combat horse accepts Carrot") - -func _test_mounted_combat_ranges() -> void: - print("\n--- Test 7: Combat & Pickup Ranges (3.0m vs 5.0m) ---") - _assert(HorseSystem.WALKING_ATTACK_RANGE == 3.0, "Walking attack range is 3.0m (300cm)") - _assert(HorseSystem.MOUNTED_ATTACK_RANGE == 5.0, "Mounted attack range is 5.0m (500cm)") - -func _finish() -> void: - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - quit(0) - else: - printerr("SOME TESTS FAILED!\n") - quit(1) diff --git a/project/test_mount_horse_parity.gd.uid b/project/test_mount_horse_parity.gd.uid deleted file mode 100644 index db95bc4f..00000000 --- a/project/test_mount_horse_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://hkfvw8m4jppi diff --git a/project/test_mounted_combat_skill_parity.gd b/project/test_mounted_combat_skill_parity.gd deleted file mode 100644 index ad11f2ca..00000000 --- a/project/test_mounted_combat_skill_parity.gd +++ /dev/null @@ -1,140 +0,0 @@ -# test_mounted_combat_skill_parity.gd —— 40250 军马冲杀与马背技能 1:1 回归测试套件 -extends SceneTree - -const MountedCombatSkillSystem = preload("res://mounted_combat_skill_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_mounted_combat_skill_parity (Direction 1: Mounted Combat 1:1) ===") - _test_horse_tiers_and_mounting() - _test_mounted_basic_attacks() - _test_military_horse_skills() - _test_stamina_and_feeding() - _finish() - -func _test_horse_tiers_and_mounting() -> void: - print("\n--- Test 1: Horse Tiers & Mounting Speeds ---") - var sys := MountedCombatSkillSystem.new() - - # Normal Horse (lvl 5) - var r1 = sys.mount_horse(5) - _assert(r1["tier"] == MountedCombatSkillSystem.HorseTier.NORMAL, "Lvl 5 is NORMAL horse") - _assert(r1["speed_bonus"] == 30, "Normal horse gives +30 speed") - sys.unmount_horse() - - # Combat Horse (lvl 15) - var r2 = sys.mount_horse(15) - _assert(r2["tier"] == MountedCombatSkillSystem.HorseTier.COMBAT, "Lvl 15 is COMBAT horse") - _assert(r2["speed_bonus"] == 40, "Combat horse gives +40 speed") - sys.unmount_horse() - - # Military Horse (lvl 25) - var r3 = sys.mount_horse(25) - _assert(r3["tier"] == MountedCombatSkillSystem.HorseTier.MILITARY, "Lvl 25 is MILITARY horse") - _assert(r3["speed_bonus"] == 50, "Military horse gives +50 speed") - sys.unmount_horse() - -func _test_mounted_basic_attacks() -> void: - print("\n--- Test 2: Mounted Basic Attacks ---") - var sys := MountedCombatSkillSystem.new() - - # Normal horse cannot attack - sys.mount_horse(5) - var fail_atk = sys.execute_mounted_attack(100, ["mob1", "mob2"]) - _assert(fail_atk["ok"] == false and fail_atk["reason"] == "TIER_TOO_LOW", "Normal horse cannot attack") - sys.unmount_horse() - - # Combat horse can attack - sys.mount_horse(15) - var atk_sig := {"fired": false, "hits": 0, "dmg": 0} - sys.mounted_attack_executed.connect(func(h: int, d: int): - atk_sig["fired"] = true - atk_sig["hits"] = h - atk_sig["dmg"] = d - ) - - var res = sys.execute_mounted_attack(500, ["m1", "m2", "m3"]) - _assert(res["ok"] == true, "Combat horse attacked") - _assert(res["hit_count"] == 3, "Hit 3 targets") - _assert(res["dmg_per_hit"] == int(500 * 1.2), "Deals 1.2x base attack (600)") - _assert(res["total_damage"] == 1800, "Total damage is 1800") - _assert(atk_sig["fired"] == true and atk_sig["dmg"] == 1800, "mounted_attack_executed signal fired") - _assert(sys.stamina == 99, "Stamina consumed 100 -> 99") - sys.unmount_horse() - -func _test_military_horse_skills() -> void: - print("\n--- Test 3: Military Horse Skills (121, 122, 123) ---") - var sys := MountedCombatSkillSystem.new() - sys.mount_horse(25) # Military horse - - var player := {"sp": 300} - var skill_sig := {"fired": false, "vnum": 0, "dmg": 0} - sys.horse_skill_cast.connect(func(v: int, _n: String, d: int, _h: int): - skill_sig["fired"] = true - skill_sig["vnum"] = v - skill_sig["dmg"] = d - ) - - # Cast 121: Horse Charge (2.5x dmg, SP 60, cd 12.0) - var c1 = sys.cast_horse_skill(MountedCombatSkillSystem.SKILL_HORSE_CHARGE, 1000, player, ["m1", "m2"]) - _assert(c1["ok"] == true, "Horse Charge cast successfully") - _assert(c1["damage"] == 2500, "Damage = 1000 * 2.5 = 2500") - _assert(player["sp"] == 240, "SP deducted (300 - 60 = 240)") - _assert(skill_sig["fired"] == true and skill_sig["vnum"] == 121, "horse_skill_cast emitted") - - # Second cast while on cooldown must fail - var fail_cd = sys.cast_horse_skill(MountedCombatSkillSystem.SKILL_HORSE_CHARGE, 1000, player, ["m1"]) - _assert(fail_cd["ok"] == false and fail_cd["reason"] == "SKILL_ON_COOLDOWN", "Cooldown enforced") - - # Update 12 seconds - sys.update(12.5) - - # Cast 122: Horse Stomp (2.0x dmg, SP 75) - var c2 = sys.cast_horse_skill(MountedCombatSkillSystem.SKILL_HORSE_STOMP, 1000, player, ["m1", "m2", "m3", "m4"]) - _assert(c2["ok"] == true and c2["damage"] == 2000, "Horse Stomp deals 2.0x (2000)") - _assert(player["sp"] == 165, "SP is now 240 - 75 = 165") - -func _test_stamina_and_feeding() -> void: - print("\n--- Test 4: Stamina Depletion & Feeding ---") - var sys := MountedCombatSkillSystem.new() - sys.mount_horse(21) - sys.stamina = 10 # Low stamina - - var inv: Array = [ - {"vnum": 50055, "name": "胡萝卜", "count": 2} - ] - - var feed_sig := {"fired": false, "rec": 0} - sys.horse_fed.connect(func(_n: String, rec: int): - feed_sig["fired"] = true - feed_sig["rec"] = rec - ) - - var res = sys.feed_horse(50055, inv) - _assert(res["ok"] == true, "Fed carrot") - _assert(res["recovered"] == 60, "Carrot restores 60 stamina") - _assert(sys.stamina == 70, "Stamina is now 10 + 60 = 70") - _assert(inv[0]["count"] == 1, "Carrot count decreased 2 -> 1") - _assert(feed_sig["fired"] == true and feed_sig["rec"] == 60, "horse_fed emitted") - -func _finish() -> void: - print("\n==========================================") - print("Mounted Combat Skill Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Mounted Combat Skill parity tests passed!") - quit(0) diff --git a/project/test_mounted_combat_skill_parity.gd.uid b/project/test_mounted_combat_skill_parity.gd.uid deleted file mode 100644 index eb674c0b..00000000 --- a/project/test_mounted_combat_skill_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cj2k305egsmpd diff --git a/project/test_mysterious_merchant_blackmarket_parity.gd b/project/test_mysterious_merchant_blackmarket_parity.gd deleted file mode 100644 index 6f097024..00000000 --- a/project/test_mysterious_merchant_blackmarket_parity.gd +++ /dev/null @@ -1,214 +0,0 @@ -# test_mysterious_merchant_blackmarket_parity.gd —— 40250 野外神秘游商与黑市折扣系统 1:1 回归测试套件 -extends SceneTree - -const MysteriousMerchantBlackmarketSystem = preload("res://mysterious_merchant_blackmarket_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_mysterious_merchant_blackmarket_parity (Direction 4: Mysterious Merchant Blackmarket 1:1) ===") - _test_merchant_spawn_and_lifecycle() - _test_blackmarket_goods_and_discount_purchase() - _test_high_price_buyback_and_antiflag() - _test_serialization_and_persistence() - _finish() - -func _test_merchant_spawn_and_lifecycle() -> void: - print("\n--- Test 1: Merchant Spawn & Lifecycle ---") - var sys := MysteriousMerchantBlackmarketSystem.new() - _assert(not sys.is_active, "Initially merchant is not active") - - var spawn_sig := {"fired": false, "map": "", "pos": Vector2.ZERO, "dur": 0.0} - sys.merchant_appeared.connect(func(m: String, p: Vector2, d: float): - spawn_sig["fired"] = true - spawn_sig["map"] = m - spawn_sig["pos"] = p - spawn_sig["dur"] = d - ) - - var depart_sig := {"fired": false} - sys.merchant_departed.connect(func(): - depart_sig["fired"] = true - ) - - # Spawn at fixed location index 0 (Desert) - var spawn_res = sys.spawn_random_merchant(1800.0, 0) - _assert(spawn_res["ok"] == true, "Spawn result ok") - _assert(sys.is_active == true, "System active flag is true") - _assert(sys.current_map == "Desert", "Spawned at Desert") - _assert(sys.current_pos == Vector2(120, 350), "Spawn pos matches Desert Oasis") - _assert(spawn_sig["fired"] == true, "merchant_appeared signal fired") - _assert(spawn_sig["dur"] == 1800.0, "Signal duration is 1800.0s") - - # Update heartbeat - advance 600s - sys.update(600.0) - _assert(is_equal_approx(sys.time_remaining, 1200.0), "Time remaining decremented to 1200s") - _assert(sys.is_active == true, "Still active after 600s") - - # Update heartbeat - advance remaining 1200s to timeout - sys.update(1200.0) - _assert(sys.is_active == false, "Merchant despawned after timeout") - _assert(depart_sig["fired"] == true, "merchant_departed signal fired on timeout") - _assert(sys.get_stock().is_empty(), "Stock cleared after merchant departed") - -func _test_blackmarket_goods_and_discount_purchase() -> void: - print("\n--- Test 2: Black Market Goods & Discount Purchases ---") - var sys := MysteriousMerchantBlackmarketSystem.new() - sys.spawn_random_merchant(1800.0, 1) # Mount_Sohan - _assert(sys.current_map == "Mount_Sohan", "Spawned at Mount Sohan") - - var stock = sys.get_stock() - _assert(stock.size() == 5, "Stock has 5 unique blackmarket items") - - # Check item 0 (Cor Draconis): base 2M, blackmarket price 1M (50% off) - _assert(stock[0]["vnum"] == 51501, "First item is Cor Draconis (51501)") - _assert(stock[0]["price"] == 1000000, "Half-price discount 1,000,000 gold") - _assert(stock[0]["base_price"] == 2000000, "Base price 2,000,000 gold") - _assert(stock[0]["stock"] == 5, "Initial stock is 5") - - var player := {"gold": 500000} # Only 500k, not enough for 1M - var inv: Array = [null, null, null] - - # Attempt buy with insufficient gold - var buy_fail = sys.buy_item(0, player, inv) - _assert(buy_fail["ok"] == false, "Buy failed with insufficient gold") - _assert(buy_fail["reason"] == "NOT_ENOUGH_GOLD", "Failure reason is NOT_ENOUGH_GOLD") - - # Give enough gold - player["gold"] = 3000000 - var bought_sig := {"fired": false, "name": "", "cost": 0} - sys.item_bought.connect(func(n: String, c: int): - bought_sig["fired"] = true - bought_sig["name"] = n - bought_sig["cost"] = c - ) - - var buy_succ = sys.buy_item(0, player, inv) - _assert(buy_succ["ok"] == true, "Buy succeeded with sufficient gold") - _assert(buy_succ["cost"] == 1000000, "Cost is 1,000,000 gold") - _assert(player["gold"] == 2000000, "Player gold deducted properly to 2,000,000") - _assert(inv[0] != null and inv[0]["vnum"] == 51501, "Cor Draconis placed into slot 0") - _assert(bought_sig["fired"] == true, "item_bought signal fired") - _assert(bought_sig["cost"] == 1000000, "Signal reported correct cost") - - # Check remaining stock - var updated_stock = sys.get_stock() - _assert(updated_stock[0]["stock"] == 4, "Stock decremented from 5 to 4") - - # Test inventory full - inv[1] = {"vnum": 1, "name": "Item 1"} - inv[2] = {"vnum": 2, "name": "Item 2"} - var buy_full = sys.buy_item(0, player, inv) - _assert(buy_full["ok"] == false, "Buy failed when inventory full") - _assert(buy_full["reason"] == "INVENTORY_FULL", "Failure reason is INVENTORY_FULL") - - # Test out of stock by depleting item 4 (Legendary Elixir, stock=2) - var big_inv: Array = [null, null, null, null, null] - player["gold"] = 10000000 - var b1 = sys.buy_item(4, player, big_inv) - _assert(b1["ok"] == true, "Bought 1st Legendary Elixir") - var b2 = sys.buy_item(4, player, big_inv) - _assert(b2["ok"] == true, "Bought 2nd Legendary Elixir") - var b3 = sys.buy_item(4, player, big_inv) - _assert(b3["ok"] == false, "3rd buy rejected") - _assert(b3["reason"] == "OUT_OF_STOCK", "Reason is OUT_OF_STOCK") - -func _test_high_price_buyback_and_antiflag() -> void: - print("\n--- Test 3: High-Price Buyback (+30% Markup) & Anti-Flags ---") - var sys := MysteriousMerchantBlackmarketSystem.new() - sys.spawn_random_merchant(1800.0, 2) # Doyyumhwan - - var player := {"gold": 100000} - # Base price 10,000. Standard sell (1/5) = 2,000. - # High-price buyback (+30%) = 2,000 * 1.3 = 2,600. - var test_rare_item := { - "vnum": 70005, - "name": "极品黑铁矿", - "count": 2, - "shop_buy_price": 10000, - "anti_flags": 0 - } - - # For count=2: unit=2600 * 2 = 5200. - var calculated_price = sys.calculate_sell_price(test_rare_item) - _assert(calculated_price == 5200, "High-price buyback calculated 5200 gold (2600 * 2)") - - var inv: Array = [test_rare_item, null] - - var sold_sig := {"fired": false, "name": "", "earned": 0} - sys.item_sold.connect(func(n: String, e: int): - sold_sig["fired"] = true - sold_sig["name"] = n - sold_sig["earned"] = e - ) - - var sell_res = sys.sell_item(0, player, inv) - _assert(sell_res["ok"] == true, "Sold rare item to wandering merchant") - _assert(sell_res["earned"] == 5200, "Earned 5200 gold") - _assert(player["gold"] == 105200, "Player gold increased to 105,200") - _assert(inv[0] == null, "Inventory slot 0 cleared") - _assert(sold_sig["fired"] == true, "item_sold signal fired") - _assert(sold_sig["earned"] == 5200, "Signal reported earned gold") - - # Test unsellable item with ITEM_ANTIFLAG_SELL (256) - var unsellable_item := { - "vnum": 90001, - "name": "绑定专属神兵", - "count": 1, - "shop_buy_price": 500000, - "anti_flags": 256 - } - inv[0] = unsellable_item - var sell_unsellable = sys.sell_item(0, player, inv) - _assert(sell_unsellable["ok"] == false, "Unsellable item rejected") - _assert(sell_unsellable["reason"] == "ITEM_ANTIFLAG_SELL", "Reason is ITEM_ANTIFLAG_SELL") - _assert(inv[0] != null, "Unsellable item not removed from inventory") - - # Test merchant inactive - sys.despawn_merchant() - var sell_inactive = sys.sell_item(0, player, inv) - _assert(sell_inactive["ok"] == false, "Sell rejected when merchant inactive") - _assert(sell_inactive["reason"] == "MERCHANT_NOT_ACTIVE", "Reason is MERCHANT_NOT_ACTIVE") - -func _test_serialization_and_persistence() -> void: - print("\n--- Test 4: Serialization & Save/Load Parity ---") - var sys1 := MysteriousMerchantBlackmarketSystem.new() - sys1.spawn_merchant("Desert", Vector2(120, 350), 1500.0) - sys1.update(300.0) # 1200s left - - # Buy 1 Cor Draconis so stock changes - var player := {"gold": 5000000} - var inv: Array = [null] - sys1.buy_item(0, player, inv) - _assert(sys1.get_stock()[0]["stock"] == 4, "Sys1 stock of item 0 is 4") - - var save_data = sys1.serialize() - _assert(save_data["is_active"] == true, "Serialized is_active true") - _assert(save_data["current_map"] == "Desert", "Serialized map matches") - _assert(save_data["stock"][0]["stock"] == 4, "Serialized stock of item 0 is 4") - - var sys2 := MysteriousMerchantBlackmarketSystem.new() - sys2.deserialize(save_data) - _assert(sys2.is_active == true, "Sys2 restored is_active") - _assert(sys2.current_map == "Desert", "Sys2 restored current_map") - _assert(sys2.current_pos == Vector2(120, 350), "Sys2 restored current_pos") - _assert(is_equal_approx(sys2.time_remaining, 1200.0), "Sys2 restored time_remaining") - _assert(sys2.get_stock()[0]["stock"] == 4, "Sys2 restored stock count 4") - -func _finish() -> void: - print("\n=======================================================") - print("Mysterious Merchant & Blackmarket Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_mysterious_merchant_blackmarket_parity.gd.uid b/project/test_mysterious_merchant_blackmarket_parity.gd.uid deleted file mode 100644 index 67ad95f6..00000000 --- a/project/test_mysterious_merchant_blackmarket_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dno7ti6084jdx diff --git a/project/test_npc_shop_parity.gd b/project/test_npc_shop_parity.gd deleted file mode 100644 index d597b851..00000000 --- a/project/test_npc_shop_parity.gd +++ /dev/null @@ -1,127 +0,0 @@ -# test_npc_shop_parity.gd —— 验证方向 1:NPC 商店库存、金币购买与 1/5 折价出售 40250 1:1 对齐 -extends SceneTree - -const NpcShopSystem = preload("res://npc_shop_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_npc_shop_parity (Direction 1: NPC Shop Buy/Sell 1:1) ===") - test_npc_shop_stocks() - test_sell_price_calculation() - test_buy_flow_and_gold_deduction() - test_buy_rejections() - test_sell_flow_and_antiflag() - - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed > 0: - printerr("TEST FAILED!") - quit(1) - else: - print("ALL TESTS PASSED!") - quit(0) - -func test_npc_shop_stocks() -> void: - print("\n--- Test 1: NPC Shop Stock Configurations ---") - _assert(NpcShopSystem.has_shop(9003), "General Store (9003) has shop registered") - _assert(NpcShopSystem.has_shop(9001), "Weapon Shop (9001) has shop registered") - _assert(NpcShopSystem.has_shop(9002), "Armor Shop (9002) has shop registered") - _assert(not NpcShopSystem.has_shop(101), "Wild dog (101) does not have a shop") - - var general_items := NpcShopSystem.get_shop_items(9003) - _assert(general_items.size() >= 10, "General Store has at least 10 basic stock items") - _assert(general_items[0].vnum == 27001, "Slot 0 is Red Potion S (27001)") - _assert(general_items[6].vnum == 22001, "Slot 6 is Town Scroll (22001)") - _assert(general_items[9].vnum == 27400, "Slot 9 is Fishing Rod (27400)") - -func test_sell_price_calculation() -> void: - print("\n--- Test 2: 40250 Official 1/5 Sell Price Calculation (shop.cpp:520) ---") - # 1. Normal item with buy_price = 1000 -> sell_price = 1000 / 5 = 200 - var normal_item := {"shop_buy_price": 1000, "anti_flags": 0} - var sell_val := NpcShopSystem.calculate_sell_price(normal_item, 1) - _assert(sell_val == 200, "1000 gold item sells for 200 gold (1/5 buy price)") - - # 2. Count = 5 -> sell_price = 200 * 5 = 1000 - var sell_val_5 := NpcShopSystem.calculate_sell_price(normal_item, 5) - _assert(sell_val_5 == 1000, "5 count sells for 1000 gold") - - # 3. Item with ITEM_ANTIFLAG_SELL cannot be sold - var unsellable_item := {"shop_buy_price": 50000, "anti_flags": NpcShopSystem.ITEM_ANTIFLAG_SELL} - var sell_unsellable := NpcShopSystem.calculate_sell_price(unsellable_item, 1) - _assert(sell_unsellable == 0, "ITEM_ANTIFLAG_SELL item sells for 0 (forbidden)") - -func test_buy_flow_and_gold_deduction() -> void: - print("\n--- Test 3: Buying Flow and Gold Deduction ---") - var player_ctx := { - "gold": 50000, - "inventory": [] - } - - # Buy 1 pack of Red Potion S from General Store (pos 0: 200 count, 1200 gold) - var buy_res := NpcShopSystem.buy_item(0, 1, 9003, player_ctx) - _assert(buy_res.ok and buy_res.code == "SUCCESS", "Buy succeeded") - _assert(buy_res.cost == 1200, "Cost is 1200 gold") - _assert(player_ctx.gold == 50000 - 1200, "Player gold deducted to 48800") - _assert(player_ctx.inventory.size() == 1, "Item added to player inventory") - _assert(player_ctx.inventory[0].vnum == 27001, "Added item is 27001") - _assert(player_ctx.inventory[0].count == 200, "Added item has 200 count") - -func test_buy_rejections() -> void: - print("\n--- Test 4: Buy Rejections (Insufficient Gold / Inventory Full) ---") - # Insufficient Gold - var poor_player := { - "gold": 500, # Needs 1200 - "inventory": [] - } - var res_poor := NpcShopSystem.buy_item(0, 1, 9003, poor_player) - _assert(not res_poor.ok and res_poor.code == "NOT_ENOUGH_GOLD", "Rejected due to insufficient gold") - _assert(poor_player.gold == 500, "Gold untouched") - - # Inventory Full - var full_player := { - "gold": 100000, - "inventory": [] - } - for i in 90: - full_player.inventory.append({"cell": i, "vnum": 10, "count": 1}) - var res_full := NpcShopSystem.buy_item(0, 1, 9003, full_player) - _assert(not res_full.ok and res_full.code == "INVENTORY_FULL", "Rejected due to inventory full") - -func test_sell_flow_and_antiflag() -> void: - print("\n--- Test 5: Selling Flow & Anti-Flag Verification ---") - var player_ctx := { - "gold": 1000, - "inventory": [ - {"cell": 0, "vnum": 10, "count": 1}, # Sword +0 (buy_price: 1000 -> sells for 200) - {"cell": 1, "vnum": 71085, "count": 2} # Scroll with anti-sell - ] - } - - var proto_mock := func(vnum: int) -> Dictionary: - if vnum == 10: - return {"shop_buy_price": 1000, "anti_flags": 0} - elif vnum == 71085: - return {"shop_buy_price": 50000, "anti_flags": NpcShopSystem.ITEM_ANTIFLAG_SELL} - return {} - - # Sell unsellable item in cell 1 - var res_unsellable := NpcShopSystem.sell_item(1, 1, player_ctx, proto_mock) - _assert(not res_unsellable.ok and res_unsellable.code == "ITEM_ANTIFLAG_SELL", - "ITEM_ANTIFLAG_SELL item sale rejected") - - # Sell valid item in cell 0 - var res_sell := NpcShopSystem.sell_item(0, 1, player_ctx, proto_mock) - _assert(res_sell.ok and res_sell.code == "SUCCESS", "Sale succeeded") - _assert(res_sell.gold_earned == 200, "Earned 200 gold") - _assert(player_ctx.gold == 1200, "Player gold increased to 1200") - _assert(player_ctx.inventory.size() == 1, "Sold item removed from inventory") - _assert(player_ctx.inventory[0].cell == 1, "Remaining item is cell 1") diff --git a/project/test_npc_shop_parity.gd.uid b/project/test_npc_shop_parity.gd.uid deleted file mode 100644 index dd54c850..00000000 --- a/project/test_npc_shop_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://6ta56apkr82f diff --git a/project/test_offline_bazaar_market_parity.gd b/project/test_offline_bazaar_market_parity.gd deleted file mode 100644 index 2b1a8d8d..00000000 --- a/project/test_offline_bazaar_market_parity.gd +++ /dev/null @@ -1,172 +0,0 @@ -# test_offline_bazaar_market_parity.gd —— 40250 单人单机广场离线摆摊与虚拟交易 1:1 回归测试套件 -extends SceneTree - -const OfflineBazaarMarketSystem = preload("res://offline_bazaar_market_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_offline_bazaar_market_parity (Direction 1: Offline Bazaar Market 1:1) ===") - _test_npc_stalls_browse_and_buy() - _test_buy_validation_errors() - _test_player_stall_open() - _test_offline_sales_and_revenue() - _test_stall_close_and_item_return() - _finish() - -func _test_npc_stalls_browse_and_buy() -> void: - print("\n--- Test 1: NPC Stalls Browsing & Purchasing ---") - var mkt := OfflineBazaarMarketSystem.new() - var stalls = mkt.get_all_stalls() - _assert(stalls.size() == 3, "Found 3 default NPC stalls in village square") - - # Browse Stall 1 (Materials) - var b_res = mkt.browse_stall(1) - _assert(b_res["ok"] == true and b_res["owner"] == "阿隆", "Browsed Stall 1 (阿隆)") - _assert(b_res["items"].size() == 4, "Stall 1 has 4 goods") - - # Buy Pearl (27993) from Stall 1 - var player := {"gold": 2000000} - var inv: Array = [] - inv.resize(10) - for i in range(10): - inv[i] = null - - var buy_sig := {"fired": false, "name": "", "cost": 0} - mkt.item_bought_from_stall.connect(func(_sid: int, iname: String, _c: int, cost: int): - buy_sig["fired"] = true - buy_sig["name"] = iname - buy_sig["cost"] = cost - ) - - # Buy 2 pearls: 500,000 * 2 = 1,000,000 gold - var buy_res = mkt.buy_from_stall(1, 0, 2, player, inv) - _assert(buy_res["ok"] == true, "Purchased 2 pearls") - _assert(player["gold"] == 1000000, "Gold deducted 2M -> 1M") - _assert(inv[0]["vnum"] == 27993 and inv[0]["count"] == 2, "Pearls placed in slot 0") - _assert(mkt.npc_stalls[1]["items"][0]["stock"] == 3, "Stock decreased 5 -> 3") - _assert(buy_sig["fired"] == true and buy_sig["name"] == "珍珠", "item_bought_from_stall signal fired") - -func _test_buy_validation_errors() -> void: - print("\n--- Test 2: Purchase Error Validations ---") - var mkt := OfflineBazaarMarketSystem.new() - var player := {"gold": 100} # Very poor - var inv: Array = [null] - - # Not enough gold - var fail_gold = mkt.buy_from_stall(1, 0, 1, player, inv) - _assert(fail_gold["ok"] == false and fail_gold["reason"] == "NOT_ENOUGH_GOLD", "Rejected: not enough gold") - - # Out of stock - player["gold"] = 999999999 - var fail_stock = mkt.buy_from_stall(1, 0, 999, player, inv) - _assert(fail_stock["ok"] == false and fail_stock["reason"] == "OUT_OF_STOCK", "Rejected: out of stock") - - # Full inventory - var full_inv := [{"vnum": 1}] - var fail_inv = mkt.buy_from_stall(1, 0, 1, player, full_inv) - _assert(fail_inv["ok"] == false and fail_inv["reason"] == "INVENTORY_FULL", "Rejected: inventory full") - -func _test_player_stall_open() -> void: - print("\n--- Test 3: Opening Private Player Stall (50200 Bundle) ---") - var mkt := OfflineBazaarMarketSystem.new() - var inv: Array = [ - {"vnum": OfflineBazaarMarketSystem.ITEM_BUNDLE_VNUM, "name": "摆摊包袱", "count": 1}, - {"vnum": 10, "name": "巨剑+9", "count": 1}, - {"vnum": 27003, "name": "红色药水(特大)", "count": 100} - ] - - var open_sig := {"fired": false, "title": "", "count": 0} - mkt.player_stall_opened.connect(func(t: String, c: int): - open_sig["fired"] = true - open_sig["title"] = t - open_sig["count"] = c - ) - - # Try open without bundle - var no_bundle_inv: Array = [{"vnum": 10, "count": 1}] - var fail_bundle = mkt.open_player_stall("我的摊位", [{"slot": 0, "price": 1000}], no_bundle_inv, true) - _assert(fail_bundle["ok"] == false and fail_bundle["reason"] == "NEED_BUNDLE_ITEM", "Rejected without bundle") - - # Open stall with bundle and 2 items - var items_to_list := [ - {"slot": 1, "price": 500000, "count": 1}, - {"slot": 2, "price": 200, "count": 50} # sell 50 out of 100 - ] - - var res = mkt.open_player_stall("【东方极速特惠商铺】", items_to_list, inv, true) - _assert(res["ok"] == true, "Player stall opened") - _assert(inv[0] == null, "Bundle item (50200) consumed") - _assert(inv[1] == null, "Listed Sword removed from inventory slot 1") - _assert(inv[2]["count"] == 50, "Potion count halved in inventory (100 - 50 = 50)") - _assert(mkt.player_stall["is_open"] == true, "Stall is open") - _assert(mkt.player_stall["items"].size() == 2, "2 goods listed in stall") - _assert(open_sig["fired"] == true and open_sig["count"] == 2, "player_stall_opened signal fired") - -func _test_offline_sales_and_revenue() -> void: - print("\n--- Test 4: Simulated Offline Sales & Revenue Collection ---") - var mkt := OfflineBazaarMarketSystem.new() - var inv: Array = [ - {"vnum": 50200, "count": 1}, - {"vnum": 28430, "name": "战士灵石+4", "count": 1} - ] - - mkt.open_player_stall("【灵石专卖】", [{"slot": 1, "price": 1000000, "count": 1}], inv, true) - _assert(mkt.player_stall["items"].size() == 1, "1 item listed") - - # Simulate 30 minutes of offline time - var sim = mkt.simulate_offline_sales(30.0) - _assert(sim["sold_count"] == 1, "Stone sold during simulated offline time") - _assert(mkt.player_stall["revenue"] == 1000000, "1M gold earned in stall revenue") - _assert(mkt.player_stall["items"].is_empty(), "Stall inventory now empty") - - # Collect revenue - var player := {"gold": 50000} - var col_res = mkt.collect_stall_revenue(player) - _assert(col_res["ok"] == true and player["gold"] == 1050000, "Collected revenue into player wallet") - _assert(mkt.player_stall["revenue"] == 0, "Stall revenue reset to 0") - -func _test_stall_close_and_item_return() -> void: - print("\n--- Test 5: Closing Stall & Returning Unsold Items ---") - var mkt := OfflineBazaarMarketSystem.new() - var inv: Array = [ - {"vnum": 50200, "count": 1}, - {"vnum": 11299, "name": "黑铁战甲+9", "count": 1} - ] - - mkt.open_player_stall("【神装铺】", [{"slot": 1, "price": 10000000, "count": 1}], inv, true) - - var close_sig := {"fired": false, "rev": -1} - mkt.player_stall_closed.connect(func(rev: int): - close_sig["fired"] = true - close_sig["rev"] = rev - ) - - # Close stall immediately without sales - var player := {"gold": 0} - var close_res = mkt.close_player_stall(inv, player) - _assert(close_res["ok"] == true, "Stall closed successfully") - _assert(mkt.player_stall["is_open"] == false, "Stall is no longer open") - _assert(close_sig["fired"] == true and close_sig["rev"] == 0, "player_stall_closed signal fired") - _assert(inv[0]["vnum"] == 11299, "Unsold armor safely returned to first empty inventory slot") - -func _finish() -> void: - print("\n==========================================") - print("Offline Bazaar Market Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Offline Bazaar Market parity tests passed!") - quit(0) diff --git a/project/test_offline_bazaar_market_parity.gd.uid b/project/test_offline_bazaar_market_parity.gd.uid deleted file mode 100644 index ab813991..00000000 --- a/project/test_offline_bazaar_market_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d15164rwj8q3s diff --git a/project/test_party_management_parity.gd b/project/test_party_management_parity.gd deleted file mode 100644 index afdbf4b0..00000000 --- a/project/test_party_management_parity.gd +++ /dev/null @@ -1,124 +0,0 @@ -# test_party_management_parity.gd —— 40250 8人组队面板、经验分配与领导力光环系统回归测试 -extends SceneTree - -const SystemClass = preload("res://party_management_system.gd") - -var _passed: int = 0 -var _failed: int = 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_party_management_parity (Batch 27 Direction 4) ===") - _test_party_capacity_and_join() - _test_member_expel_and_leave() - _test_leadership_tactical_auras() - _test_exp_distribution_modes() - _test_hp_percentage_synchronization() - - print("\n=== Result: %d Passed, %d Failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - else: - printerr("SOME TESTS FAILED!\n") - quit(0 if _failed == 0 else 1) - -func _test_party_capacity_and_join() -> void: - print("\n--- Test 1: Party Creation & 8-Member Limit ---") - var party = SystemClass.new(1, "LeaderOne", 50) - _assert(party.members.size() == 1, "Party created with 1 member (Leader)") - _assert(party.leader_pid == 1, "Leader PID is 1") - - # Add 7 more members (total 8) - for i in range(2, 9): - var r = party.add_member(i, "Member%d" % i, 40 + i) - _assert(r["success"] == true, "Member %d joined party" % i) - - _assert(party.members.size() == 8, "Party reached 8 members") - - # 9th member must be rejected - var r_overflow = party.add_member(9, "OverflowMember", 50) - _assert(r_overflow["error_code"] == "PARTY_FULL", "Reject 9th member (max 8)") - -func _test_member_expel_and_leave() -> void: - print("\n--- Test 2: Expel & Leave Mechanics ---") - var party = SystemClass.new(1, "Leader", 60) - party.add_member(2, "Alice", 55) - party.add_member(3, "Bob", 50) - - # Non-leader cannot expel - var r_fail = party.expel_member(2, 3) - _assert(r_fail["error_code"] == "NOT_LEADER", "Non-leader cannot expel member") - - # Leader expels Alice - var r_expel = party.expel_member(1, 2) - _assert(r_expel["success"] == true, "Leader successfully expelled Alice") - _assert(party.members.has(2) == false, "Alice no longer in party") - - # Leader leaves party -> Bob promoted to new leader - var r_lead_leave = party.leave_party(1) - _assert(r_lead_leave["success"] == true, "Leader left party") - _assert(party.leader_pid == 3, "Bob promoted to new leader") - _assert(party.members[3].is_leader == true, "Bob marked as leader") - - # Bob leaves -> Party dissolves - var r_dissolve = party.leave_party(3) - _assert(r_dissolve["dissolved"] == true, "Party dissolved when last member left") - -func _test_leadership_tactical_auras() -> void: - print("\n--- Test 3: Leader Tactical Auras (Roles & Buffs) ---") - var party = SystemClass.new(1, "Commander", 75) - party.add_member(2, "Warrior", 70) - party.add_member(3, "Sura", 72) - - # Non-leader cannot assign role - var r_non_lead = party.set_member_role(2, 2, SystemClass.ROLE_ATTACKER) - _assert(r_non_lead["error_code"] == "NOT_LEADER", "Non-leader cannot assign role") - - # Leader assigns Warrior as ATTACKER - var r1 = party.set_member_role(1, 2, SystemClass.ROLE_ATTACKER) - _assert(r1["success"] == true, "Assigned Warrior as ATTACKER") - var buff1 = party.get_member_buff(2) - _assert(buff1["stat"] == "att_grade" and buff1["val"] == 30, "Warrior receives +30 att_grade buff") - - # Leader assigns Sura as TANKER - var r2 = party.set_member_role(1, 3, SystemClass.ROLE_TANKER) - _assert(r2["success"] == true, "Assigned Sura as TANKER") - var buff2 = party.get_member_buff(3) - _assert(buff2["stat"] == "max_hp" and buff2["val"] == 2500, "Sura receives +2500 max_hp buff") - - # Reject duplicate ATTACKER assignment to Sura - var r_dup = party.set_member_role(1, 3, SystemClass.ROLE_ATTACKER) - _assert(r_dup["error_code"] == "ROLE_ALREADY_ASSIGNED", "Reject duplicate role assignment") - -func _test_exp_distribution_modes() -> void: - print("\n--- Test 4: Experience Distribution Modes ---") - var party = SystemClass.new(1, "PlayerA", 20) # Level 20 - party.add_member(2, "PlayerB", 80) # Level 80 - - # 1. PARITY (Equal mode) - party.set_exp_distribution_mode(1, SystemClass.EXP_MODE_PARITY) - var exp_parity = party.distribute_exp(1000) - _assert(exp_parity[1] == 500, "PlayerA gets 500 in PARITY mode") - _assert(exp_parity[2] == 500, "PlayerB gets 500 in PARITY mode") - - # 2. LEVEL (Weighted mode: 20% vs 80%) - party.set_exp_distribution_mode(1, SystemClass.EXP_MODE_LEVEL) - var exp_level = party.distribute_exp(1000) - _assert(exp_level[1] == 200, "PlayerA (Lv20) gets 200 (20%) in LEVEL mode") - _assert(exp_level[2] == 800, "PlayerB (Lv80) gets 800 (80%) in LEVEL mode") - -func _test_hp_percentage_synchronization() -> void: - print("\n--- Test 5: HP Gauge Percentage Synchronization ---") - var party = SystemClass.new(1, "PlayerA", 50) - party.sync_member_hp(1, 3000, 6000) - _assert(is_equal_approx(party.members[1].get_hp_percent(), 0.5), "HP percentage is exactly 50%") - - party.sync_member_hp(1, 6000, 6000) - _assert(is_equal_approx(party.members[1].get_hp_percent(), 1.0), "HP percentage is 100%") diff --git a/project/test_party_management_parity.gd.uid b/project/test_party_management_parity.gd.uid deleted file mode 100644 index dd5d07ce..00000000 --- a/project/test_party_management_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bcr88asr1j3qh diff --git a/project/test_pet_companion_parity.gd b/project/test_pet_companion_parity.gd deleted file mode 100644 index 187d532e..00000000 --- a/project/test_pet_companion_parity.gd +++ /dev/null @@ -1,160 +0,0 @@ -# test_pet_companion_parity.gd —— 40250 伴随宠物系统 1:1 纯净回归测试套件 -extends SceneTree - -const PetCompanionSystem = preload("res://pet_companion_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_pet_companion_parity (Direction 2: Pet Companion System 1:1) ===") - _test_pet_table_and_validity() - _test_summon_and_bonuses() - _test_dismiss_and_toggle() - _test_3d_movement_and_states() - _test_time_expiry() - _finish() - -func _test_pet_table_and_validity() -> void: - print("\n--- Test 1: Pet Table Configuration & Seal Validation ---") - var pet_sys := PetCompanionSystem.new() - - # Invalid seal item - var invalid_res = pet_sys.toggle_pet_seal({"vnum": 99999}, Vector3.ZERO) - _assert(invalid_res["ok"] == false and invalid_res["reason"] == "NOT_A_PET_SEAL", "Invalid seal rejected") - - # Check configured pets in PET_TABLE - _assert(PetCompanionSystem.PET_TABLE.has(53001), "Fire Phoenix (53001) in table") - _assert(PetCompanionSystem.PET_TABLE.has(53002), "Reindeer (53002) in table") - _assert(PetCompanionSystem.PET_TABLE.has(53003), "Ice Phoenix (53003) in table") - _assert(PetCompanionSystem.PET_TABLE.has(53005), "Bao Bao (53005) in table") - _assert(PetCompanionSystem.PET_TABLE.has(53006), "Leonidas (53006) in table") - - var f_phoenix = PetCompanionSystem.PET_TABLE[53001] - _assert(f_phoenix["mob_vnum"] == 34001, "Fire Phoenix mob vnum is 34001") - _assert(f_phoenix["bonuses"]["max_hp"] == 1500, "Fire Phoenix bonus Max HP is 1500") - _assert(f_phoenix["bonuses"]["monster_dmg_pct"] == 15, "Fire Phoenix bonus monster dmg is 15%") - -func _test_summon_and_bonuses() -> void: - print("\n--- Test 2: Summoning & Stat Injection ---") - var pet_sys := PetCompanionSystem.new() - var signal_data := { - "fired": false, - "name": "", - "vnum": 0 - } - - pet_sys.pet_summoned.connect(func(pname: String, vnum: int): - signal_data["fired"] = true - signal_data["name"] = pname - signal_data["vnum"] = vnum - ) - - var player_pos := Vector3(100.0, 0.0, 200.0) - var res = pet_sys.toggle_pet_seal({"vnum": 53001, "duration": 3600.0}, player_pos) - - _assert(res["ok"] == true, "Fire Phoenix summon succeeded") - _assert(res["action"] == "summon", "Action is summon") - _assert(pet_sys.is_summoned == true, "is_summoned is true") - _assert(pet_sys.current_seal_vnum == 53001, "current_seal_vnum is 53001") - _assert(signal_data["fired"] == true, "pet_summoned signal emitted") - _assert(signal_data["name"] == "火凤凰", "Summoned pet name is 火凤凰") - _assert(signal_data["vnum"] == 53001, "Summoned pet vnum is 53001") - - var bonuses = pet_sys.get_active_bonuses() - _assert(bonuses["max_hp"] == 1500, "Active bonus max_hp is 1500") - _assert(bonuses["monster_dmg_pct"] == 15, "Active bonus monster_dmg_pct is 15%") - -func _test_dismiss_and_toggle() -> void: - print("\n--- Test 3: Dismiss & Switch Pets ---") - var pet_sys := PetCompanionSystem.new() - var dismiss_data := { - "fired": false, - "name": "" - } - - pet_sys.pet_dismissed.connect(func(pname: String): - dismiss_data["fired"] = true - dismiss_data["name"] = pname - ) - - var player_pos := Vector3.ZERO - # Summon 53005 (Bao Bao) - pet_sys.toggle_pet_seal({"vnum": 53005}, player_pos) - _assert(pet_sys.is_summoned == true, "Bao Bao summoned") - _assert(pet_sys.get_active_bonuses()["critical_pct"] == 15, "Bao Bao grants 15% crit") - - # Toggle 53005 again -> dismiss - var res = pet_sys.toggle_pet_seal({"vnum": 53005}, player_pos) - _assert(res["ok"] == true and res["action"] == "dismiss", "Toggling same seal dismisses pet") - _assert(pet_sys.is_summoned == false, "Pet is no longer summoned") - _assert(dismiss_data["fired"] == true, "pet_dismissed signal emitted") - _assert(dismiss_data["name"] == "小胖熊猫", "Dismissed name matches") - _assert(pet_sys.get_active_bonuses().is_empty(), "No bonuses active when pet dismissed") - - # Summon 53006 (Leonidas) and then summon 53003 (Ice Phoenix) to switch - pet_sys.toggle_pet_seal({"vnum": 53006}, player_pos) - _assert(pet_sys.current_seal_vnum == 53006, "Leonidas active") - _assert(pet_sys.get_active_bonuses()["att_pct"] == 10, "Leonidas grants 10% att") - - # Switch directly to 53003 - var switch_res = pet_sys.toggle_pet_seal({"vnum": 53003}, player_pos) - _assert(switch_res["ok"] == true and switch_res["action"] == "summon", "Direct switch to Ice Phoenix succeeded") - _assert(pet_sys.current_seal_vnum == 53003, "Active pet is now Ice Phoenix") - _assert(pet_sys.get_active_bonuses()["def_pct"] == 15, "Ice Phoenix grants 15% def") - -func _test_3d_movement_and_states() -> void: - print("\n--- Test 4: 3D Movement & State Machine ---") - var pet_sys := PetCompanionSystem.new() - var player_pos := Vector3(0.0, 0.0, 0.0) - pet_sys.toggle_pet_seal({"vnum": 53001}, player_pos) - - # 1. When close (dist <= 2.0), state is wait - pet_sys.pet_position = Vector3(1.0, 0.0, 0.0) # dist = 1.0m <= 2.0m - pet_sys.update_movement(player_pos, 0.1) - _assert(pet_sys.pet_state == "wait", "Pet waits within 2.0m") - - # 2. When player moves to (10, 0, 0), dist is 9.0m (> 2.0m) - player_pos = Vector3(10.0, 0.0, 0.0) - var prev_pos = pet_sys.pet_position - pet_sys.update_movement(player_pos, 0.5) # dt = 0.5s -> move speed 4.5m/s -> move 2.25m towards player - _assert(pet_sys.pet_state == "run", "Pet state is run when following player") - _assert(pet_sys.pet_position.x > prev_pos.x, "Pet moved towards player") - - # 3. When player teleports far away (> 25.0m) - player_pos = Vector3(100.0, 0.0, 100.0) - pet_sys.update_movement(player_pos, 0.1) - var new_dist = pet_sys.pet_position.distance_to(player_pos) - _assert(new_dist < 5.0, "Pet instantly teleports near player when dist >= 25m") - _assert(pet_sys.pet_state == "wait", "Pet returns to wait state after teleport") - -func _test_time_expiry() -> void: - print("\n--- Test 5: Pet Duration Expiration ---") - var pet_sys := PetCompanionSystem.new() - pet_sys.toggle_pet_seal({"vnum": 53001, "duration": 10.0}, Vector3.ZERO) - _assert(pet_sys.is_summoned == true, "Pet summoned with 10s duration") - - pet_sys.update(4.0) - _assert(pet_sys.is_summoned == true, "Pet still active at 4s elapsed") - _assert(is_equal_approx(pet_sys.remaining_time, 6.0), "6s remaining") - - pet_sys.update(7.0) # Exceeds remaining - _assert(pet_sys.is_summoned == false, "Pet dismissed automatically when duration expires") - _assert(pet_sys.get_active_bonuses().is_empty(), "No bonuses after expiration") - -func _finish() -> void: - print("\n==========================================") - print("Pet Companion Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_pet_companion_parity.gd.uid b/project/test_pet_companion_parity.gd.uid deleted file mode 100644 index a4c11334..00000000 --- a/project/test_pet_companion_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cgyjhe8vqjehi diff --git a/project/test_pk_penalty_parity.gd b/project/test_pk_penalty_parity.gd deleted file mode 100644 index 677af779..00000000 --- a/project/test_pk_penalty_parity.gd +++ /dev/null @@ -1,122 +0,0 @@ -# test_pk_penalty_parity.gd —— 40250 恶名死亡惩罚与露西之戒保护 1:1 纯净回归测试套件 -extends SceneTree - -const PkPenaltySystem = preload("res://pk_penalty_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_pk_penalty_parity (Direction 3: PK Penalty & Lucy Ring 1:1) ===") - _test_safe_conditions_no_drop() - _test_lucy_ring_protection() - _test_negative_karma_drop_and_item_lock() - _test_zen_bean_and_cloak() - _finish() - -func _test_safe_conditions_no_drop() -> void: - print("\n--- Test 1: Safe Conditions (Level < 50 or Positive Karma) ---") - var pks := PkPenaltySystem.new() - var eq: Dictionary = {"weapon": {"vnum": 169, "name": "战神之剑+9"}} - var inv: Array = [{"vnum": 10, "name": "木剑"}] - - # Level < 50 with negative karma - var r1 = pks.process_death_penalty(45, -5000, eq, inv, 0.01) - _assert(r1["dropped"] == false, "No drop when player level < 50") - - # Positive karma at Level 75 - var r2 = pks.process_death_penalty(75, 8000, eq, inv, 0.01) - _assert(r2["dropped"] == false, "No drop when alignment >= 0 (Noble)") - -func _test_lucy_ring_protection() -> void: - print("\n--- Test 2: Lucy's Ring (70043) Shatter Protection ---") - var pks := PkPenaltySystem.new() - var eq: Dictionary = { - "weapon": {"vnum": 169, "name": "战神之剑+9"}, - "ring1": {"vnum": PkPenaltySystem.ITEM_LUCY_RING, "name": "露西之戒"} - } - var inv: Array = [{"vnum": 11299, "name": "黑钢甲+9"}] - - var ring_signal := {"fired": false} - pks.lucy_ring_protected.connect(func(): ring_signal["fired"] = true) - - # Level 75, Cruel (-15,000 karma) - var res = pks.process_death_penalty(75, -15000, eq, inv, 0.01) - _assert(res["dropped"] == false, "No items dropped") - _assert(res["ring_shattered"] == true, "Lucy's Ring shattered") - _assert(eq["ring1"] == null, "Lucy's Ring consumed from equipment slot") - _assert(eq["weapon"] != null, "Valuable weapon preserved") - _assert(inv[0] != null, "Valuable armor in inventory preserved") - _assert(ring_signal["fired"] == true, "lucy_ring_protected signal fired") - -func _test_negative_karma_drop_and_item_lock() -> void: - print("\n--- Test 3: Drop Mechanics & Item Lock Immunity ---") - var pks := PkPenaltySystem.new() - var eq: Dictionary = { - "weapon": {"vnum": 169, "name": "战神之剑+9", "is_locked": true} # Locked! - } - var inv: Array = [ - {"vnum": 11299, "name": "黑钢甲+9", "is_locked": true}, # Locked! - {"vnum": 50054, "name": "胡萝卜", "is_locked": false} # Unlocked, can drop! - ] - - var drop_signal := {"fired": false, "name": ""} - pks.item_dropped_on_death.connect(func(it: Dictionary): - drop_signal["fired"] = true - drop_signal["name"] = it.get("name", "") - ) - - # Level 75, Malicious (-5000 karma, 10% chance). Forced roll 0.05 <= 0.10 - var res = pks.process_death_penalty(75, -5000, eq, inv, 0.05) - _assert(res["dropped"] == true, "Death drop triggered") - _assert(res["dropped_item"]["vnum"] == 50054, "Unlocked carrot dropped") - _assert(inv[1] == null, "Carrot removed from inventory") - _assert(eq["weapon"] != null, "Locked weapon was NOT dropped") - _assert(inv[0] != null, "Locked armor was NOT dropped") - _assert(drop_signal["fired"] == true and drop_signal["name"] == "胡萝卜", "item_dropped_on_death signal fired") - -func _test_zen_bean_and_cloak() -> void: - print("\n--- Test 4: Zen Bean (70102) & Peace Cloak (70048) ---") - var pks := PkPenaltySystem.new() - var inv: Array = [ - {"vnum": PkPenaltySystem.ITEM_ZEN_BEAN, "name": "禅豆", "count": 2} - ] - - # 1. Use on positive karma rejected - var r_bad = pks.use_zen_bean(0, inv, 1000) - _assert(r_bad["ok"] == false and r_bad["reason"] == "NOT_EVIL", "Cannot use Zen Bean if alignment >= 0") - - # 2. Use on -1200 karma -> -700 karma - var r1 = pks.use_zen_bean(0, inv, -1200) - _assert(r1["ok"] == true, "Zen bean consumed") - _assert(r1["new_alignment"] == -700, "Alignment restored by +500 to -700") - _assert(inv[0]["count"] == 1, "1 Zen bean remaining in stack") - - # 3. Use on -200 karma -> capped at 0 - var r2 = pks.use_zen_bean(0, inv, -200) - _assert(r2["ok"] == true and r2["new_alignment"] == 0, "Alignment capped at 0 (neutral)") - _assert(inv[0] == null, "Stack consumed") - - # 4. Peace Cloak hides red name - var eq_with_cloak := {"body": null, "cloak": {"vnum": PkPenaltySystem.ITEM_CLOAK_OF_PEACE}} - _assert(pks.is_red_name_hidden(eq_with_cloak) == true, "Cloak of Peace hides red name") - - var eq_no_cloak := {"body": null} - _assert(pks.is_red_name_hidden(eq_no_cloak) == false, "No cloak reveals red name") - -func _finish() -> void: - print("\n==========================================") - print("PK Penalty Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_pk_penalty_parity.gd.uid b/project/test_pk_penalty_parity.gd.uid deleted file mode 100644 index 4e521942..00000000 --- a/project/test_pk_penalty_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bxghje5i2w0ks diff --git a/project/test_player_trade_exchange_parity.gd b/project/test_player_trade_exchange_parity.gd deleted file mode 100644 index 40267af0..00000000 --- a/project/test_player_trade_exchange_parity.gd +++ /dev/null @@ -1,223 +0,0 @@ -# test_player_trade_exchange_parity.gd —— 40250 面对面交易与防调包双向锁定系统回归测试 -extends SceneTree - -const SystemClass = preload("res://player_trade_exchange_system.gd") - -var _passed: int = 0 -var _failed: int = 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_player_trade_exchange_parity (Batch 26 Direction 3) ===") - _test_trade_session_initialization() - _test_untradeable_items_rejection() - _test_anti_swap_lock_reset_security() - _test_gold_offer_validation() - _test_successful_atomic_exchange() - _test_insufficient_space_rejection() - _test_cancel_trade() - - print("\n=== Result: %d Passed, %d Failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - else: - printerr("SOME TESTS FAILED!\n") - quit(0 if _failed == 0 else 1) - -func _create_player(id: int, name: String, gold: int, inv_size: int = 10) -> Dictionary: - var inv: Array = [] - for i in range(inv_size): - inv.append(null) - return { - "id": id, - "name": name, - "gold": gold, - "inv": inv - } - -func _test_trade_session_initialization() -> void: - print("\n--- Test 1: Trade Session Initialization & Distance ---") - var trade = SystemClass.new() - var p1 = _create_player(101, "Alice", 500000) - var p2 = _create_player(102, "Bob", 300000) - - # 1. Reject distance > 1000 - var r_dist = trade.start_exchange(p1, p2, 1200.0) - _assert(r_dist["error_code"] == "TOO_FAR", "Reject trade if distance > 1000") - - # 2. Reject self trade - var r_self = trade.start_exchange(p1, p1, 100.0) - _assert(r_self["error_code"] == "CANNOT_TRADE_SELF", "Reject trade with self") - - # 3. Successful start - var r_ok = trade.start_exchange(p1, p2, 100.0) - _assert(r_ok["success"] == true, "Trade session opened successfully") - _assert(trade.is_active == true, "Trade is active") - -func _test_untradeable_items_rejection() -> void: - print("\n--- Test 2: Untradeable Items Rejection ---") - var trade = SystemClass.new() - var p1 = _create_player(101, "Alice", 500000) - var p2 = _create_player(102, "Bob", 300000) - - p1["inv"][0] = { "vnum": 1, "name": "Normal Item", "anti_give": false, "is_soulbound": false } - p1["inv"][1] = { "vnum": 2, "name": "Bound Item", "anti_give": false, "is_soulbound": true } - p1["inv"][2] = { "vnum": 3, "name": "Anti-Give Item", "anti_give": true, "is_soulbound": false } - p1["inv"][3] = { "vnum": 4, "name": "Equipped Item", "is_equipped": true } - - trade.start_exchange(p1, p2) - - # Soulbound rejected - var r_bound = trade.add_item(101, 1, 0) - _assert(r_bound["error_code"] == "ITEM_UNTRADEABLE", "Soulbound item rejected from trade") - - # Anti-give rejected - var r_antigive = trade.add_item(101, 2, 0) - _assert(r_antigive["error_code"] == "ITEM_UNTRADEABLE", "Anti-give flag item rejected from trade") - - # Equipped rejected - var r_equip = trade.add_item(101, 3, 0) - _assert(r_equip["error_code"] == "ITEM_UNTRADEABLE", "Equipped item rejected from trade") - - # Normal item accepted - var r_norm = trade.add_item(101, 0, 0) - _assert(r_norm["success"] == true, "Tradeable item successfully added to slot 0") - -func _test_anti_swap_lock_reset_security() -> void: - print("\n--- Test 3: 40250 Anti-Swap Security (Lock Reset on Modification) ---") - var trade = SystemClass.new() - var p1 = _create_player(101, "Alice", 500000) - var p2 = _create_player(102, "Bob", 300000) - p1["inv"][0] = { "vnum": 10, "name": "Sword" } - p2["inv"][0] = { "vnum": 20, "name": "Shield" } - p2["inv"][1] = { "vnum": 30, "name": "Dagger" } - - trade.start_exchange(p1, p2) - trade.add_item(101, 0, 0) - trade.add_item(102, 0, 0) - - # Alice locks ready - trade.set_ready(101, true) - _assert(trade.side_a.is_ready == true, "Alice locked ready") - - # Bob attempts to add another item (or swap item) - trade.add_item(102, 1, 1) - _assert(trade.side_a.is_ready == false, "Alice's ready lock instantly revoked on Bob item add") - _assert(trade.side_b.is_ready == false, "Bob's ready lock also reset") - - # Alice locks ready again - trade.set_ready(101, true) - _assert(trade.side_a.is_ready == true, "Alice locked ready again") - - # Bob modifies gold - trade.set_gold(102, 10000) - _assert(trade.side_a.is_ready == false, "Alice's ready lock instantly revoked on Bob gold change") - - # Alice locks ready again - trade.set_ready(101, true) - - # Bob removes item - trade.remove_item(102, 1) - _assert(trade.side_a.is_ready == false, "Alice's ready lock instantly revoked on Bob item remove") - -func _test_gold_offer_validation() -> void: - print("\n--- Test 4: Gold Offer Validation ---") - var trade = SystemClass.new() - var p1 = _create_player(101, "Alice", 100000) - var p2 = _create_player(102, "Bob", 50000) - - trade.start_exchange(p1, p2) - - # Reject offer exceeding gold - var r_fail = trade.set_gold(101, 150000) - _assert(r_fail["error_code"] == "INSUFFICIENT_GOLD", "Reject gold offer exceeding owned gold") - - # Accept valid offer - var r_ok = trade.set_gold(101, 80000) - _assert(r_ok["success"] == true, "Valid gold offer accepted") - _assert(trade.side_a.offered_gold == 80000, "Alice offered 80,000 gold") - -func _test_successful_atomic_exchange() -> void: - print("\n--- Test 5: Successful Atomic Trade Execution ---") - var trade = SystemClass.new() - var p1 = _create_player(101, "Alice", 500000, 5) - var p2 = _create_player(102, "Bob", 200000, 5) - - p1["inv"][0] = { "vnum": 101, "name": "Dragon Sword+9" } - p2["inv"][0] = { "vnum": 202, "name": "Black Steel Armor+9" } - - trade.start_exchange(p1, p2) - - # Alice puts Dragon Sword and 100,000 gold - trade.add_item(101, 0, 0) - trade.set_gold(101, 100000) - - # Bob puts Black Steel Armor and 40,000 gold - trade.add_item(102, 0, 0) - trade.set_gold(102, 40000) - - # Both confirm - var r_ready_a = trade.set_ready(101, true) - _assert(r_ready_a["trade_completed"] == false, "Trade not completed after only Alice ready") - - var r_ready_b = trade.set_ready(102, true) - _assert(r_ready_b["trade_completed"] == true, "Trade atomically completed when both ready") - _assert(trade.is_active == false, "Trade session closed") - - # Check Gold: - # Alice: 500,000 - 100,000 + 40,000 = 440,000 - _assert(p1["gold"] == 440000, "Alice gold correctly calculated (440,000)") - # Bob: 200,000 - 40,000 + 100,000 = 260,000 - _assert(p2["gold"] == 260000, "Bob gold correctly calculated (260,000)") - - # Check Items: - # Alice now has Black Steel Armor - var alice_has_armor := false - for it in p1["inv"]: - if it != null and it.get("vnum") == 202: - alice_has_armor = true - _assert(alice_has_armor == true, "Alice received Bob's Black Steel Armor") - - # Bob now has Dragon Sword - var bob_has_sword := false - for it in p2["inv"]: - if it != null and it.get("vnum") == 101: - bob_has_sword = true - _assert(bob_has_sword == true, "Bob received Alice's Dragon Sword") - -func _test_insufficient_space_rejection() -> void: - print("\n--- Test 6: CheckSpace Rejection (Inventory Full) ---") - var trade = SystemClass.new() - # Bob's inventory has only 1 slot, and it is full with an item he is NOT trading - var p1 = _create_player(101, "Alice", 500000, 5) - var p2 = _create_player(102, "Bob", 200000, 1) - - p1["inv"][0] = { "vnum": 101, "name": "Item A" } - p2["inv"][0] = { "vnum": 999, "name": "Untraded Item (Full)" } # Not trading this - - trade.start_exchange(p1, p2) - trade.add_item(101, 0, 0) # Alice offers Item A - - trade.set_ready(101, true) - var res = trade.set_ready(102, true) - _assert(res["trade_completed"] == false, "Trade aborted due to space check") - _assert(res["error_code"] == "PLAYER_B_INSUFFICIENT_SPACE", "Detected Bob has insufficient inventory space") - -func _test_cancel_trade() -> void: - print("\n--- Test 7: Trade Cancellation ---") - var trade = SystemClass.new() - var p1 = _create_player(101, "Alice", 500000, 5) - var p2 = _create_player(102, "Bob", 200000, 5) - - trade.start_exchange(p1, p2) - _assert(trade.is_active == true, "Trade started") - - trade.cancel_trade() - _assert(trade.is_active == false, "Trade canceled and inactive") diff --git a/project/test_player_trade_exchange_parity.gd.uid b/project/test_player_trade_exchange_parity.gd.uid deleted file mode 100644 index 3a29f5d9..00000000 --- a/project/test_player_trade_exchange_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cbovg1tm3oxxc diff --git a/project/test_point_reset_parity.gd b/project/test_point_reset_parity.gd deleted file mode 100644 index 6b242022..00000000 --- a/project/test_point_reset_parity.gd +++ /dev/null @@ -1,99 +0,0 @@ -# test_point_reset_parity.gd -# 检验 40250 属性点/技能重置老妪洗点与两阶段确认系统 -extends SceneTree - -const PointResetSystem = preload("res://point_reset_system.gd") - -func _init() -> void: - print("--- 开始测试 40250 洗点老妪属性与技能重置系统 (test_point_reset_parity) ---") - var sys = PointResetSystem.new() - var fails: int = 0 - - # 1. 初始化战士数据 (Lv 25, 2000 金币, CON 20, INT 3, STR 50, DEX 10, 剩余属性点 2) - # 战士初始: CON 4, INT 3, STR 6, DEX 3 - # 投入点数: CON +16, STR +44, DEX +7 = 67 点 - sys.init_player_data( - 0, # WARRIOR - 25, # Lv 25 - 2000, # 2000 Yang - {"CON": 20, "INT": 3, "STR": 50, "DEX": 10}, - 2, # 2 unallocated stat points - 1, # 分支 1 (体战) - 0, - {1: 17, 2: 5} # 17级剑气, 5级狂暴 - ) - - # 2. 两阶段弹窗生命周期测试 - sys.open_dialog() - if sys.state != PointResetSystem.DialogState.INITIAL_PROMPT: - printerr("FAIL: 打开后未进入 INITIAL_PROMPT 状态: ", sys.state) - fails += 1 - - var opened_confirm = sys.open_confirm_dialog() - if not opened_confirm or sys.state != PointResetSystem.DialogState.CONFIRMATION: - printerr("FAIL: 未成功转入 CONFIRMATION 阶段: ", sys.state) - fails += 1 - - # 3. 接受洗点并核对扣款与属性全额回退 - var reset_ok = sys.accept_reset() - if not reset_ok: - printerr("FAIL: 确认洗点失败!") - fails += 1 - if sys.player_gold != 1500: # 2000 - 500 = 1500 - printerr("FAIL: 500 金币扣除错误,剩余金币: ", sys.player_gold) - fails += 1 - if sys.last_packet_sent != "/pointreset": - printerr("FAIL: 发送协议指令不符: ", sys.last_packet_sent) - fails += 1 - - # 属性应重置为战士初始值 [CON:4, INT:3, STR:6, DEX:3] - if sys.current_stats["CON"] != 4 or sys.current_stats["STR"] != 6 or sys.current_stats["DEX"] != 3 or sys.current_stats["INT"] != 3: - printerr("FAIL: 四维属性未恢复初始基准: ", sys.current_stats) - fails += 1 - - # 未分配属性点应为: 2(原未分配) + 67(洗回) = 69 点 - if sys.unallocated_stat_points != 69: - printerr("FAIL: 属性点回退总额不符,期望 69,实际: ", sys.unallocated_stat_points) - fails += 1 - - if sys.state != PointResetSystem.DialogState.CLOSED: - printerr("FAIL: 洗点完成后弹窗未关闭: ", sys.state) - fails += 1 - - # 4. 金币不足时拦截测试 - sys.player_gold = 200 - sys.open_dialog() - var confirm_when_broke = sys.open_confirm_dialog() - if confirm_when_broke or sys.state != PointResetSystem.DialogState.CLOSED: - printerr("FAIL: 金币不足时未能正确阻断!") - fails += 1 - - # 5. 老妇人 30 级以下技能重洗测试 - sys.player_gold = 50000 - var skill_reset_ok = sys.reset_skills_at_old_woman() - if not skill_reset_ok: - printerr("FAIL: 25 级角色老妇人洗技能失败!") - fails += 1 - if sys.current_skill_tree != 0: - printerr("FAIL: 洗技能后技能树分支应重置为 0: ", sys.current_skill_tree) - fails += 1 - if sys.active_skills.size() != 0: - printerr("FAIL: 洗技能后技能字典应清空: ", sys.active_skills) - fails += 1 - if sys.unallocated_skill_points != 22: # 17 + 5 = 22 - printerr("FAIL: 技能点退回不符,期望 22,实际: ", sys.unallocated_skill_points) - fails += 1 - - # 6. 超出 30 级老妇人洗技能拒绝测试 - sys.player_level = 31 - var high_lv_reset = sys.reset_skills_at_old_woman() - if high_lv_reset: - printerr("FAIL: 超过 30 级老妇人洗技能未被拒绝!") - fails += 1 - - if fails == 0: - print("PASS: test_point_reset_parity (8/8 全部通过)") - quit(0) - else: - printerr("FAILED: test_point_reset_parity (", fails, " 项失败)") - quit(1) diff --git a/project/test_point_reset_parity.gd.uid b/project/test_point_reset_parity.gd.uid deleted file mode 100644 index f14eafc1..00000000 --- a/project/test_point_reset_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bjesoi811yncf diff --git a/project/test_polymorph_marble_parity.gd b/project/test_polymorph_marble_parity.gd deleted file mode 100644 index b6eed071..00000000 --- a/project/test_polymorph_marble_parity.gd +++ /dev/null @@ -1,132 +0,0 @@ -# test_polymorph_marble_parity.gd —— 40250 官方变身宝珠与怪物精魂附体系统 1:1 回归测试套件 -extends SceneTree - -const PolymorphMarbleSystem = preload("res://polymorph_marble_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_polymorph_marble_parity (Direction 1: Polymorph Marble 1:1) ===") - _test_marble_transformation_and_bonuses() - _test_skill_mastery_durations() - _test_mount_and_skill_restrictions() - _test_expiration_and_stat_reversion() - _test_serialization() - _finish() - -func _test_marble_transformation_and_bonuses() -> void: - print("\n--- Test 1: Transformation & Bonus Stat Injection ---") - var sys := PolymorphMarbleSystem.new() - var player := {"attack_power": 500, "defense": 300, "move_speed": 100} - - var start_sig := {"fired": false, "mob": 0, "name": "", "bonus_type": 0} - sys.polymorph_started.connect(func(m: int, n: String, _d: float, bt: int, _bv: int): - start_sig["fired"] = true - start_sig["mob"] = m - start_sig["name"] = n - start_sig["bonus_type"] = bt - ) - - # 1. Barbarian Infantry (501) - Attack Bonus +40 - var marble_barbarian := {"vnum": PolymorphMarbleSystem.VNUM_POLYMORPH_MARBLE, "mob_vnum": 501} - var res1 = sys.polymorph_character(marble_barbarian, PolymorphMarbleSystem.SKILL_NORMAL, player) - _assert(res1["ok"] == true, "Polymorphed into Barbarian Infantry") - _assert(sys.is_polymorphed == true, "is_polymorphed is true") - _assert(player["attack_power"] == 540, "Attack power increased 500 -> 540 (+40)") - _assert(start_sig["fired"] == true, "polymorph_started signal emitted") - _assert(start_sig["mob"] == 501, "Mob vnum is 501") - - # 2. Directly switch to White Hair Spider (492) - Def Bonus +50 - var marble_spider := {"vnum": PolymorphMarbleSystem.VNUM_POLYMORPH_MARBLE, "mob_vnum": 492} - var res2 = sys.polymorph_character(marble_spider, PolymorphMarbleSystem.SKILL_NORMAL, player) - _assert(res2["ok"] == true, "Switched into White Hair Spider") - _assert(player["attack_power"] == 500, "Previous attack bonus (+40) cleared cleanly") - _assert(player["defense"] == 350, "Defense increased 300 -> 350 (+50)") - -func _test_skill_mastery_durations() -> void: - print("\n--- Test 2: 40250 Mastery Skill Durations ---") - _assert(PolymorphMarbleSystem.get_duration_by_mastery(PolymorphMarbleSystem.SKILL_NORMAL) == 600.0, "Normal: 600s (10 min)") - _assert(PolymorphMarbleSystem.get_duration_by_mastery(PolymorphMarbleSystem.SKILL_MASTER) == 900.0, "Master: 900s (15 min)") - _assert(PolymorphMarbleSystem.get_duration_by_mastery(PolymorphMarbleSystem.SKILL_GRAND_MASTER) == 1200.0, "Grand Master: 1200s (20 min)") - _assert(PolymorphMarbleSystem.get_duration_by_mastery(PolymorphMarbleSystem.SKILL_PERFECT_MASTER) == 1500.0, "Perfect Master: 1500s (25 min)") - -func _test_mount_and_skill_restrictions() -> void: - print("\n--- Test 3: Mount & Character Skill Restrictions ---") - var sys := PolymorphMarbleSystem.new() - var player := {"attack_power": 100} - _assert(sys.can_mount_horse() == true, "Normal character can mount horse") - _assert(sys.can_cast_player_skills() == true, "Normal character can cast skills") - - # Transform - sys.polymorph_character({"vnum": PolymorphMarbleSystem.VNUM_POLYMORPH_MARBLE, "mob_vnum": 101}, PolymorphMarbleSystem.SKILL_NORMAL, player) - _assert(sys.can_mount_horse() == false, "Polymorphed character CANNOT mount horse") - _assert(sys.can_cast_player_skills() == false, "Polymorphed character CANNOT cast player skills") - - # Revert - sys.revert_polymorph(player) - _assert(sys.can_mount_horse() == true, "Reverted character can mount horse again") - _assert(sys.can_cast_player_skills() == true, "Reverted character can cast skills again") - -func _test_expiration_and_stat_reversion() -> void: - print("\n--- Test 4: Expiration Heartbeat & Stat Reversion ---") - var sys := PolymorphMarbleSystem.new() - var player := {"move_speed": 100} - var marble_fox := {"vnum": PolymorphMarbleSystem.VNUM_POLYMORPH_MARBLE, "mob_vnum": 1901} # 九尾妖狐 (+30 ms) - - var end_sig := {"fired": false, "mob": 0} - sys.polymorph_ended.connect(func(m: int, _n: String): - end_sig["fired"] = true - end_sig["mob"] = m - ) - - sys.polymorph_character(marble_fox, PolymorphMarbleSystem.SKILL_NORMAL, player) # 600s - _assert(player["move_speed"] == 130, "Move speed boosted to 130") - - # Advance 500s (100s left) - sys.update(500.0, player) - _assert(sys.is_polymorphed == true, "Still polymorphed at 500s") - _assert(player["move_speed"] == 130, "Speed boost still active") - - # Advance remaining 100s -> timeout - sys.update(100.0, player) - _assert(sys.is_polymorphed == false, "Polymorph expired after 600s") - _assert(player["move_speed"] == 100, "Move speed reversed cleanly to 100") - _assert(end_sig["fired"] == true, "polymorph_ended signal emitted") - _assert(end_sig["mob"] == 1901, "Reported Nine Tails mob vnum") - -func _test_serialization() -> void: - print("\n--- Test 5: Serialization & Restoration ---") - var sys1 := PolymorphMarbleSystem.new() - var player := {"defense": 200} - sys1.polymorph_character({"vnum": PolymorphMarbleSystem.VNUM_POLYMORPH_MARBLE, "mob_vnum": 492}, PolymorphMarbleSystem.SKILL_MASTER, player) - sys1.update(300.0, player) # 600s remaining - - var data = sys1.serialize() - _assert(data["is_polymorphed"] == true, "Serialized is_polymorphed") - _assert(data["active_mob_vnum"] == 492, "Serialized mob vnum") - _assert(data["active_stat_key"] == "defense", "Serialized stat key") - - var sys2 := PolymorphMarbleSystem.new() - sys2.deserialize(data) - _assert(sys2.is_polymorphed == true, "Restored is_polymorphed") - _assert(sys2.active_mob_vnum == 492, "Restored mob vnum 492") - _assert(sys2.active_mob_name == "白发蜘蛛", "Restored name") - _assert(is_equal_approx(sys2.time_remaining, 600.0), "Restored time remaining") - -func _finish() -> void: - print("\n=======================================================") - print("Polymorph Marble Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_polymorph_marble_parity.gd.uid b/project/test_polymorph_marble_parity.gd.uid deleted file mode 100644 index 4a66ee6a..00000000 --- a/project/test_polymorph_marble_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://p25r13uramvn diff --git a/project/test_polymorph_parity.gd b/project/test_polymorph_parity.gd deleted file mode 100644 index aff1a142..00000000 --- a/project/test_polymorph_parity.gd +++ /dev/null @@ -1,116 +0,0 @@ -# test_polymorph_parity.gd —— 40250 变身球与变身术技能 1:1 纯净回归测试套件 -extends SceneTree - -const PolymorphSystem = preload("res://polymorph_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_polymorph_parity (Direction 2: Polymorph System 1:1) ===") - _test_marble_item_and_prerequisites() - _test_duration_and_skill_scaling() - _test_transformation_stat_boost() - _test_combat_and_action_restrictions() - _test_termination_and_death() - _finish() - -func _test_marble_item_and_prerequisites() -> void: - print("\n--- Test 1: Marble Item & Transformation Prerequisites ---") - var ps := PolymorphSystem.new() - var marble := {"vnum": 70104, "mob_vnum": 101} # Wild Boar - var non_marble := {"vnum": 10, "mob_vnum": 101} - - # Non marble rejected - var res = ps.use_polymorph_marble(non_marble, false, false) - _assert(res["ok"] == false and res["reason"] == "NOT_A_MARBLE", "Non-marble item rejected") - - # Dead player rejected - res = ps.use_polymorph_marble(marble, true, false) - _assert(res["ok"] == false and res["reason"] == "PLAYER_DEAD", "Dead player cannot transform") - - # Mounted player rejected - res = ps.use_polymorph_marble(marble, false, true) - _assert(res["ok"] == false and res["reason"] == "PLAYER_MOUNTED", "Mounted player cannot transform") - - # Peaceful on foot succeeds - res = ps.use_polymorph_marble(marble, false, false) - _assert(res["ok"] == true, "Transformation succeeds on foot while alive") - _assert(ps.is_polymorphed == true, "Player is now in polymorphed state") - _assert(ps.current_mob_vnum == 101, "Current mob vnum is 101 (Wild Boar)") - -func _test_duration_and_skill_scaling() -> void: - print("\n--- Test 2: Duration Scaling with Skill Level (1~P) ---") - # Level 1: 620s (~10 min) - _assert(PolymorphSystem.get_duration_by_skill(1) == 620.0, "Skill Lv1: 620s") - # Level 20 (M1): 1200s (20 min) - _assert(PolymorphSystem.get_duration_by_skill(20) == 1200.0, "Skill M1 (Lv20): 1200s (20 min)") - # Level 30 (G1): 1500s (25 min) - _assert(PolymorphSystem.get_duration_by_skill(30) == 1500.0, "Skill G1 (Lv30): 1500s (25 min)") - # Level 40 (P): 2100s (35 min) - _assert(PolymorphSystem.get_duration_by_skill(40) == 2100.0, "Skill P (Lv40): 2100s (35 min)") - -func _test_transformation_stat_boost() -> void: - print("\n--- Test 3: Monster Attributes & Skill Scaling Boost ---") - var ps := PolymorphSystem.new() - ps.poly_skill_level = 40 # P grade (multiplier 1.0 + 40*0.025 = 2.0x) - - var tiger_marble := {"vnum": 70104, "mob_vnum": 114} # White Tiger (base atk: 85) - var res = ps.use_polymorph_marble(tiger_marble, false, false) - _assert(res["ok"] == true, "White tiger transformation succeeds") - _assert(ps.bonus_atk == 170, "P-Grade Tiger ATK is doubled: 85 * 2.0 = 170") - _assert(ps.bonus_def == 40, "Tiger DEF is 40") - _assert(ps.bonus_speed == 140, "Tiger movement speed is 140") - -func _test_combat_and_action_restrictions() -> void: - print("\n--- Test 4: Action Restrictions (Skills, Mount, Weapon) ---") - var ps := PolymorphSystem.new() - var marble := {"vnum": 70104, "mob_vnum": 110} # Bear - ps.use_polymorph_marble(marble, false, false) - - # Active skills blocked - _assert(ps.can_use_skill(1) == false, "Warrior skill 1 (Three-way Cut) blocked") - _assert(ps.can_use_skill(2) == false, "Warrior skill 2 (Sword Spin) blocked") - _assert(ps.can_use_skill(129) == true, "Polymorph skill 129 permitted") - - # Weapon change blocked - _assert(ps.can_change_weapon() == false, "Weapon swapping blocked while transformed") - - # Mounting blocked - _assert(ps.can_mount() == false, "Mounting horse blocked while transformed") - -func _test_termination_and_death() -> void: - print("\n--- Test 5: Revert on Countdown Expiry & Death ---") - var ps := PolymorphSystem.new() - var marble := {"vnum": 70104, "mob_vnum": 101} - ps.use_polymorph_marble(marble, false, false) - _assert(ps.is_polymorphed == true, "Polymorphed initially") - - # Fast forward time to expire - ps.update(ps.remaining_time + 1.0) - _assert(ps.is_polymorphed == false, "Polymorph ended after countdown expired") - _assert(ps.bonus_atk == 0, "Bonus ATK cleared") - _assert(ps.can_use_skill(1) == true, "Skills unlocked again after polymorph ends") - - # Re-transform and test death - ps.use_polymorph_marble(marble, false, false) - _assert(ps.is_polymorphed == true, "Re-transformed") - ps.on_player_death() - _assert(ps.is_polymorphed == false, "Polymorph immediately cancelled on player death") - -func _finish() -> void: - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - quit(0) - else: - printerr("SOME TESTS FAILED!\n") - quit(1) diff --git a/project/test_polymorph_parity.gd.uid b/project/test_polymorph_parity.gd.uid deleted file mode 100644 index 24b3d5a3..00000000 --- a/project/test_polymorph_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cj2kik8aknd7s diff --git a/project/test_portable_service_parity.gd b/project/test_portable_service_parity.gd deleted file mode 100644 index fe948045..00000000 --- a/project/test_portable_service_parity.gd +++ /dev/null @@ -1,92 +0,0 @@ -# test_portable_service_parity.gd —— 40250 随身便民服务道具 1:1 纯净回归测试套件 -extends SceneTree - -const PortableServiceSystem = preload("res://portable_service_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_portable_service_parity (Direction 2: Portable Services 1:1) ===") - _test_portable_safebox_usage() - _test_portable_blacksmith_usage() - _test_portable_store_and_buying() - _finish() - -func _test_portable_safebox_usage() -> void: - print("\n--- Test 1: Portable Safebox (70005) ---") - var pss := PortableServiceSystem.new() - var inv: Array = [ - {"vnum": PortableServiceSystem.ITEM_PORTABLE_SAFEBOX, "name": "随身仓库卷轴", "count": 3} - ] - - var open_signal := {"fired": false, "svc": ""} - pss.service_opened.connect(func(s: String): - open_signal["fired"] = true - open_signal["svc"] = s - ) - - var res = pss.use_portable_safebox(0, inv) - _assert(res["ok"] == true, "Portable safebox opened") - _assert(pss.active_service == "safebox", "active_service is safebox") - _assert(inv[0]["count"] == 2, "1 scroll consumed (3 -> 2)") - _assert(open_signal["fired"] == true and open_signal["svc"] == "safebox", "service_opened signal fired") - -func _test_portable_blacksmith_usage() -> void: - print("\n--- Test 2: Portable Blacksmith (70006) ---") - var pss := PortableServiceSystem.new() - var inv: Array = [ - {"vnum": PortableServiceSystem.ITEM_PORTABLE_BLACKSMITH, "name": "随身铁匠手稿", "count": 1} - ] - - var res = pss.use_portable_blacksmith(0, inv) - _assert(res["ok"] == true, "Portable blacksmith opened") - _assert(pss.active_service == "blacksmith", "active_service is blacksmith") - _assert(inv[0] == null, "Single scroll consumed to null") - -func _test_portable_store_and_buying() -> void: - print("\n--- Test 3: Portable General Store (70007) & Purchasing ---") - var pss := PortableServiceSystem.new() - var player_data := {"gold": 1000} # Less than 3200 - var inv: Array = [ - {"vnum": PortableServiceSystem.ITEM_PORTABLE_GENERAL_STORE, "name": "随身杂货铺契约", "count": 1}, - null - ] - - # 1. Open store - var r_open = pss.use_portable_store(0, inv) - _assert(r_open["ok"] == true, "Portable store opened") - _assert(pss.active_service == "store", "active_service is store") - _assert(r_open["goods"].size() == 4, "Store offers 4 items") - - # 2. Buy with insufficient Yang - var r_poor = pss.buy_goods_from_portable_store(0, player_data, inv) - _assert(r_poor["ok"] == false and r_poor["reason"] == "INSUFFICIENT_YANG", "Blocked with insufficient Yang") - - # 3. Buy 200x Big Red Potions (price: 3200) with 10,000 Yang - player_data["gold"] = 10000 - var r_buy = pss.buy_goods_from_portable_store(0, player_data, inv) - _assert(r_buy["ok"] == true, "Purchased red potions") - _assert(player_data["gold"] == 6800, "3200 Yang deducted (10000 -> 6800)") - - # Notice slot 0 was emptied when using the scroll, so purchase goes to slot 0! - var purchased_slot = r_buy["slot"] - _assert(purchased_slot >= 0 and inv[purchased_slot]["vnum"] == 27003, "Red potions received into inventory") - _assert(inv[purchased_slot]["count"] == 200, "200 count received") - -func _finish() -> void: - print("\n==========================================") - print("Portable Service Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_portable_service_parity.gd.uid b/project/test_portable_service_parity.gd.uid deleted file mode 100644 index a00b924a..00000000 --- a/project/test_portable_service_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bdfpphyulenv5 diff --git a/project/test_private_shop_bundle_parity.gd b/project/test_private_shop_bundle_parity.gd deleted file mode 100644 index 764a5c60..00000000 --- a/project/test_private_shop_bundle_parity.gd +++ /dev/null @@ -1,99 +0,0 @@ -# test_private_shop_bundle_parity.gd —— 验证方向 4:束发包袱皮与个人私有商店摆摊 40250 1:1 对齐 -extends SceneTree - -const PrivateShopSystem = preload("res://private_shop_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_private_shop_bundle_parity (Direction 4: Private Shop Bundle 1:1) ===") - test_bundle_item_check() - test_prerequisites() - test_sign_validation() - test_item_stocking() - test_shop_build_and_stall_vnum() - - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed > 0: - printerr("TEST FAILED!") - quit(1) - else: - print("ALL TESTS PASSED!") - quit(0) - -func test_bundle_item_check() -> void: - print("\n--- Test 1: Bundle Item Vnum Check (50200) ---") - _assert(PrivateShopSystem.is_bundle(50200), "50200 is Bundle (ITEM_BUNDLE)") - _assert(not PrivateShopSystem.is_bundle(10), "Sword is not a bundle") - _assert(not PrivateShopSystem.is_bundle(27001), "Potion is not a bundle") - -func test_prerequisites() -> void: - print("\n--- Test 2: Shop Opening Prerequisites (Battle, Mount, Death) ---") - # Fails when dead - var r_dead := PrivateShopSystem.can_open_private_shop(false, false, true) - _assert(not r_dead.ok and r_dead.code == "CANNOT_DEAD", "Rejected while dead") - - # Fails when mounted - var r_mount := PrivateShopSystem.can_open_private_shop(false, true, false) - _assert(not r_mount.ok and r_mount.code == "CANNOT_MOUNTED", "Rejected while mounted") - - # Fails when fighting - var r_fight := PrivateShopSystem.can_open_private_shop(true, false, false) - _assert(not r_fight.ok and r_fight.code == "CANNOT_FIGHTING", "Rejected while in battle") - - # Succeeds when peaceful - var r_ok := PrivateShopSystem.can_open_private_shop(false, false, false) - _assert(r_ok.ok, "Can open shop when peaceful on foot") - -func test_sign_validation() -> void: - print("\n--- Test 3: Shop Sign Text Validation ---") - var v_empty := PrivateShopSystem.validate_sign("") - _assert(not v_empty.ok and v_empty.code == "SIGN_EMPTY", "Empty sign rejected") - - var long_sign := "这是一个超级无敌非常长长长长长长长长长长长长长长长长的名字" # > 25 chars - var v_long := PrivateShopSystem.validate_sign(long_sign) - _assert(not v_long.ok and v_long.code == "SIGN_TOO_LONG", "Sign > 25 chars rejected") - - var v_valid := PrivateShopSystem.validate_sign("神装特价店") - _assert(v_valid.ok and v_valid.sign == "神装特价店", "Valid sign accepted") - -func test_item_stocking() -> void: - print("\n--- Test 4: Staging Items with Price into Stock ---") - var stock := {} - var sword_item := {"cell": 0, "vnum": 10, "count": 1} - - # Zero/negative price rejected - var r_zero := PrivateShopSystem.add_item_to_stock(stock, 0, sword_item, 0) - _assert(not r_zero.ok and r_zero.code == "INVALID_PRICE", "Zero price rejected") - - # Valid add - var r_add := PrivateShopSystem.add_item_to_stock(stock, 0, sword_item, 5000) - _assert(r_add.ok and stock.has(0), "Item added to stock slot 0") - _assert(stock[0].price == 5000, "Price set to 5000 gold") - - # Remove from stock - _assert(PrivateShopSystem.remove_item_from_stock(stock, 0), "Item removed from stock slot 0") - _assert(not stock.has(0), "Stock slot 0 is now empty") - -func test_shop_build_and_stall_vnum() -> void: - print("\n--- Test 5: Shop Build & Stall Model (Vnum 30000) ---") - var stock := { - 0: {"cell": 0, "vnum": 10, "count": 1, "price": 5000} - } - var stall_pos := Vector3(120, 0, 80) - - var res_build := PrivateShopSystem.build_private_shop("平民杂货摊", stock, stall_pos) - _assert(res_build.ok and res_build.code == "SHOP_OPENED", "Shop opened successfully") - _assert(res_build.stall_vnum == PrivateShopSystem.VNUM_SHOP_HOST_STALL, - "Stall model is 30000 (SHOP_HOST)") - _assert(res_build.items_count == 1, "Staged item count is 1") - _assert(res_build.position == stall_pos, "Stall spawned at current position") diff --git a/project/test_private_shop_bundle_parity.gd.uid b/project/test_private_shop_bundle_parity.gd.uid deleted file mode 100644 index 3ee633c5..00000000 --- a/project/test_private_shop_bundle_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b86xbo3226y6v diff --git a/project/test_privilege_empire_guild_parity.gd b/project/test_privilege_empire_guild_parity.gd deleted file mode 100644 index b65b039a..00000000 --- a/project/test_privilege_empire_guild_parity.gd +++ /dev/null @@ -1,120 +0,0 @@ -# test_privilege_empire_guild_parity.gd —— Metin2 40250 官方帝国与公会恩泽特权 1:1 测试套件 -extends SceneTree - -const PrivilegeEmpireGuildSystem = preload("res://privilege_empire_guild_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_privilege_empire_guild_parity (Direction 4: Privileges & Rates 1:1) ===") - - test_empire_and_guild_privileges() - test_effective_privilege_max_calculation() - test_unique_items_stacking() - test_privilege_expiration() - test_serialization() - - print("\n=======================================================") - print("Privilege System Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - - if _failed > 0: - quit(1) - else: - quit(0) - -func test_empire_and_guild_privileges() -> void: - print("\n--- Test 1: Empire & Guild Privileges Setup (40250 1:1) ---") - var system = PrivilegeEmpireGuildSystem.new() - - # 辛秀帝国 (1) 开启双倍经验 (+100% EXP, 持续 3600 秒) - system.set_empire_privilege(1, PrivilegeEmpireGuildSystem.PRIV_EXP, 100, 3600.0) - # 公会 (ID: 501) 开启爆率加成 (+50% Item Drop, 持续 1800 秒) - system.set_guild_privilege(501, PrivilegeEmpireGuildSystem.PRIV_ITEM_DROP, 50, 1800.0) - - # 辛秀国民查询经验加成 - var exp_bonus = system.get_effective_privilege(1001, 1, 0, PrivilegeEmpireGuildSystem.PRIV_EXP) - _assert(exp_bonus == 100, "Shinsoo citizen enjoys +100% EXP bonus") - - # 敌国 (Chunjo 2) 查询经验加成 - var enemy_exp = system.get_effective_privilege(2001, 2, 0, PrivilegeEmpireGuildSystem.PRIV_EXP) - _assert(enemy_exp == 0, "Chunjo citizen has 0% EXP bonus") - - # 公会 501 成员查询爆率加成 - var drop_bonus = system.get_effective_privilege(1001, 1, 501, PrivilegeEmpireGuildSystem.PRIV_ITEM_DROP) - _assert(drop_bonus == 50, "Guild 501 member enjoys +50% Item Drop bonus") - -func test_effective_privilege_max_calculation() -> void: - print("\n--- Test 2: Effective Privilege MAX Rule (40250 1:1) ---") - var system = PrivilegeEmpireGuildSystem.new() - - # 全服全境特权: +20% EXP - system.set_empire_privilege(0, PrivilegeEmpireGuildSystem.PRIV_EXP, 20, 3600.0) - # 辛秀专属帝国特权: +50% EXP - system.set_empire_privilege(1, PrivilegeEmpireGuildSystem.PRIV_EXP, 50, 3600.0) - # 辛秀第一公会 (888) 特权: +80% EXP - system.set_guild_privilege(888, PrivilegeEmpireGuildSystem.PRIV_EXP, 80, 3600.0) - - # 依据 40250 原版 priv_manager.cpp: - # val = MAX(val_ch, GetPrivByEmpire(0, type)) -> 20 - # val = MAX(val, GetPrivByEmpire(ch->GetEmpire(), type)) -> 50 - # val = MAX(val, GetPrivByGuild(ch->GetGuild()->GetID(), type)) -> 80 - var final_exp = system.get_effective_privilege(1001, 1, 888, PrivilegeEmpireGuildSystem.PRIV_EXP) - _assert(final_exp == 80, "Computed MAX value (80%) among global, empire, and guild") - -func test_unique_items_stacking() -> void: - print("\n--- Test 3: Experience Ring & Thief Glove Stacking (40250 1:1) ---") - var system = PrivilegeEmpireGuildSystem.new() - system.set_empire_privilege(1, PrivilegeEmpireGuildSystem.PRIV_EXP, 50, 3600.0) - system.set_empire_privilege(1, PrivilegeEmpireGuildSystem.PRIV_ITEM_DROP, 25, 3600.0) - - # 佩戴经验指环 (+50% EXP) -> 50 + 50 = 100% - var exp_with_ring = system.get_effective_privilege(1001, 1, 0, PrivilegeEmpireGuildSystem.PRIV_EXP, true, false) - _assert(exp_with_ring == 100, "EXP stacked with Experience Ring (+50% = 100%)") - - # 佩戴小偷手套 (+100% Drop) -> 25 + 100 = 125% - var drop_with_glove = system.get_effective_privilege(1001, 1, 0, PrivilegeEmpireGuildSystem.PRIV_ITEM_DROP, false, true) - _assert(drop_with_glove == 125, "Drop rate stacked with Thief Glove (+100% = 125%)") - -func test_privilege_expiration() -> void: - print("\n--- Test 4: Privilege Expiration with Delta Time ---") - var system = PrivilegeEmpireGuildSystem.new() - system.set_empire_privilege(1, PrivilegeEmpireGuildSystem.PRIV_EXP, 100, 500.0) - system.set_guild_privilege(777, PrivilegeEmpireGuildSystem.PRIV_GOLD_DROP, 30, 200.0) - - # 推进 300 秒 (公会 200 秒特权已过期,帝国 500 秒特权尚存) - system.update(300.0) - var guild_gold = system.get_effective_privilege(1001, 1, 777, PrivilegeEmpireGuildSystem.PRIV_GOLD_DROP) - _assert(guild_gold == 0, "Guild privilege expired after 300s") - var emp_exp = system.get_effective_privilege(1001, 1, 777, PrivilegeEmpireGuildSystem.PRIV_EXP) - _assert(emp_exp == 100, "Empire privilege still active") - - # 推进 300 秒 (帝国特权也过期) - system.update(300.0) - emp_exp = system.get_effective_privilege(1001, 1, 777, PrivilegeEmpireGuildSystem.PRIV_EXP) - _assert(emp_exp == 0, "Empire privilege expired after 600s") - -func test_serialization() -> void: - print("\n--- Test 5: Privilege State Serialization ---") - var system = PrivilegeEmpireGuildSystem.new() - system.set_empire_privilege(1, PrivilegeEmpireGuildSystem.PRIV_EXP, 75, 1000.0) - system.set_character_privilege(999, PrivilegeEmpireGuildSystem.PRIV_GOLD_DROP, 15) - - var data = system.serialize() - _assert(data.has("empire_privs") and data.has("char_privs"), "Serialized data") - - var restored = PrivilegeEmpireGuildSystem.new() - restored.deserialize(data) - var emp_exp = restored.get_effective_privilege(1001, 1, 0, PrivilegeEmpireGuildSystem.PRIV_EXP) - _assert(emp_exp == 75, "Restored 75% empire EXP") - var char_gold = restored.get_effective_privilege(999, 1, 0, PrivilegeEmpireGuildSystem.PRIV_GOLD_DROP) - _assert(char_gold == 15, "Restored 15% character gold drop") diff --git a/project/test_privilege_empire_guild_parity.gd.uid b/project/test_privilege_empire_guild_parity.gd.uid deleted file mode 100644 index 82a46d3c..00000000 --- a/project/test_privilege_empire_guild_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c3s7camybb8ax diff --git a/project/test_refine_effect_parity.gd b/project/test_refine_effect_parity.gd deleted file mode 100644 index fe5c5ec4..00000000 --- a/project/test_refine_effect_parity.gd +++ /dev/null @@ -1,87 +0,0 @@ -# test_refine_effect_parity.gd —— 验证方向 3:+7/+8/+9 装备专属发光特效 40250 1:1 对齐 -extends SceneTree - -const RefineEffectSystem = preload("res://refine_effect_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_refine_effect_parity (Direction 3: Refine Shine Effects 1:1) ===") - test_refine_level_extraction() - test_shine_threshold_check() - test_weapon_shine_specs() - test_armor_shine_specs() - test_particles_generation() - - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed > 0: - printerr("TEST FAILED!") - quit(1) - else: - print("ALL TESTS PASSED!") - quit(0) - -func test_refine_level_extraction() -> void: - print("\n--- Test 1: Vnum to Refine Level Extraction ---") - _assert(RefineEffectSystem.get_refine_level_from_vnum(10) == 0, "Sword+0 (vnum 10) is level 0") - _assert(RefineEffectSystem.get_refine_level_from_vnum(16) == 6, "Sword+6 (vnum 16) is level 6") - _assert(RefineEffectSystem.get_refine_level_from_vnum(17) == 7, "Sword+7 (vnum 17) is level 7") - _assert(RefineEffectSystem.get_refine_level_from_vnum(18) == 8, "Sword+8 (vnum 18) is level 8") - _assert(RefineEffectSystem.get_refine_level_from_vnum(19) == 9, "Sword+9 (vnum 19) is level 9") - -func test_shine_threshold_check() -> void: - print("\n--- Test 2: Shine Activation Threshold (Level >= 7) ---") - _assert(not RefineEffectSystem.is_shining_item(10, 1), "Sword+0 does not shine") - _assert(not RefineEffectSystem.is_shining_item(16, 1), "Sword+6 does not shine") - _assert(RefineEffectSystem.is_shining_item(17, 1), "Sword+7 activates shine") - _assert(RefineEffectSystem.is_shining_item(18, 1), "Sword+8 activates shine") - _assert(RefineEffectSystem.is_shining_item(19, 1), "Sword+9 activates shine") - _assert(not RefineEffectSystem.is_shining_item(27001, 3), "Potions do not shine") - -func test_weapon_shine_specs() -> void: - print("\n--- Test 3: 40250 Weapon Shine Specs (InstanceBase.cpp:2698) ---") - var spec7 := RefineEffectSystem.get_shine_spec(17, 1, false) - _assert(spec7.shining and spec7.level == 7, "Sword+7 spec has level 7") - _assert(spec7.attach_bone == RefineEffectSystem.ATTACH_BONE_WEAPON_RIGHT, "Sword attached to PART_WEAPON") - _assert("sword_7.mse" in spec7.effect_path, "Sword+7 uses sword_7.mse") - - var spec8 := RefineEffectSystem.get_shine_spec(18, 1, false) - _assert("sword_8.mse" in spec8.effect_path, "Sword+8 uses sword_8.mse") - - var spec9 := RefineEffectSystem.get_shine_spec(19, 1, false) - _assert("sword_9.mse" in spec9.effect_path, "Sword+9 uses sword_9.mse") - _assert(spec9.light_energy > spec7.light_energy, "+9 has higher light energy than +7") - - # Dual daggers left hand attachment - var spec_left := RefineEffectSystem.get_shine_spec(1179, 1, true) - _assert(spec_left.attach_bone == RefineEffectSystem.ATTACH_BONE_WEAPON_LEFT, "Left hand weapon attached to PART_WEAPON_LEFT") - -func test_armor_shine_specs() -> void: - print("\n--- Test 4: 40250 Armor Shine Specs (InstanceBase.cpp:2739) ---") - # Regular armor +9 (11209) - var spec_armor9 := RefineEffectSystem.get_shine_spec(11209, 2) - _assert(spec_armor9.shining and spec_armor9.level == 9, "Armor+9 has shine") - _assert(spec_armor9.attach_bone == RefineEffectSystem.ATTACH_BONE_ARMOR, "Armor attached to Bip01") - _assert("armor_9.mse" in spec_armor9.effect_path, "Armor+9 uses armor_9.mse") - - # Special Lv 66+ Black Steel Armor +9 (11299) -> special dragon aura - var spec_special := RefineEffectSystem.get_shine_spec(11299, 2) - _assert("armor-4-2.mse" in spec_special.effect_path, "Lv 66+ armor uses special armor-4-2.mse effect") - -func test_particles_generation() -> void: - print("\n--- Test 5: Godot Particle Node Generation ---") - var spec := RefineEffectSystem.get_shine_spec(19, 1) - var p: GPUParticles3D = RefineEffectSystem.create_shine_particles(spec) - _assert(p != null, "GPUParticles3D created successfully") - _assert(p.name == "RefineShineParticles_L9", "Particle node named properly") - _assert(p.draw_pass_1 is QuadMesh, "Draw pass mesh is configured") - p.free() diff --git a/project/test_refine_effect_parity.gd.uid b/project/test_refine_effect_parity.gd.uid deleted file mode 100644 index 04001e3a..00000000 --- a/project/test_refine_effect_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://u7ulbhd6fglk diff --git a/project/test_safebox_storage_keeper_parity.gd b/project/test_safebox_storage_keeper_parity.gd deleted file mode 100644 index 7e8316e6..00000000 --- a/project/test_safebox_storage_keeper_parity.gd +++ /dev/null @@ -1,165 +0,0 @@ -# test_safebox_storage_keeper_parity.gd —— 40250 仓库保管员金条熔铸与密码保险箱系统回归测试 -extends SceneTree - -const SystemClass = preload("res://safebox_storage_keeper_system.gd") - -var _passed: int = 0 -var _failed: int = 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_safebox_storage_keeper_parity (Batch 26 Direction 2) ===") - _test_password_and_open_checks() - _test_password_change() - _test_safebox_storage_operations() - _test_gold_bar_smelting_and_encashment() - _test_safebox_edge_cases() - - print("\n=== Result: %d Passed, %d Failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - else: - printerr("SOME TESTS FAILED!\n") - quit(0 if _failed == 0 else 1) - -func _test_password_and_open_checks() -> void: - print("\n--- Test 1: Safebox Password Authentication & Cooldown/Distance ---") - var box = SystemClass.new() - - # 1. Distance > 1000 fails - var r_dist = box.open_safebox("000000", 1500.0) - _assert(r_dist["error_code"] == "TOO_FAR", "Reject opening safebox if distance > 1000") - - # 2. Wrong password fails - var r_wrong = box.open_safebox("123456", 200.0) - _assert(r_wrong["error_code"] == "WRONG_PASSWORD", "Reject opening safebox with wrong password") - _assert(box.is_open == false, "Safebox remains closed on wrong password") - - # 3. Default password "000000" succeeds - var r_ok = box.open_safebox("000000", 200.0) - _assert(r_ok["success"] == true, "Default password 000000 opens safebox") - _assert(box.is_open == true, "Safebox is open") - - # 4. Opening within 10s fails with COOLDOWN_ACTIVE - box.close_safebox() - box.current_time = 1005 # only 5 seconds later - var r_cd = box.open_safebox("000000", 200.0) - _assert(r_cd["error_code"] == "COOLDOWN_ACTIVE", "Reject opening safebox within 10 seconds") - - # 5. Opening after 10s succeeds - box.current_time = 1015 - var r_cd_ok = box.open_safebox("000000", 200.0) - _assert(r_cd_ok["success"] == true, "Open safebox succeeds after cooldown expires") - -func _test_password_change() -> void: - print("\n--- Test 2: Password Change Verification ---") - var box = SystemClass.new() - box.open_safebox("000000", 200.0) - - # Old password mismatch - var r_fail = box.change_password("999999", "654321") - _assert(r_fail["error_code"] == "OLD_PASSWORD_MISMATCH", "Reject password change if old password mismatches") - - # Change password successfully - var r_succ = box.change_password("000000", "888888") - _assert(r_succ["success"] == true, "Password successfully changed to 888888") - _assert(box.password == "888888", "Internal password updated") - - # Re-open with new password - box.close_safebox() - box.current_time += 20 - var r_new_open = box.open_safebox("888888", 200.0) - _assert(r_new_open["success"] == true, "Can open safebox with new password") - -func _test_safebox_storage_operations() -> void: - print("\n--- Test 3: Storage Deposit, Withdraw & Stacking (45 Slots) ---") - var box = SystemClass.new() - box.open_safebox("000000", 200.0) - - var sword = { "vnum": 10, "name": "Sword+0", "count": 1, "stackable": false } - var potion1 = { "vnum": 27001, "name": "Red Potion (S)", "count": 50, "stackable": true } - var potion2 = { "vnum": 27001, "name": "Red Potion (S)", "count": 25, "stackable": true } - - # Deposit sword into slot 0 - var dep1 = box.deposit_item(0, sword) - _assert(dep1["success"] == true, "Sword deposited into slot 0") - _assert(box.safebox_grid[0]["vnum"] == 10, "Slot 0 holds Sword+0") - - # Deposit potion into slot 1 - var dep2 = box.deposit_item(1, potion1) - _assert(dep2["success"] == true, "Potion deposited into slot 1") - - # Stacking into slot 1 - var dep3 = box.deposit_item(1, potion2) - _assert(dep3["success"] == true, "Potion stacked into slot 1") - _assert(box.safebox_grid[1]["count"] == 75, "Slot 1 potion count updated to 75") - - # Moving item from slot 0 to slot 5 - var move_ok = box.move_safebox_item(0, 5) - _assert(move_ok == true, "Moved item from slot 0 to slot 5") - _assert(box.safebox_grid[0] == null, "Slot 0 is now empty") - _assert(box.safebox_grid[5]["vnum"] == 10, "Slot 5 now holds Sword+0") - - # Withdraw item from slot 5 - var wd = box.withdraw_item(5) - _assert(wd["success"] == true, "Withdrew item from slot 5") - _assert(wd["item"]["vnum"] == 10, "Withdrawn item is Sword+0") - _assert(box.safebox_grid[5] == null, "Slot 5 is now empty") - -func _test_gold_bar_smelting_and_encashment() -> void: - print("\n--- Test 4: Official 1% Smelting Tax & Encashment (80003~80007) ---") - var box = SystemClass.new() - - # 1. Smelt 50k bar (80003): cost = 50500 (1% tax) - var player_gold := 100000 - var s1 = box.smelt_gold_bar(player_gold, 80003) - _assert(s1["success"] == true, "Smelted 50k silver bar") - _assert(s1["cost"] == 50500, "Cost is exactly 50,500 (1% fee)") - _assert(s1["new_gold"] == 49500, "Player gold reduced to 49,500") - _assert(s1["bar_item"]["face_value"] == 50000, "Bar face value is 50,000") - - # 2. Smelt 2M bar (80007): cost = 2,020,000 (1% tax) - player_gold = 3000000 - var s2 = box.smelt_gold_bar(player_gold, 80007) - _assert(s2["success"] == true, "Smelted 2M gold bar") - _assert(s2["cost"] == 2020000, "Cost is exactly 2,020,000") - _assert(s2["new_gold"] == 980000, "Player gold reduced to 980,000") - - # 3. Insufficient gold rejected - var s_fail = box.smelt_gold_bar(1000, 80003) - _assert(s_fail["error_code"] == "INSUFFICIENT_GOLD", "Reject smelting when gold < cost") - - # 4. Encash 2M bar (80007) back to cash: grants 2,000,000 face value - var c1 = box.encash_gold_bar(500000, 80007) - _assert(c1["success"] == true, "Encash 2M gold bar") - _assert(c1["cash_value"] == 2000000, "Cash value is 2,000,000") - _assert(c1["new_gold"] == 2500000, "Player gold increased to 2,500,000") - -func _test_safebox_edge_cases() -> void: - print("\n--- Test 5: Closed Safebox Operations & Invalid Slots ---") - var box = SystemClass.new() - - # Deposit while closed - var dep_closed = box.deposit_item(0, { "vnum": 1, "count": 1 }) - _assert(dep_closed["error_code"] == "NOT_OPEN", "Reject deposit when safebox is closed") - - # Withdraw while closed - var wd_closed = box.withdraw_item(0) - _assert(wd_closed["error_code"] == "NOT_OPEN", "Reject withdraw when safebox is closed") - - # Open safebox - box.open_safebox("000000", 200.0) - - # Invalid slots (< 0 or >= 45) - var dep_inv = box.deposit_item(45, { "vnum": 1, "count": 1 }) - _assert(dep_inv["error_code"] == "INVALID_SLOT", "Reject deposit into slot 45 (max 44)") - - var wd_inv = box.withdraw_item(-1) - _assert(wd_inv["error_code"] == "INVALID_SLOT", "Reject withdraw from slot -1") diff --git a/project/test_safebox_storage_keeper_parity.gd.uid b/project/test_safebox_storage_keeper_parity.gd.uid deleted file mode 100644 index 19c22265..00000000 --- a/project/test_safebox_storage_keeper_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://8lpx6mgvi0dh diff --git a/project/test_save_snapshot_parity.gd b/project/test_save_snapshot_parity.gd deleted file mode 100644 index 99c2a4da..00000000 --- a/project/test_save_snapshot_parity.gd +++ /dev/null @@ -1,248 +0,0 @@ -# test_save_snapshot_parity.gd —— 40250 角色全量快照存读档系统 1:1 回归测试套件 -extends SceneTree - -const SaveSnapshotSystem = preload("res://save_snapshot_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_save_snapshot_parity (Direction 4: Save Snapshot System 1:1) ===") - _test_snapshot_roundtrip() - _test_disk_save_load_delete() - _test_slot_summaries() - _test_validation_and_errors() - _finish() - -func _create_sample_player() -> Dictionary: - var inv := [] - inv.resize(90) - for i in range(90): - inv[i] = null - inv[0] = { - "vnum": 10, - "name": "巨剑+9", - "count": 1, - "sockets": [28430, 28431, 28432], - "attrs": [[1, 2000], [5, 10]] - } - inv[1] = { - "vnum": 27003, - "name": "红色药水(特大)", - "count": 200 - } - - var belt_inv := [] - belt_inv.resize(16) - for i in range(16): - belt_inv[i] = null - belt_inv[0] = { - "vnum": 27102, - "name": "绿色药水(大)", - "count": 50 - } - - return { - "name": "剑圣东方", - "job": 0, # 战士 - "level": 75, - "exp": 1500000, - "hp": 12500, - "max_hp": 12500, - "sp": 3200, - "max_sp": 3200, - "stamina": 1000, - "alignment": 12000, # 骑士精神 - "gold": 98765432, - "str": 90, - "dex": 70, - "con": 90, - "int_stat": 30, - "stat_points": 5, - "skill_points": 2, - "pos": Vector3(456.7, 12.3, 789.0), - "map_name": "sohan", - "playtime": 7200, - "equipment": { - 0: {"vnum": 189, "name": "毒剑+9", "refine": 9}, - 1: {"vnum": 11299, "name": "黑铁战甲+9", "refine": 9}, - 11: {"vnum": 18009, "name": "灵魂腰带+9", "refine_level": 9} - }, - "inventory": inv, - "belt_item": {"vnum": 18009, "name": "灵魂腰带+9", "refine_level": 9}, - "belt_inventory": belt_inv, - "costumes": { - 0: {"vnum": 41001, "name": "沙漠武士服(男)", "costume_type": "body", "remaining_time": 500000}, - 1: {"vnum": 45001, "name": "武士发型", "costume_type": "hair", "remaining_time": 500000} - }, - "skills": { - 1: {"name": "三连斩", "level": 40, "master_type": 3}, # P - 2: {"name": "剑气", "level": 40, "master_type": 3} - }, - "alchemy": { - 0: {"vnum": 111001, "name": "巨龙钻石", "grade": 5} - }, - "monster_cards": [101, 102, 103, 1093], - "achievements": ["FIRST_METIN_KILL", "TOWER_FLOOR_9"], - "active_title": "屠龙尊者" - } - -func _test_snapshot_roundtrip() -> void: - print("\n--- Test 1: In-Memory Snapshot Roundtrip & Full Schema Parity ---") - var sys := SaveSnapshotSystem.new() - var original := _create_sample_player() - - var snapshot := sys.create_snapshot(original, 1) - _assert(snapshot.has("meta") and snapshot.has("stats") and snapshot.has("inventory"), "Snapshot contains main root keys") - _assert(snapshot["meta"]["schema_version"] == SaveSnapshotSystem.SCHEMA_VERSION, "Schema version is 40250.1") - _assert(snapshot["summary"]["name"] == "剑圣东方", "Summary name matches") - _assert(snapshot["summary"]["job_name"] == "战士", "Summary job translated to 战士") - _assert(snapshot["inventory"].size() == 90, "Inventory has exact 90 slots") - _assert(snapshot["belt_system"]["belt_inventory"].size() == 16, "Belt inventory has exact 16 slots") - - # Restore - var restore_res := sys.restore_snapshot(snapshot) - _assert(restore_res["ok"] == true, "Restore succeeded") - - var restored: Dictionary = restore_res["player_data"] - _assert(restored["name"] == original["name"], "Player name preserved") - _assert(restored["level"] == original["level"], "Level preserved (75)") - _assert(restored["gold"] == original["gold"], "Gold preserved (98765432)") - _assert(restored["alignment"] == original["alignment"], "Alignment preserved (12000)") - _assert(restored["pos"] is Vector3 and restored["pos"].is_equal_approx(Vector3(456.7, 12.3, 789.0)), "Vector3 position accurately restored") - - # Inventory restoration - _assert(restored["inventory"].size() == 90, "Restored inventory size is 90") - _assert(restored["inventory"][0]["vnum"] == 10, "Slot 0 item vnum 10 preserved") - _assert(restored["inventory"][0]["sockets"] == [28430, 28431, 28432], "Sockets preserved") - _assert(restored["inventory"][1]["count"] == 200, "Potion count 200 preserved") - _assert(restored["inventory"][2] == null, "Empty inventory slots remain null") - - # Equipment restoration - _assert(restored["equipment"].has(0) and restored["equipment"][0]["name"] == "毒剑+9", "Equipment slot 0 (WEAPON) preserved") - _assert(restored["equipment"].has(1) and restored["equipment"][1]["name"] == "黑铁战甲+9", "Equipment slot 1 (ARMOR) preserved") - - # Belt, Costumes, Progression - _assert(restored["belt_inventory"][0]["vnum"] == 27102, "Belt potion in slot 0 preserved") - _assert(restored["costumes"].has(0) and restored["costumes"][0]["vnum"] == 41001, "Costume body preserved") - _assert(restored["skills"].has(1) and restored["skills"][1]["level"] == 40, "Skill P level preserved") - _assert(restored["monster_cards"].size() == 4, "Monster card collection preserved") - _assert(restored["active_title"] == "屠龙尊者", "Active title preserved") - -func _test_disk_save_load_delete() -> void: - print("\n--- Test 2: Disk IO - Save, Load, and Delete Slots ---") - var sys := SaveSnapshotSystem.new() - var test_dir := OS.get_cache_dir().path_join("metin2_snapshot_test_%d" % int(Time.get_unix_time_from_system())) - var player := _create_sample_player() - - var save_signal := {"fired": false, "slot": 0, "path": ""} - sys.snapshot_saved.connect(func(s: int, p: String): - save_signal["fired"] = true - save_signal["slot"] = s - save_signal["path"] = p - ) - - var load_signal := {"fired": false, "slot": 0, "path": ""} - sys.snapshot_loaded.connect(func(s: int, p: String): - load_signal["fired"] = true - load_signal["slot"] = s - load_signal["path"] = p - ) - - var delete_signal := {"fired": false, "slot": 0} - sys.snapshot_deleted.connect(func(s: int): - delete_signal["fired"] = true - delete_signal["slot"] = s - ) - - # Save to slot 1 - var save_res = sys.save_slot(1, player, test_dir) - _assert(save_res["ok"] == true, "Save slot 1 returned ok") - _assert(save_signal["fired"] == true and save_signal["slot"] == 1, "snapshot_saved signal fired") - _assert(FileAccess.file_exists(save_res["path"]), "Save file exists on disk") - - # Load from slot 1 - var load_res = sys.load_slot(1, test_dir) - _assert(load_res["ok"] == true, "Load slot 1 returned ok") - _assert(load_signal["fired"] == true and load_signal["slot"] == 1, "snapshot_loaded signal fired") - _assert(load_res["player_data"]["name"] == "剑圣东方", "Loaded player name matches") - _assert(load_res["player_data"]["gold"] == 98765432, "Loaded player gold matches") - - # Delete slot 1 - var del_res = sys.delete_slot(1, test_dir) - _assert(del_res["ok"] == true, "Delete slot 1 returned ok") - _assert(delete_signal["fired"] == true and delete_signal["slot"] == 1, "snapshot_deleted signal fired") - _assert(FileAccess.file_exists(save_res["path"]) == false, "Save file removed from disk") - - # Loading after deletion should fail gracefully - var reload_fail = sys.load_slot(1, test_dir) - _assert(reload_fail["ok"] == false and reload_fail["reason"] == "FILE_NOT_FOUND", "Loading deleted slot fails with FILE_NOT_FOUND") - - # Clean up directory - DirAccess.remove_absolute(test_dir) - -func _test_slot_summaries() -> void: - print("\n--- Test 3: Fast Multi-Slot Summaries ---") - var sys := SaveSnapshotSystem.new() - var test_dir := OS.get_cache_dir().path_join("metin2_summary_test_%d" % int(Time.get_unix_time_from_system())) - var player1 := _create_sample_player() - var player2 := _create_sample_player() - player2["name"] = "幻影刺客" - player2["job"] = 1 # 刺客 - player2["level"] = 60 - - # Save slot 1 and slot 3 (leave slot 2, 4, 5 empty) - sys.save_slot(1, player1, test_dir) - sys.save_slot(3, player2, test_dir) - - var summaries := sys.get_slot_summaries(test_dir, 5) - _assert(summaries.size() == 5, "Summaries has 5 entries") - _assert(summaries[0]["is_empty"] == false and summaries[0]["name"] == "剑圣东方" and summaries[0]["level"] == 75, "Slot 1 summary accurate") - _assert(summaries[1]["is_empty"] == true, "Slot 2 summary shows is_empty == true") - _assert(summaries[2]["is_empty"] == false and summaries[2]["name"] == "幻影刺客" and summaries[2]["job_name"] == "刺客", "Slot 3 summary accurate") - _assert(summaries[3]["is_empty"] == true, "Slot 4 summary is_empty") - _assert(summaries[4]["is_empty"] == true, "Slot 5 summary is_empty") - - # Clean up - sys.delete_slot(1, test_dir) - sys.delete_slot(3, test_dir) - DirAccess.remove_absolute(test_dir) - -func _test_validation_and_errors() -> void: - print("\n--- Test 4: Validation & Error Handling ---") - var sys := SaveSnapshotSystem.new() - - var invalid_meta := sys.validate_snapshot({"foo": "bar"}) - _assert(invalid_meta["ok"] == false and invalid_meta["reason"] == "MISSING_META", "Missing meta rejected") - - var bad_version := sys.validate_snapshot({"meta": {"schema_version": "999.0"}}) - _assert(bad_version["ok"] == false and bad_version["reason"] == "VERSION_MISMATCH", "Version mismatch rejected") - - var bad_json_res := sys.import_json("This is not JSON!") - _assert(bad_json_res["ok"] == false and bad_json_res["reason"] == "INVALID_JSON_ROOT", "Invalid JSON string rejected") - - var player := _create_sample_player() - var json_str := sys.export_json(player) - _assert(json_str.length() > 0, "export_json produces non-empty JSON") - var parsed_res := sys.import_json(json_str) - _assert(parsed_res["ok"] == true and parsed_res["player_data"]["name"] == "剑圣东方", "import_json parses valid JSON accurately") - -func _finish() -> void: - print("\n==========================================") - print("Save Snapshot Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Save Snapshot parity tests passed!") - quit(0) diff --git a/project/test_save_snapshot_parity.gd.uid b/project/test_save_snapshot_parity.gd.uid deleted file mode 100644 index 4136ae01..00000000 --- a/project/test_save_snapshot_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b1c6dqivctw7p diff --git a/project/test_security_lock_parity.gd b/project/test_security_lock_parity.gd deleted file mode 100644 index 79a7317c..00000000 --- a/project/test_security_lock_parity.gd +++ /dev/null @@ -1,219 +0,0 @@ -# test_security_lock_parity.gd —— 40250 仓库安全密码与装备防丢锁定 1:1 纯净回归测试套件 -extends SceneTree - -const SecurityLockSystem = preload("res://security_lock_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_security_lock_parity (Direction 4: Security Lock System 1:1) ===") - _test_safebox_password_auth() - _test_safebox_password_change() - _test_item_security_lock_and_unlock() - _test_item_action_gates() - _test_character_deletion_code() - _finish() - -func _test_safebox_password_auth() -> void: - print("\n--- Test 1: Safebox Password Authentication & Lockout ---") - var sec := SecurityLockSystem.new() - var unlock_data := {"fired": false} - sec.safebox_unlocked.connect(func(): unlock_data["fired"] = true) - - _assert(sec.safebox_password == "000000", "Default safebox password is '000000'") - _assert(sec.is_safebox_unlocked == false, "Safebox initially closed") - - # Wrong password 1st time - var res1 = sec.open_safebox("111111") - _assert(res1["ok"] == false and res1["reason"] == "WRONG_PASSWORD", "Wrong password fails") - _assert(res1["remaining_attempts"] == 4, "4 attempts remaining") - - # Fail 4 more times to reach 5 - sec.open_safebox("111111") - sec.open_safebox("111111") - sec.open_safebox("111111") - var res5 = sec.open_safebox("111111") - _assert(res5["ok"] == false and res5["reason"] == "WRONG_PASSWORD_LOCKOUT", "5th failure triggers lockout") - _assert(sec.is_locked_out == true, "is_locked_out is true") - - # Blocked while locked out even with correct password - var locked_res = sec.open_safebox("000000") - _assert(locked_res["ok"] == false and locked_res["reason"] == "LOCKOUT", "Action blocked while locked out") - - # Reset lockout - sec.reset_lockout() - _assert(sec.is_locked_out == false and sec.failed_attempts == 0, "Lockout reset successfully") - - # Now correct password opens safebox - var ok_res = sec.open_safebox("000000") - _assert(ok_res["ok"] == true, "Correct password opened safebox") - _assert(sec.is_safebox_unlocked == true, "Safebox is unlocked") - _assert(unlock_data["fired"] == true, "safebox_unlocked signal fired") - - # Close safebox - var close_data := {"fired": false} - sec.safebox_closed.connect(func(): close_data["fired"] = true) - sec.close_safebox() - _assert(sec.is_safebox_unlocked == false, "Safebox closed") - _assert(close_data["fired"] == true, "safebox_closed signal fired") - -func _test_safebox_password_change() -> void: - print("\n--- Test 2: Password Change Validation (Strict 6 Digits) ---") - var sec := SecurityLockSystem.new() - - # Wrong old password - var r1 = sec.change_safebox_password("999999", "123456") - _assert(r1["ok"] == false and r1["reason"] == "WRONG_OLD_PASSWORD", "Wrong old password rejected") - - # Invalid format: 5 digits - var r2 = sec.change_safebox_password("000000", "12345") - _assert(r2["ok"] == false and r2["reason"] == "INVALID_PASSWORD_FORMAT", "5-digit password rejected") - - # Invalid format: 7 digits - var r3 = sec.change_safebox_password("000000", "1234567") - _assert(r3["ok"] == false and r3["reason"] == "INVALID_PASSWORD_FORMAT", "7-digit password rejected") - - # Invalid format: Contains non-digits - var r4 = sec.change_safebox_password("000000", "12a456") - _assert(r4["ok"] == false and r4["reason"] == "INVALID_PASSWORD_FORMAT", "Alphanumeric password rejected") - - # Same password - var r5 = sec.change_safebox_password("000000", "000000") - _assert(r5["ok"] == false and r5["reason"] == "SAME_PASSWORD", "Identical new password rejected") - - # Valid change to "888888" - var pwd_data := {"fired": false, "new_pwd": ""} - sec.password_changed.connect(func(np: String): - pwd_data["fired"] = true - pwd_data["new_pwd"] = np - ) - - var r6 = sec.change_safebox_password("000000", "888888") - _assert(r6["ok"] == true, "Valid 6-digit password changed successfully") - _assert(sec.safebox_password == "888888", "Active password updated to '888888'") - _assert(pwd_data["fired"] == true and pwd_data["new_pwd"] == "888888", "password_changed signal fired") - - # Verify old password no longer works - var old_try = sec.open_safebox("000000") - _assert(old_try["ok"] == false, "Old password '000000' no longer valid") - - # Verify new password works - var new_try = sec.open_safebox("888888") - _assert(new_try["ok"] == true, "New password '888888' valid") - -func _test_item_security_lock_and_unlock() -> void: - print("\n--- Test 3: Item Anti-Theft Lock & Unlock ---") - var sec := SecurityLockSystem.new() - var test_item: Dictionary = { - "vnum": 169, - "name": "战神之剑+9", - "count": 1 - } - - var lock_signal := {"fired": false, "name": ""} - sec.item_locked.connect(func(n: String): - lock_signal["fired"] = true - lock_signal["name"] = n - ) - - var unlock_signal := {"fired": false, "name": ""} - sec.item_unlocked.connect(func(n: String): - unlock_signal["fired"] = true - unlock_signal["name"] = n - ) - - # Lock with wrong password - var r1 = sec.lock_item(test_item, "999999") - _assert(r1["ok"] == false and r1["reason"] == "WRONG_SECURITY_PASSWORD", "Wrong password prevents locking") - - # Lock with correct password - var r2 = sec.lock_item(test_item, "000000") - _assert(r2["ok"] == true, "Item locked successfully") - _assert(test_item["is_locked"] == true, "Item is_locked flag is true") - _assert(lock_signal["fired"] == true and lock_signal["name"] == "战神之剑+9", "item_locked signal fired") - - # Attempt to lock already locked item - var r3 = sec.lock_item(test_item, "000000") - _assert(r3["ok"] == false and r3["reason"] == "ALREADY_LOCKED", "Already locked item rejected") - - # Unlock with wrong password - var r4 = sec.unlock_item(test_item, "999999") - _assert(r4["ok"] == false and r4["reason"] == "WRONG_SECURITY_PASSWORD", "Wrong password prevents unlocking") - - # Unlock with correct password - var r5 = sec.unlock_item(test_item, "000000") - _assert(r5["ok"] == true, "Item unlocked successfully") - _assert(test_item["is_locked"] == false, "Item is_locked flag is false") - _assert(unlock_signal["fired"] == true and unlock_signal["name"] == "战神之剑+9", "item_unlocked signal fired") - - # Attempt to unlock unlocked item - var r6 = sec.unlock_item(test_item, "000000") - _assert(r6["ok"] == false and r6["reason"] == "NOT_LOCKED", "Already unlocked item rejected") - -func _test_item_action_gates() -> void: - print("\n--- Test 4: Action Protection Gates (Drop, Sell, Destroy, Trade, Refine) ---") - var sec := SecurityLockSystem.new() - var normal_item: Dictionary = {"vnum": 10, "name": "木剑+0", "is_locked": false} - var locked_item: Dictionary = {"vnum": 11299, "name": "黑钢甲+9", "is_locked": true} - - # Normal item allowed everywhere - _assert(sec.can_drop_item(normal_item)["allowed"] == true, "Normal item can be dropped") - _assert(sec.can_sell_item(normal_item)["allowed"] == true, "Normal item can be sold") - _assert(sec.can_destroy_item(normal_item)["allowed"] == true, "Normal item can be destroyed") - _assert(sec.can_trade_item(normal_item)["allowed"] == true, "Normal item can be traded") - _assert(sec.can_refine_item(normal_item)["allowed"] == true, "Normal item can be refined") - - # Locked item blocked everywhere with ITEM_LOCKED - var drop_gate = sec.can_drop_item(locked_item) - _assert(drop_gate["allowed"] == false and drop_gate["reason"] == "ITEM_LOCKED", "Locked item DROP blocked") - - var sell_gate = sec.can_sell_item(locked_item) - _assert(sell_gate["allowed"] == false and sell_gate["reason"] == "ITEM_LOCKED", "Locked item SELL blocked") - - var destroy_gate = sec.can_destroy_item(locked_item) - _assert(destroy_gate["allowed"] == false and destroy_gate["reason"] == "ITEM_LOCKED", "Locked item DESTROY blocked") - - var trade_gate = sec.can_trade_item(locked_item) - _assert(trade_gate["allowed"] == false and trade_gate["reason"] == "ITEM_LOCKED", "Locked item TRADE blocked") - - var refine_gate = sec.can_refine_item(locked_item) - _assert(refine_gate["allowed"] == false and refine_gate["reason"] == "ITEM_LOCKED", "Locked item REFINE blocked") - -func _test_character_deletion_code() -> void: - print("\n--- Test 5: 7-Digit Character Deletion Security Code ---") - var sec := SecurityLockSystem.new() - _assert(sec.char_delete_code == "1234567", "Default 7-digit deletion code is '1234567'") - - # Invalid length: 6 digits - var r1 = sec.verify_character_deletion("123456") - _assert(r1["ok"] == false and r1["reason"] == "INVALID_CODE_FORMAT", "6-digit deletion code rejected") - - # Invalid format: non-numeric - var r2 = sec.verify_character_deletion("123456a") - _assert(r2["ok"] == false and r2["reason"] == "INVALID_CODE_FORMAT", "Alphanumeric deletion code rejected") - - # Wrong 7-digit code - var r3 = sec.verify_character_deletion("7654321") - _assert(r3["ok"] == false and r3["reason"] == "WRONG_DELETE_CODE", "Wrong deletion code rejected") - - # Correct 7-digit code - var r4 = sec.verify_character_deletion("1234567") - _assert(r4["ok"] == true, "Correct 7-digit code authorizes deletion") - -func _finish() -> void: - print("\n==========================================") - print("Security Lock Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_security_lock_parity.gd.uid b/project/test_security_lock_parity.gd.uid deleted file mode 100644 index 2ef17df5..00000000 --- a/project/test_security_lock_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://tlmw0ks337om diff --git a/project/test_skill_book_parity.gd b/project/test_skill_book_parity.gd deleted file mode 100644 index 73eb2f99..00000000 --- a/project/test_skill_book_parity.gd +++ /dev/null @@ -1,117 +0,0 @@ -# test_skill_book_parity.gd —— 技能书研读与进阶突破系统 1:1 回归测试套件 -# 运行方式: -# /opt/homebrew/bin/godot --headless --path metin2-client/project --script test_skill_book_parity.gd -extends SceneTree - -const SkillBookSystem = preload("res://skill_book_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_skill_book_parity (Direction 2: Skill Books & M->G->P 1:1) ===") - _test_need_books_formula() - _test_preconditions() - _test_reading_flow_and_scrolls() - _test_breakthrough_to_grandmaster() - _test_soul_stone_training() - _finish() - -func _test_need_books_formula() -> void: - print("\n--- Test 1: 40250 Need Books Formula (need_bookcount = level - 20) ---") - _assert(SkillBookSystem.get_need_books(21) == 1, "M1 (Lv 21) requires 1 book") - _assert(SkillBookSystem.get_need_books(22) == 2, "M2 (Lv 22) requires 2 books") - _assert(SkillBookSystem.get_need_books(25) == 5, "M5 (Lv 25) requires 5 books") - _assert(SkillBookSystem.get_need_books(30) == 10, "M10 (Lv 30) requires 10 books") - -func _test_preconditions() -> void: - print("\n--- Test 2: Preconditions (EXP, Tier, Cooldown) ---") - var now := 100000 - - # 1. Reject if normal tier (< 20) - var r1 := SkillBookSystem.read_skill_book(1, 15, SkillBookSystem.MASTER_NORMAL, 0, 50000, 0, now, false, false) - _assert(r1["result"] == SkillBookSystem.Result.ERR_NOT_MASTER_TIER, "Reject normal tier (< 20) skill") - - # 2. Reject if EXP < 20,000 - var r2 := SkillBookSystem.read_skill_book(1, 21, SkillBookSystem.MASTER_M, 0, 19999, 0, now, false, false) - _assert(r2["result"] == SkillBookSystem.Result.ERR_INSUFFICIENT_EXP, "Reject if player EXP < 20,000") - - # 3. Reject if on cooldown - var r3 := SkillBookSystem.read_skill_book(1, 21, SkillBookSystem.MASTER_M, 0, 50000, now + 3600, now, false, false) - _assert(r3["result"] == SkillBookSystem.Result.ERR_COOLDOWN, "Reject if on 24h cooldown") - - # 4. Exorcism scroll (71001) bypasses cooldown - var r4 := SkillBookSystem.read_skill_book(1, 21, SkillBookSystem.MASTER_M, 0, 50000, now + 3600, now, true, false, 90) - _assert(r4["result"] == SkillBookSystem.Result.SUCCESS_LEVEL_UP, "Exorcism scroll bypasses cooldown") - _assert(r4["consumed_exorcism"] == true, "Exorcism scroll consumed") - - # 5. Reject if already Grandmaster (G1) - var r5 := SkillBookSystem.read_skill_book(1, 31, SkillBookSystem.MASTER_G, 0, 50000, 0, now, false, false) - _assert(r5["result"] == SkillBookSystem.Result.ERR_ALREADY_G_OR_P, "Reject if already Grandmaster (G1)") - -func _test_reading_flow_and_scrolls() -> void: - print("\n--- Test 3: Reading Probabilities & Hermit's Advice (71094) ---") - var now := 100000 - - # Normal roll 50 (<= 65) -> Failure - var fail_res := SkillBookSystem.read_skill_book(1, 22, SkillBookSystem.MASTER_M, 0, 50000, 0, now, false, false, 50) - _assert(fail_res["result"] == SkillBookSystem.Result.FAIL_READ, "Roll 50 fails (base fail percent 65)") - _assert(fail_res["exp_consumed"] == 20000, "20,000 EXP consumed on failure") - _assert(fail_res["read_count"] == 0, "read_count not incremented on failure") - _assert(fail_res["next_read_time"] == now + 86400, "24h cooldown applied on failure") - - # Hermit's Advice roll 10 -> Guaranteed Success (fail_percent 0) - var hermit_res := SkillBookSystem.read_skill_book(1, 22, SkillBookSystem.MASTER_M, 0, 50000, 0, now, false, true, 10) - _assert(hermit_res["result"] == SkillBookSystem.Result.SUCCESS_READ_ONCE, "Hermit advice guarantees success even on roll 10") - _assert(hermit_res["consumed_hermit"] == true, "Hermit advice consumed") - _assert(hermit_res["read_count"] == 1, "M2 (needs 2) read_count becomes 1") - - # Read second book to complete M2 -> M3 - var up_res := SkillBookSystem.read_skill_book(1, 22, SkillBookSystem.MASTER_M, 1, 50000, 0, now, true, true, 10) - _assert(up_res["result"] == SkillBookSystem.Result.SUCCESS_LEVEL_UP, "Second book triggers level up") - _assert(up_res["level"] == 23, "Level upgraded to 23 (M3)") - _assert(up_res["read_count"] == 0, "read_count reset to 0 upon level up") - -func _test_breakthrough_to_grandmaster() -> void: - print("\n--- Test 4: Breakthrough from M10 (Lv 29/30) to Grandmaster (G1) ---") - var now := 100000 - # M10 (level 29), already read 9 books, needs 9 (level - 20 = 9) - var g_res := SkillBookSystem.read_skill_book(1, 29, SkillBookSystem.MASTER_M, 8, 50000, 0, now, true, true, 10) - _assert(g_res["result"] == SkillBookSystem.Result.SUCCESS_LEVEL_UP, "M10 completes final book") - _assert(g_res["level"] == 30, "Skill level advances to 30") - _assert(g_res["master_type"] == SkillBookSystem.MASTER_G, "Skill grade transforms into Grandmaster (MASTER_G)") - -func _test_soul_stone_training() -> void: - print("\n--- Test 5: Soul Stone (50513) G1 -> P Progression ---") - # G1 (Lv 30) -> G2, karma 5000 (positive) - var s1 := SkillBookSystem.soul_stone_train(1, 30, SkillBookSystem.MASTER_G, 5000, 20) - _assert(s1["success"] == true, "Soul stone training succeeds on roll 20 (<= 30%)") - _assert(s1["level"] == 31, "Advances to G2 (Lv 31)") - _assert(s1["karma_consumed"] == 1000, "G1 training consumed 1,000 karma") - - # Evil character (karma -2000): karma cost doubled! - var s2 := SkillBookSystem.soul_stone_train(1, 30, SkillBookSystem.MASTER_G, -2000, 20) - _assert(s2["karma_consumed"] == 2000, "Negative karma doubles consumption (2,000 karma)") - - # G10 (Lv 39) -> P (Lv 40) - var s_p := SkillBookSystem.soul_stone_train(1, 39, SkillBookSystem.MASTER_G, 10000, 15) - _assert(s_p["success"] == true, "G10 succeeds into Perfect Master") - _assert(s_p["level"] == 40, "Skill level reaches 40") - _assert(s_p["master_type"] == SkillBookSystem.MASTER_P, "Skill grade transforms into Perfect Master (MASTER_P / 'P')") - -func _finish() -> void: - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - quit(0) - else: - printerr("SOME TESTS FAILED!\n") - quit(1) diff --git a/project/test_skill_book_parity.gd.uid b/project/test_skill_book_parity.gd.uid deleted file mode 100644 index 22679810..00000000 --- a/project/test_skill_book_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://byhkkditn4pke diff --git a/project/test_skill_book_reading_parity.gd b/project/test_skill_book_reading_parity.gd deleted file mode 100644 index 240868ff..00000000 --- a/project/test_skill_book_reading_parity.gd +++ /dev/null @@ -1,166 +0,0 @@ -# test_skill_book_reading_parity.gd —— 40250 大师技能书悟道与进阶 1:1 回归测试 -extends SceneTree - -const SystemClass = preload("res://skill_book_reading_system.gd") - -var _passed: int = 0 -var _failed: int = 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_skill_book_reading_parity (Batch 26 Direction 1) ===") - _test_level_curve_and_book_requirements() - _test_exp_and_cooldown_mechanics() - _test_exorcism_and_hermit_scrolls() - _test_failure_handling() - _test_full_m1_to_g1_progression() - _test_edge_cases_and_rejections() - - print("\n=== Result: %d Passed, %d Failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - else: - printerr("SOME TESTS FAILED!\n") - quit(0 if _failed == 0 else 1) - -func _test_level_curve_and_book_requirements() -> void: - print("\n--- Test 1: Level Curve & 40250 Need Books Formula ---") - var sys = SystemClass.new() - sys.init_skill(1, 15) # Normal - _assert(sys.get_skill_master_type(1) == SystemClass.SKILL_NORMAL, "Lv 15 is SKILL_NORMAL") - - sys.init_skill(1, 20) # M1 - _assert(sys.get_skill_master_type(1) == SystemClass.SKILL_MASTER, "Lv 20 is SKILL_MASTER (M1)") - _assert(sys.get_need_books_for_next_level(1) == 1, "M1 requires 1 book to M2") - - sys.init_skill(1, 21) # M2 - _assert(sys.get_need_books_for_next_level(1) == 2, "M2 requires 2 books to M3") - - sys.init_skill(1, 24) # M5 - _assert(sys.get_need_books_for_next_level(1) == 5, "M5 requires 5 books to M6") - - sys.init_skill(1, 29) # M10 - _assert(sys.get_need_books_for_next_level(1) == 10, "M10 requires 10 books to G1") - - # Total books from M1 to G1: sum(1..10) = 55 - var total_books: int = 0 - for lvl in range(20, 30): - sys.init_skill(1, lvl) - total_books += sys.get_need_books_for_next_level(1) - _assert(total_books == 55, "Total successful books needed for M1->G1 is exactly 55") - -func _test_exp_and_cooldown_mechanics() -> void: - print("\n--- Test 2: EXP Consumption & 24h Cooldown ---") - var sys = SystemClass.new(50000, 1000000) - sys.init_skill(1, 20) # M1 - - # Insufficient EXP check - sys.player_exp = 15000 - var res1 = sys.learn_skill_by_book(1, 1, 1) # roll=1 (success) - _assert(res1["error_code"] == "INSUFFICIENT_EXP", "Blocks reading when EXP < 20,000") - _assert(sys.player_exp == 15000, "EXP not deducted when blocked") - - # Sufficient EXP check - sys.player_exp = 50000 - var res2 = sys.learn_skill_by_book(1, 1, 1) - _assert(res2["error_code"] == "OK" and res2["success"] == true, "Reading succeeds with sufficient EXP") - _assert(sys.player_exp == 30000, "20,000 EXP deducted on attempt (50000 -> 30000)") - _assert(res2["level_up"] == true, "M1 leveled up to M2") - - # 24h Cooldown check (86400s) - _assert(sys.skills[1]["next_read_time"] == 1000000 + 86400, "Next read time set to now + 86400") - sys.current_time = 1000000 + 40000 # 40,000s passed, still < 86400 - var res3 = sys.learn_skill_by_book(1, 1, 1) - _assert(res3["error_code"] == "COOLDOWN_ACTIVE", "Reading blocked during 24h cooldown") - -func _test_exorcism_and_hermit_scrolls() -> void: - print("\n--- Test 3: Exorcism Scroll (71001) & Hermit Advice (71094) ---") - var sys = SystemClass.new(100000, 1000000) - sys.init_skill(1, 21) # M2 (needs 2 books) - sys.skills[1]["next_read_time"] = 1000000 + 50000 # In cooldown - - # Use Exorcism Scroll (71001) - sys.use_exorcism_scroll() - _assert(sys.has_affect(SystemClass.EXORCISM_SCROLL_VNUM) == true, "Exorcism affect active") - - # Reading while in cooldown with Exorcism - var res = sys.learn_skill_by_book(1, 1, 1) - _assert(res["error_code"] == "OK", "Exorcism scroll bypassed cooldown") - _assert(res["consumed_exorcism"] == true, "Exorcism scroll consumed") - _assert(sys.has_affect(SystemClass.EXORCISM_SCROLL_VNUM) == false, "Exorcism affect removed after read") - - # Use Hermit Advice (71094) - sys.use_hermit_advice() - _assert(sys.has_affect(SystemClass.HERMIT_ADVICE_VNUM) == true, "Hermit advice affect active") - - # Reading with roll=99 (which normally fails base 35%) - sys.use_exorcism_scroll() - var res2 = sys.learn_skill_by_book(1, 1, 99) - _assert(res2["success"] == true, "Hermit advice forced 100% success rate on roll=99") - _assert(res2["consumed_hermit"] == true, "Hermit advice consumed") - _assert(sys.has_affect(SystemClass.HERMIT_ADVICE_VNUM) == false, "Hermit affect removed after read") - -func _test_failure_handling() -> void: - print("\n--- Test 4: Reading Failure Penalty & Retention ---") - var sys = SystemClass.new(100000, 1000000) - sys.init_skill(1, 22, 1) # M3, read_count = 1 (needs 3 books) - - # Roll 90 without hermit advice -> Failure - var res = sys.learn_skill_by_book(1, 1, 90) - _assert(res["success"] == false, "Reading failed on roll 90") - _assert(res["error_code"] == "READ_FAIL", "Error code is READ_FAIL") - _assert(sys.skills[1]["level"] == 22, "Skill level remains unchanged at 22") - _assert(sys.skills[1]["read_count"] == 1, "Skill read_count preserved at 1") - _assert(sys.player_exp == 80000, "EXP still deducted on failure (20,000 EXP)") - _assert(sys.skills[1]["next_read_time"] == 1000000 + 86400, "Cooldown applied even on failure") - -func _test_full_m1_to_g1_progression() -> void: - print("\n--- Test 5: Full M1 to G1 Progression (55 Books) ---") - var sys = SystemClass.new(5000000, 1000000) - sys.init_skill(1, 20, 0) # Start at M1 (Lv 20) - - var books_succeeded: int = 0 - var final_res: Dictionary = {} - - # Loop through all 55 books with 100% success using scrolls - for step in range(55): - sys.use_exorcism_scroll() - sys.use_hermit_advice() - final_res = sys.learn_skill_by_book(1, 1) - _assert(final_res["success"] == true, "Book %d read succeeded" % (step + 1)) - books_succeeded += 1 - - _assert(books_succeeded == 55, "Successfully read 55 books") - _assert(sys.skills[1]["level"] == 30, "Skill level reached 30 (G1)") - _assert(sys.get_skill_master_type(1) == SystemClass.SKILL_GRAND_MASTER, "Skill tier is SKILL_GRAND_MASTER") - _assert(final_res["breakthrough_g"] == true, "Final book triggered breakthrough_g flag") - -func _test_edge_cases_and_rejections() -> void: - print("\n--- Test 6: Edge Cases & Rejections ---") - var sys = SystemClass.new(100000, 1000000) - - # 1. Unknown skill - var r1 = sys.learn_skill_by_book(999, 999) - _assert(r1["error_code"] == "NOT_LEARNABLE", "Reject unregistered skill") - - # 2. Skill mismatch on book socket - sys.init_skill(1, 20) - var r2 = sys.learn_skill_by_book(1, 2) # Socket says skill 2, but reading for skill 1 - _assert(r2["error_code"] == "SKILL_MISMATCH", "Reject skill book with mismatched socket") - - # 3. Normal tier skill (< 20) - sys.init_skill(2, 10) - var r3 = sys.learn_skill_by_book(2, 2) - _assert(r3["error_code"] == "NOT_MASTER_TIER", "Reject normal tier skill (< 20)") - - # 4. G1 tier skill (>= 30) - sys.init_skill(3, 30) - var r4 = sys.learn_skill_by_book(3, 3) - _assert(r4["error_code"] == "ALREADY_G_OR_P", "Reject Grandmaster tier skill (>= 30)") diff --git a/project/test_skill_book_reading_parity.gd.uid b/project/test_skill_book_reading_parity.gd.uid deleted file mode 100644 index a2921c7f..00000000 --- a/project/test_skill_book_reading_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cyuhr5wad5jri diff --git a/project/test_socket_glow_fx_parity.gd b/project/test_socket_glow_fx_parity.gd deleted file mode 100644 index 7928ea92..00000000 --- a/project/test_socket_glow_fx_parity.gd +++ /dev/null @@ -1,132 +0,0 @@ -# test_socket_glow_fx_parity.gd —— 40250 装备灵石镶嵌发光流光粒子 1:1 回归测试套件 -extends SceneTree - -const SocketGlowFxSystem = preload("res://socket_glow_fx_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_socket_glow_fx_parity (Direction 3: Socket Glow FX 1:1) ===") - _test_stone_color_roster() - _test_base_refine_auras() - _test_socket_stone_glow_blending() - _test_material_application() - _finish() - -func _test_stone_color_roster() -> void: - print("\n--- Test 1: Spirit Stone Color Roster ---") - _assert(SocketGlowFxSystem.STONE_COLORS.size() >= 8, "Has at least 8 Spirit Stone colors defined") - _assert(SocketGlowFxSystem.STONE_COLORS.has(28430), "Warrior Stone +4 defined") - _assert(SocketGlowFxSystem.STONE_COLORS.has(28432), "Sura Stone +4 defined") - _assert(SocketGlowFxSystem.STONE_COLORS.has(28436), "Cooldown Stone +4 defined") - _assert(SocketGlowFxSystem.STONE_COLORS.has(28438), "Protection Stone +4 defined") - -func _test_base_refine_auras() -> void: - print("\n--- Test 2: Base Refine Auras (+0~+6 vs +7/+8/+9) ---") - var sys := SocketGlowFxSystem.new() - - # +6 without stones - var item_plus6 := {"vnum": 10, "refine_level": 6, "sockets": []} - var g6 = sys.calculate_equipment_glow(item_plus6) - _assert(g6["is_glowing"] == false, "+6 item without stones has no glow") - _assert(g6["glow_intensity"] == 0.0, "Intensity is 0.0") - - # +7 item - var item_plus7 := {"vnum": 10, "refine_level": 7, "sockets": []} - var g7 = sys.calculate_equipment_glow(item_plus7) - _assert(g7["is_glowing"] == true, "+7 item glows") - _assert(g7["glow_intensity"] == 0.6, "+7 intensity is 0.6") - _assert(g7["particle_rate"] == 12, "+7 particle rate is 12") - - # +8 item - var item_plus8 := {"vnum": 10, "refine_level": 8, "sockets": []} - var g8 = sys.calculate_equipment_glow(item_plus8) - _assert(g8["glow_intensity"] == 1.2, "+8 intensity is 1.2") - _assert(g8["has_trail"] == true, "+8 has weapon swing trail") - - # +9 item - var item_plus9 := {"vnum": 10, "refine_level": 9, "sockets": []} - var g9 = sys.calculate_equipment_glow(item_plus9) - _assert(g9["glow_intensity"] == 2.2, "+9 intensity is 2.2") - _assert(g9["swirl_enabled"] == true, "+9 has mythic swirling aura") - _assert(g9["particle_rate"] == 55, "+9 particle rate is 55") - -func _test_socket_stone_glow_blending() -> void: - print("\n--- Test 3: Socket Stones Color & Intensity Blending ---") - var sys := SocketGlowFxSystem.new() - - var sig_data := {"fired": false, "vnum": 0, "intensity": 0.0} - sys.glow_profile_updated.connect(func(v: int, i: float, _c: Color): - sig_data["fired"] = true - sig_data["vnum"] = v - sig_data["intensity"] = i - ) - - # +9 Poison Sword with Warrior Stone (28430, Red) - var sword_red := { - "vnum": 189, - "refine_level": 9, - "sockets": [28430] - } - var g_red = sys.calculate_equipment_glow(sword_red) - _assert(g_red["is_glowing"] == true, "Glowing active") - _assert(g_red["glow_intensity"] > 2.2, "Intensity boosted by spirit stone (> 2.2)") - _assert(g_red["primary_color"].r > 0.9, "Warrior stone creates vivid red color") - _assert(sig_data["fired"] == true and sig_data["vnum"] == 189, "glow_profile_updated signal fired") - - # +9 Sword with Sura Stone (28432, Dark Violet) - var sword_purple := { - "vnum": 189, - "refine_level": 9, - "sockets": [28432] - } - var g_purple = sys.calculate_equipment_glow(sword_purple) - _assert(g_purple["primary_color"].b > 0.9, "Sura stone creates deep violet/blue tone") - - # Item with 3 stones: Red + Purple + Gold - var sword_tri := { - "vnum": 189, - "refine_level": 9, - "sockets": [28430, 28432, 28436] - } - var g_tri = sys.calculate_equipment_glow(sword_tri) - _assert(g_tri["stone_count"] == 3, "3 socket stones counted") - _assert(g_tri["secondary_color"] == SocketGlowFxSystem.STONE_COLORS[28430]["color"], "Secondary color tracks slot 0 primary stone") - -func _test_material_application() -> void: - print("\n--- Test 4: StandardMaterial3D Direct Injection ---") - var sys := SocketGlowFxSystem.new() - var mat := StandardMaterial3D.new() - - var item_plus9 := {"vnum": 10, "refine_level": 9, "sockets": [28430]} - var glow = sys.calculate_equipment_glow(item_plus9) - - sys.apply_to_material(mat, glow) - _assert(mat.emission_enabled == true, "Material emission enabled") - _assert(mat.emission == glow["primary_color"], "Emission color matches primary color") - _assert(is_equal_approx(mat.emission_energy_multiplier, glow["glow_intensity"]), "Energy multiplier matches glow intensity") - - # Apply empty glow - sys.apply_to_material(mat, sys._empty_glow()) - _assert(mat.emission_enabled == false, "Emission disabled for non-glowing item") - -func _finish() -> void: - print("\n==========================================") - print("Socket Glow FX Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Socket Glow FX parity tests passed!") - quit(0) diff --git a/project/test_socket_glow_fx_parity.gd.uid b/project/test_socket_glow_fx_parity.gd.uid deleted file mode 100644 index 7770a063..00000000 --- a/project/test_socket_glow_fx_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cws3gfse5gytr diff --git a/project/test_solo_guild_domain_parity.gd b/project/test_solo_guild_domain_parity.gd deleted file mode 100644 index e3e5b496..00000000 --- a/project/test_solo_guild_domain_parity.gd +++ /dev/null @@ -1,155 +0,0 @@ -# test_solo_guild_domain_parity.gd —— 40250 个人公会领地与建筑建造 1:1 回归测试套件 -extends SceneTree - -const SoloGuildDomainSystem = preload("res://solo_guild_domain_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_solo_guild_domain_parity (Direction 2: Solo Guild Domain 1:1) ===") - _test_claim_domain() - _test_construct_and_upgrade_buildings() - _test_guild_blacksmith_refining() - _test_guardian_altar_passives() - _finish() - -func _test_claim_domain() -> void: - print("\n--- Test 1: Claiming Guild Domain ---") - var sys := SoloGuildDomainSystem.new() - var poor_player := {"gold": 1000} - - # Fail on low gold - var fail_gold = sys.claim_domain("神圣殿堂", 10, poor_player) - _assert(fail_gold["ok"] == false and fail_gold["reason"] == "NOT_ENOUGH_GOLD", "Rejected: not enough gold") - - # Fail on low guild level - var rich_player := {"gold": 50000000} - var fail_lvl = sys.claim_domain("神圣殿堂", 8, rich_player) - _assert(fail_lvl["ok"] == false and fail_lvl["reason"] == "GUILD_LEVEL_TOO_LOW", "Rejected: guild level < 10") - - # Success - var claim_sig := {"fired": false, "name": "", "cost": 0} - sys.domain_claimed.connect(func(gname: String, cost: int): - claim_sig["fired"] = true - claim_sig["name"] = gname - claim_sig["cost"] = cost - ) - - var res = sys.claim_domain("神圣殿堂", 12, rich_player) - _assert(res["ok"] == true, "Claimed domain") - _assert(sys.has_domain == true, "has_domain is true") - _assert(rich_player["gold"] == 30000000, "Cost 20M deducted (50M -> 30M)") - _assert(claim_sig["fired"] == true and claim_sig["name"] == "神圣殿堂", "domain_claimed signal emitted") - - # Duplicate claim rejected - var fail_dup = sys.claim_domain("再次开辟", 12, rich_player) - _assert(fail_dup["ok"] == false and fail_dup["reason"] == "ALREADY_HAS_DOMAIN", "Duplicate claim rejected") - -func _test_construct_and_upgrade_buildings() -> void: - print("\n--- Test 2: Constructing & Upgrading Buildings ---") - var sys := SoloGuildDomainSystem.new() - var player := {"gold": 50000000} - sys.claim_domain("东方荣耀", 10, player) # 50M -> 30M - - var build_sig := {"fired": false, "btype": 0} - sys.building_constructed.connect(func(btype: int, _n: String, _l: int): - build_sig["fired"] = true - build_sig["btype"] = btype - ) - - # Construct Guild Blacksmith (10M gold) - var c_res = sys.construct_building(SoloGuildDomainSystem.BuildingType.BLACKSMITH, player) - _assert(c_res["ok"] == true, "Blacksmith constructed") - _assert(player["gold"] == 20000000, "Cost 10M deducted (30M -> 20M)") - _assert(sys.constructed_buildings.has(SoloGuildDomainSystem.BuildingType.BLACKSMITH), "Blacksmith in buildings list") - _assert(build_sig["fired"] == true and build_sig["btype"] == 1, "building_constructed emitted") - - # Re-construct rejected - var fail_c = sys.construct_building(SoloGuildDomainSystem.BuildingType.BLACKSMITH, player) - _assert(fail_c["ok"] == false and fail_c["reason"] == "ALREADY_CONSTRUCTED", "Cannot build twice") - - # Upgrade Blacksmith (Lv 1 -> Lv 2, cost: 8M) - var up_sig := {"fired": false, "lvl": 0} - sys.building_upgraded.connect(func(_b: int, lvl: int): - up_sig["fired"] = true - up_sig["lvl"] = lvl - ) - - var up_res = sys.upgrade_building(SoloGuildDomainSystem.BuildingType.BLACKSMITH, player) - _assert(up_res["ok"] == true and up_res["new_level"] == 2, "Upgraded Blacksmith to Lv 2") - _assert(up_sig["fired"] == true and up_sig["lvl"] == 2, "building_upgraded emitted") - _assert(player["gold"] == 12000000, "8M upgrade cost deducted (20M -> 12M)") - -func _test_guild_blacksmith_refining() -> void: - print("\n--- Test 3: Guild Blacksmith Enhanced Refining ---") - var sys := SoloGuildDomainSystem.new() - var player := {"gold": 50000000} - sys.claim_domain("锻造公会", 10, player) - sys.construct_building(SoloGuildDomainSystem.BuildingType.BLACKSMITH, player) - - var item := {"vnum": 10, "name": "巨剑", "refine_level": 7} - - var ref_sig := {"fired": false, "succ": false, "lvl": 0} - sys.guild_refine_result.connect(func(succ: bool, lvl: int): - ref_sig["fired"] = true - ref_sig["succ"] = succ - ref_sig["lvl"] = lvl - ) - - # Refine with force_success = true (+7 -> +8) - var res1 = sys.guild_blacksmith_refine(item, player, true) - _assert(res1["ok"] == true and res1["success"] == true, "Refined to +8") - _assert(item["refine_level"] == 8, "Item refine level is now 8") - _assert(ref_sig["fired"] == true and ref_sig["succ"] == true, "guild_refine_result emitted") - - # Refine to +9 - sys.guild_blacksmith_refine(item, player, true) - _assert(item["refine_level"] == 9, "Item refine level is now 9 (+9 max)") - - # Further refine at +9 must fail - var fail_max = sys.guild_blacksmith_refine(item, player, true) - _assert(fail_max["ok"] == false and fail_max["reason"] == "ALREADY_MAX_REFINE", "Cannot refine past +9") - -func _test_guardian_altar_passives() -> void: - print("\n--- Test 4: Guild Guardian Altar Passive Auras ---") - var sys := SoloGuildDomainSystem.new() - var player := {"gold": 50000000} - sys.claim_domain("神坛公会", 10, player) - - # No altar yet - _assert(sys.get_altar_passive_buffs().is_empty(), "No buffs without Altar") - - # Build Guardian Altar (Lv 1) - sys.construct_building(SoloGuildDomainSystem.BuildingType.GUARDIAN_ALTAR, player) - var b1 = sys.get_altar_passive_buffs() - _assert(b1["max_hp"] == 1500, "Lv 1 Altar gives +1500 Max HP") - _assert(b1["exp_rate_pct"] == 10, "Lv 1 Altar gives +10% Exp") - _assert(b1["all_stats"] == 5, "Lv 1 Altar gives +5 All Stats") - - # Upgrade Altar to Lv 2 - sys.upgrade_building(SoloGuildDomainSystem.BuildingType.GUARDIAN_ALTAR, player) - var b2 = sys.get_altar_passive_buffs() - _assert(b2["max_hp"] == 3000, "Lv 2 Altar gives +3000 Max HP") - _assert(b2["exp_rate_pct"] == 20, "Lv 2 Altar gives +20% Exp") - _assert(b2["all_stats"] == 10, "Lv 2 Altar gives +10 All Stats") - -func _finish() -> void: - print("\n==========================================") - print("Solo Guild Domain Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Solo Guild Domain parity tests passed!") - quit(0) diff --git a/project/test_solo_guild_domain_parity.gd.uid b/project/test_solo_guild_domain_parity.gd.uid deleted file mode 100644 index 1792e1eb..00000000 --- a/project/test_solo_guild_domain_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://gy6raqfblm3n diff --git a/project/test_soul_stone_grandmaster_parity.gd b/project/test_soul_stone_grandmaster_parity.gd deleted file mode 100644 index 35bc302f..00000000 --- a/project/test_soul_stone_grandmaster_parity.gd +++ /dev/null @@ -1,123 +0,0 @@ -# test_soul_stone_grandmaster_parity.gd —— Metin2 40250 灵魂石领悟宗师绝技 1:1 测试套件 -extends SceneTree - -const SoulStoneGrandmasterSystem = preload("res://soul_stone_grandmaster_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_soul_stone_grandmaster_parity (Direction 1: Soul Stone Grandmaster 1:1) ===") - - test_eligibility_and_alignment_cost() - test_successful_training_and_p_tier() - test_failure_alignment_penalty() - test_cooldown_and_exorcism() - test_serialization() - - print("\n=======================================================") - print("Soul Stone Grandmaster Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================") - - if _failed > 0: - quit(1) - else: - quit(0) - -func test_eligibility_and_alignment_cost() -> void: - print("\n--- Test 1: Eligibility & Alignment Calculations (40250 1:1) ---") - var system = SoulStoneGrandmasterSystem.new() - - # 检查 G 阶资格: 30(G1) ~ 39(G10) 允许,其他拒绝 - _assert(system.can_train_skill(20) == false, "M-tier skill (Lv 20) not eligible for soul stone") - _assert(system.can_train_skill(30) == true, "G1 (Lv 30) eligible") - _assert(system.can_train_skill(39) == true, "G10 (Lv 39) eligible") - _assert(system.can_train_skill(40) == false, "P-tier (Lv 40) already maxed") - - # 善恶值消耗计算: need_alignment = 1000 + 500 * (level - 30) - # 正善恶值玩家 - _assert(system.get_required_alignment(30, 5000) == 1000, "G1 costs 1000 alignment") - _assert(system.get_required_alignment(31, 5000) == 1500, "G2 costs 1500 alignment") - _assert(system.get_required_alignment(39, 5000) == 5500, "G10 costs 5500 alignment") - - # 负善恶值 (邪恶阵营) 翻倍惩罚 (40250 quest: need_alignment = need_alignment * 2) - _assert(system.get_required_alignment(30, -2000) == 2000, "G1 costs 2000 alignment for evil player") - _assert(system.get_required_alignment(39, -500) == 11000, "G10 costs 11000 alignment for evil player") - -func test_successful_training_and_p_tier() -> void: - print("\n--- Test 2: Successful Training & Reaching P (40250 1:1) ---") - var system = SoulStoneGrandmasterSystem.new() - - var inv: Array = [ - {"vnum": 50513, "count": 2} # 灵魂石 x2 - ] - - # 1. 成功从 G1 (30) 升到 G2 (31) - var res = system.train_grandmaster_skill(inv, 0, 1, "三重斩", 30, 8000, false, true) - _assert(res["ok"] == true and res["success"] == true, "Training succeeded") - _assert(res["new_level"] == 31, "Skill level promoted to 31 (G2)") - _assert(res["alignment_cost"] == 1000, "Deducted 1000 alignment") - _assert(res["new_alignment"] == 7000, "New alignment is 7000 (8000 - 1000)") - _assert(inv[0]["count"] == 1, "Consumed 1 Soul Stone") - - # 清除冷却用于测试 - system.use_exorcism_scroll() - - # 2. 成功从 G10 (39) 登峰造极突破至 P 阶 (40) - res = system.train_grandmaster_skill(inv, 0, 1, "三重斩", 39, 10000, false, true) - _assert(res["ok"] == true and res["success"] == true, "Promoted to P tier") - _assert(res["new_level"] == 40, "Level reached 40") - _assert(res["is_perfect_master"] == true, "Reached Perfect Master!") - _assert(res["tier_text"] == "P 阶 (Perfect Master)", "Tier text is Perfect Master") - -func test_failure_alignment_penalty() -> void: - print("\n--- Test 3: Failure & Alignment Penalty (40250 1:1) ---") - var system = SoulStoneGrandmasterSystem.new() - - var inv: Array = [{"vnum": 50513, "count": 1}] - # 强制失败 (roll = 90 > 30) - var res = system.train_grandmaster_skill(inv, 0, 2, "旋风斩", 30, 5000, false, false, 90) - _assert(res["ok"] == true and res["success"] == false, "Training failed") - _assert(res["new_level"] == 30, "Skill level unchanged at 30") - _assert(res["alignment_cost"] >= 330 and res["alignment_cost"] <= 500, "Deducted 1/3 to 1/2 penalty") - _assert(res["new_alignment"] < 5000, "Alignment penalised") - _assert(inv[0] == null, "Soul stone shattered and removed") - -func test_cooldown_and_exorcism() -> void: - print("\n--- Test 4: Cooldown & Exorcism Scroll (40250 1:1) ---") - var system = SoulStoneGrandmasterSystem.new() - var inv: Array = [{"vnum": 50513, "count": 2}] - - system.train_grandmaster_skill(inv, 0, 1, "三重斩", 30, 8000, false, true) - _assert(system.training_cooldown_left > 0.0, "Cooldown active after training") - - # 冷却期间再次参悟 -> 拦截 - var res = system.train_grandmaster_skill(inv, 0, 1, "三重斩", 31, 7000, false, true) - _assert(res["ok"] == false, "Rejected during cooldown") - _assert(res["reason"] == "COOLDOWN_ACTIVE", "Reason is COOLDOWN_ACTIVE") - - # 使用驱魔卷轴 (71001) 清除心境冷却 - system.use_exorcism_scroll() - _assert(system.training_cooldown_left == 0.0, "Cooldown cleared by exorcism scroll") - - # 再次参悟 -> 成功 - res = system.train_grandmaster_skill(inv, 0, 1, "三重斩", 31, 7000, false, true) - _assert(res["ok"] == true, "Trained successfully after exorcism") - -func test_serialization() -> void: - print("\n--- Test 5: Serialization & Restoration ---") - var system = SoulStoneGrandmasterSystem.new() - system.training_cooldown_left = 15000.0 - var data = system.serialize() - - var restored = SoulStoneGrandmasterSystem.new() - restored.deserialize(data) - _assert(restored.training_cooldown_left == 15000.0, "Restored cooldown") diff --git a/project/test_soul_stone_grandmaster_parity.gd.uid b/project/test_soul_stone_grandmaster_parity.gd.uid deleted file mode 100644 index baf938e2..00000000 --- a/project/test_soul_stone_grandmaster_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c5s06easynetm diff --git a/project/test_soulbound_lock_security_parity.gd b/project/test_soulbound_lock_security_parity.gd deleted file mode 100644 index 685ed956..00000000 --- a/project/test_soulbound_lock_security_parity.gd +++ /dev/null @@ -1,138 +0,0 @@ -# test_soulbound_lock_security_parity.gd —— 40250 灵魂绑定与防丢安全锁 1:1 回归测试套件 -extends SceneTree - -const SoulboundLockSecuritySystem = preload("res://soulbound_lock_security_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_soulbound_lock_security_parity (Direction 3: Soulbound Security 1:1) ===") - _test_security_predicates() - _test_soul_binding_flow() - _test_unbinding_and_cancellation() - _test_unbinding_completion() - _finish() - -func _test_security_predicates() -> void: - print("\n--- Test 1: Static Security Predicates (Death Drop, Sell, Stall) ---") - var normal_item := {"vnum": 10, "is_soulbound": false} - var bound_item := {"vnum": 189, "is_soulbound": true} - - # Normal item can drop, sell, stall - _assert(SoulboundLockSecuritySystem.can_drop_on_death(normal_item) == true, "Normal item drops on red name death") - _assert(SoulboundLockSecuritySystem.can_sell_to_shop(normal_item) == true, "Normal item can be sold to shop") - _assert(SoulboundLockSecuritySystem.can_drop_to_ground(normal_item) == true, "Normal item can be dropped to ground") - _assert(SoulboundLockSecuritySystem.can_trade_or_stall(normal_item) == true, "Normal item can be traded or stalled") - - # Soulbound item is 100% protected - _assert(SoulboundLockSecuritySystem.can_drop_on_death(bound_item) == false, "Soulbound item IMMUNE to death drop") - _assert(SoulboundLockSecuritySystem.can_sell_to_shop(bound_item) == false, "Soulbound item CANNOT be sold to shop") - _assert(SoulboundLockSecuritySystem.can_drop_to_ground(bound_item) == false, "Soulbound item CANNOT be dropped to ground") - _assert(SoulboundLockSecuritySystem.can_trade_or_stall(bound_item) == false, "Soulbound item CANNOT be put on bazaar stalls") - -func _test_soul_binding_flow() -> void: - print("\n--- Test 2: Applying Soul Binding (72051) ---") - var sys := SoulboundLockSecuritySystem.new() - var inv: Array = [ - {"vnum": SoulboundLockSecuritySystem.VNUM_SOULBIND_SCROLL, "name": "灵魂绑定卷轴", "count": 2}, - {"vnum": 189, "name": "毒剑+9", "is_soulbound": false} - ] - - var bind_sig := {"fired": false, "slot": -1, "name": ""} - sys.item_soulbound.connect(func(s: int, iname: String): - bind_sig["fired"] = true - bind_sig["slot"] = s - bind_sig["name"] = iname - ) - - var res = sys.bind_item(0, 1, inv) - _assert(res["ok"] == true, "Item soulbound") - _assert(inv[1]["is_soulbound"] == true, "is_soulbound is true") - _assert(inv[0]["count"] == 1, "Scroll count decreased (2 -> 1)") - _assert(bind_sig["fired"] == true and bind_sig["slot"] == 1 and bind_sig["name"] == "毒剑+9", "item_soulbound emitted") - - # Duplicate binding rejected - var fail_dup = sys.bind_item(0, 1, inv) - _assert(fail_dup["ok"] == false and fail_dup["reason"] == "ALREADY_SOULBOUND", "Duplicate binding rejected") - _assert(inv[0]["count"] == 1, "Scroll not consumed on failure") - -func _test_unbinding_and_cancellation() -> void: - print("\n--- Test 3: Unbinding Initiation (72052) & Cancellation ---") - var sys := SoulboundLockSecuritySystem.new() - var inv: Array = [ - {"vnum": SoulboundLockSecuritySystem.VNUM_SOULUNBIND_SCROLL, "name": "灵魂解绑卷轴", "count": 1}, - {"vnum": 189, "name": "毒剑+9", "is_soulbound": true} - ] - - var unbind_start_sig := {"fired": false, "time": 0.0} - sys.unbinding_started.connect(func(_s: int, _n: String, t: float): - unbind_start_sig["fired"] = true - unbind_start_sig["time"] = t - ) - - # Start unbinding (72 hours) - var res = sys.start_unbinding(0, 1, inv, 72.0) - _assert(res["ok"] == true, "Unbinding started") - _assert(inv[0] == null, "Unbind scroll consumed") - _assert(unbind_start_sig["fired"] == true and unbind_start_sig["time"] == 72.0 * 3600.0, "unbinding_started emitted with 72h") - _assert(inv[1]["unbind_remaining_time"] == 72.0 * 3600.0, "Cooldown active") - - # Cancel unbinding - var cancel_sig := {"fired": false} - sys.unbinding_cancelled.connect(func(_s: int, _n: String): cancel_sig["fired"] = true) - - var cancel_res = sys.cancel_unbinding(1, inv) - _assert(cancel_res["ok"] == true, "Unbinding cancelled") - _assert(cancel_sig["fired"] == true, "unbinding_cancelled emitted") - _assert(inv[1]["unbind_remaining_time"] == 0.0, "Timer cleared") - _assert(inv[1]["is_soulbound"] == true, "Item remains safely soulbound") - -func _test_unbinding_completion() -> void: - print("\n--- Test 4: Finalizing Unbinding After Cooldown ---") - var sys := SoulboundLockSecuritySystem.new() - var inv: Array = [ - {"vnum": SoulboundLockSecuritySystem.VNUM_SOULUNBIND_SCROLL, "count": 1}, - {"vnum": 189, "name": "毒剑+9", "is_soulbound": true} - ] - - # Start short 1-second unbind for testing - sys.start_unbinding(0, 1, inv, 1.0 / 3600.0) # 1 second - - # Immediate finalize attempt should fail - var fail_early = sys.finalize_unbinding(1, inv) - _assert(fail_early["ok"] == false and fail_early["reason"] == "COOLDOWN_NOT_FINISHED", "Cannot finalize before timer") - - # Update 2 seconds - sys.update(2.0, inv) - _assert(inv[1]["unbind_remaining_time"] == 0.0, "Timer expired") - - # Finalize unbind - var unbound_sig := {"fired": false} - sys.item_unbound.connect(func(_s: int, _n: String): unbound_sig["fired"] = true) - - var done_res = sys.finalize_unbinding(1, inv) - _assert(done_res["ok"] == true, "Item unbound") - _assert(unbound_sig["fired"] == true, "item_unbound emitted") - _assert(inv[1]["is_soulbound"] == false, "Item is no longer soulbound") - _assert(SoulboundLockSecuritySystem.can_drop_on_death(inv[1]) == true, "Now subject to normal drop rules") - -func _finish() -> void: - print("\n==========================================") - print("Soulbound Security Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Soulbound Security parity tests passed!") - quit(0) diff --git a/project/test_soulbound_lock_security_parity.gd.uid b/project/test_soulbound_lock_security_parity.gd.uid deleted file mode 100644 index a2166406..00000000 --- a/project/test_soulbound_lock_security_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://6n52fdihhboe diff --git a/project/test_spider_dungeon_key_parity.gd b/project/test_spider_dungeon_key_parity.gd deleted file mode 100644 index 5a2fafd0..00000000 --- a/project/test_spider_dungeon_key_parity.gd +++ /dev/null @@ -1,189 +0,0 @@ -# test_spider_dungeon_key_parity.gd —— 40250 蜘蛛洞穴通行证与蛛后巢穴密匙系统 1:1 回归测试套件 -extends SceneTree - -const SpiderDungeonKeySystem = preload("res://spider_dungeon_key_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_spider_dungeon_key_parity (Direction 4: Spider Dungeon Key 1:1) ===") - _test_passage_ticket_entry() - _test_arachnid_key_crafting() - _test_baroness_lair_and_boss_fight() - _test_lair_timeout() - _test_serialization() - _finish() - -func _test_passage_ticket_entry() -> void: - print("\n--- Test 1: Passage Ticket (71095) Entry to SD2 & SD3 ---") - var sys := SpiderDungeonKeySystem.new() - var inv: Array = [ - {"vnum": SpiderDungeonKeySystem.VNUM_PASSAGE_TICKET, "name": "蛛洞通行证", "count": 2} - ] - - # 1. Level too low (< 40 for SD2) - var res_low = sys.enter_spider_dungeon(2, 35, inv) - _assert(res_low["ok"] == false, "Entry rejected: level 35 < 40") - _assert(res_low["reason"] == "LEVEL_TOO_LOW", "Reason is LEVEL_TOO_LOW") - - var entry_sig := {"fired": false, "floor": 0, "map": ""} - sys.ticket_consumed_entry_granted.connect(func(f: int, m: String): - entry_sig["fired"] = true - entry_sig["floor"] = f - entry_sig["map"] = m - ) - - # 2. Enter SD2 (Lv 45) - var res_sd2 = sys.enter_spider_dungeon(2, 45, inv) - _assert(res_sd2["ok"] == true, "Entered SD2 successfully") - _assert(inv[0]["count"] == 1, "Ticket count decremented 2 -> 1") - _assert(entry_sig["fired"] == true, "ticket_consumed_entry_granted signal emitted") - _assert(entry_sig["floor"] == 2 and entry_sig["map"] == "Spider_Dungeon_2", "Floor 2 reported") - - # 3. Enter SD3 (Lv 55) - var res_sd3 = sys.enter_spider_dungeon(3, 55, inv) - _assert(res_sd3["ok"] == true, "Entered SD3 successfully") - _assert(inv[0] == null, "Last ticket consumed completely") - _assert(entry_sig["floor"] == 3 and entry_sig["map"] == "Spider_Dungeon_3", "Floor 3 reported") - - # 4. Attempt enter without ticket - var res_no_ticket = sys.enter_spider_dungeon(2, 50, inv) - _assert(res_no_ticket["ok"] == false, "Entry rejected without ticket") - _assert(res_no_ticket["reason"] == "NO_PASSAGE_TICKET", "Reason is NO_PASSAGE_TICKET") - -func _test_arachnid_key_crafting() -> void: - print("\n--- Test 2: Crafting Arachnid Key (30324) at Scholar Uriel ---") - var sys := SpiderDungeonKeySystem.new() - var player_gold := {"gold": 100000} - var inv: Array = [ - {"vnum": SpiderDungeonKeySystem.VNUM_SPIDER_POISON_SACK, "name": "蜘蛛毒囊", "count": 1}, - {"vnum": SpiderDungeonKeySystem.VNUM_SPIDER_EGG_SACK, "name": "蜘蛛卵囊", "count": 1}, - {"vnum": SpiderDungeonKeySystem.VNUM_SPIDER_EYES, "name": "蜘蛛眼睛", "count": 1}, - {"vnum": SpiderDungeonKeySystem.VNUM_SPIDER_LEGS, "name": "蜘蛛腿", "count": 1} - ] - - var craft_sig := {"fired": false, "count": 0} - sys.arachnid_key_crafted.connect(func(c: int): - craft_sig["fired"] = true - craft_sig["count"] = c - ) - - # 1. Missing Gold test - player_gold["gold"] = 1000 - var res_no_gold = sys.craft_arachnid_key(inv, player_gold) - _assert(res_no_gold["ok"] == false, "Crafting rejected: gold 1000 < 50000") - _assert(res_no_gold["reason"] == "NOT_ENOUGH_GOLD", "Reason is NOT_ENOUGH_GOLD") - - # 2. Successful craft with 50,000 gold - player_gold["gold"] = 80000 - var res_craft = sys.craft_arachnid_key(inv, player_gold) - _assert(res_craft["ok"] == true, "Crafted Arachnid Key successfully") - _assert(player_gold["gold"] == 30000, "50,000 gold deducted (80000 -> 30000)") - _assert(craft_sig["fired"] == true, "arachnid_key_crafted signal emitted") - - # Verify key exists in inventory - var key_found := false - for it in inv: - if it != null and int(it.get("vnum", 0)) == SpiderDungeonKeySystem.VNUM_ARACHNID_KEY: - key_found = true - break - _assert(key_found == true, "Arachnid Key (30324) generated in inventory") - -func _test_baroness_lair_and_boss_fight() -> void: - print("\n--- Test 3: Baroness Lair Entry, Boss Slay & Rewards ---") - var sys := SpiderDungeonKeySystem.new() - var inv: Array = [ - {"vnum": SpiderDungeonKeySystem.VNUM_ARACHNID_KEY, "name": "蛛网密匙", "count": 1}, - null, null, null - ] - - var open_sig := {"fired": false, "dur": 0.0} - sys.baroness_lair_opened.connect(func(d: float): - open_sig["fired"] = true - open_sig["dur"] = d - ) - - var slay_sig := {"fired": false, "drops": []} - sys.baroness_slain.connect(func(d: Array): - slay_sig["fired"] = true - slay_sig["drops"] = d - ) - - # 1. Open Lair with key - var res_open = sys.open_baroness_lair(55, inv) - _assert(res_open["ok"] == true, "Baroness Lair opened") - _assert(sys.is_lair_active == true, "Lair is active") - _assert(sys.baroness_alive == true, "Spider Baroness is alive") - _assert(inv[0] == null, "Arachnid key consumed") - _assert(open_sig["fired"] == true, "baroness_lair_opened signal emitted") - _assert(open_sig["dur"] == 1200.0, "Lair duration is 1200s (20 min)") - - # 2. Slay Baroness - var res_slay = sys.slay_baroness(inv) - _assert(res_slay["ok"] == true, "Baroness defeated") - _assert(sys.is_lair_active == false, "Lair closed after victory") - _assert(slay_sig["fired"] == true, "baroness_slain signal emitted") - - # Check drops in inventory - var chest_found := false - for it in inv: - if it != null and int(it.get("vnum", 0)) == SpiderDungeonKeySystem.VNUM_CHEST_BARONESS: - chest_found = true - break - _assert(chest_found == true, "King Spider Chest (50181) received in inventory") - -func _test_lair_timeout() -> void: - print("\n--- Test 4: Lair Timeout Failure ---") - var sys := SpiderDungeonKeySystem.new() - var inv: Array = [ - {"vnum": SpiderDungeonKeySystem.VNUM_ARACHNID_KEY, "name": "蛛网密匙", "count": 1} - ] - sys.open_baroness_lair(55, inv) - - var close_sig := {"fired": false, "succ": true} - sys.lair_closed.connect(func(s: bool): - close_sig["fired"] = true - close_sig["succ"] = s - ) - - # Advance time by 1200s - sys.update(1200.0) - _assert(sys.is_lair_active == false, "Lair closed upon timeout") - _assert(close_sig["fired"] == true, "lair_closed signal emitted") - _assert(close_sig["succ"] == false, "Failed challenge due to timeout") - -func _test_serialization() -> void: - print("\n--- Test 5: Serialization & Restoration ---") - var sys1 := SpiderDungeonKeySystem.new() - var inv: Array = [ - {"vnum": SpiderDungeonKeySystem.VNUM_ARACHNID_KEY, "name": "蛛网密匙", "count": 1} - ] - sys1.open_baroness_lair(55, inv) - sys1.update(300.0) # 900s left - - var data = sys1.serialize() - _assert(data["is_lair_active"] == true, "Serialized is_lair_active") - - var sys2 := SpiderDungeonKeySystem.new() - sys2.deserialize(data) - _assert(sys2.is_lair_active == true, "Restored is_lair_active") - _assert(sys2.baroness_alive == true, "Restored baroness_alive") - _assert(is_equal_approx(sys2.lair_time_remaining, 900.0), "Restored time remaining 900s") - -func _finish() -> void: - print("\n=======================================================") - print("Spider Dungeon Key Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_spider_dungeon_key_parity.gd.uid b/project/test_spider_dungeon_key_parity.gd.uid deleted file mode 100644 index 5770030d..00000000 --- a/project/test_spider_dungeon_key_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cq4wdwkt712jm diff --git a/project/test_spider_dungeon_parity.gd b/project/test_spider_dungeon_parity.gd deleted file mode 100644 index 5ed2dad3..00000000 --- a/project/test_spider_dungeon_parity.gd +++ /dev/null @@ -1,134 +0,0 @@ -# test_spider_dungeon_parity.gd —— 40250 蜘蛛男爵夫人巢穴 1:1 纯净回归测试套件 -extends SceneTree - -const SpiderDungeonSystem = preload("res://spider_dungeon_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_spider_dungeon_parity (Direction 1: Spider Dungeon 3 & Baroness 1:1) ===") - _test_entry_and_key_consumption() - _test_egg_frenzy_mechanics() - _test_combat_and_minions_summon() - _test_baroness_kill_and_drops() - _test_timeout_failure() - _finish() - -func _test_entry_and_key_consumption() -> void: - print("\n--- Test 1: Entry Level & Arachnid Key (30327) ---") - var sds := SpiderDungeonSystem.new() - var inv: Array = [ - {"vnum": 30327}, # Arachnid Key - null - ] - - # Level 40 rejected (requires 50) - var res = sds.enter_dungeon(40, inv) - _assert(res["ok"] == false and res["reason"] == "LEVEL_TOO_LOW", "Level < 50 rejected") - - # Missing key rejected - res = sds.enter_dungeon(50, [null]) - _assert(res["ok"] == false and res["reason"] == "MISSING_KEY", "Missing Arachnid Key rejected") - - # Valid entry succeeds - res = sds.enter_dungeon(55, inv) - _assert(res["ok"] == true, "Entry succeeds with Level 55 and key") - _assert(inv[0] == null, "Arachnid key consumed from inventory") - _assert(sds.is_active == true, "Dungeon is now active") - _assert(sds.remaining_time == 1200.0, "20-minute countdown initialized") - _assert(sds.eggs_remaining == 4, "4 spider eggs present") - -func _test_egg_frenzy_mechanics() -> void: - print("\n--- Test 2: Egg Frenzy Mechanics (-DEF, +ATK) ---") - var sds := SpiderDungeonSystem.new() - sds.enter_dungeon(60, [{"vnum": 30327}]) - - _assert(sds.baroness_def == 400, "Initial Baroness DEF is 400") - _assert(sds.baroness_atk == 800, "Initial Baroness ATK is 800") - - # Destroy 1st egg: -15% DEF (340), +20% ATK (960) - var res = sds.destroy_egg() - _assert(res["ok"] == true, "1st egg destroyed") - _assert(sds.baroness_def == 340, "Baroness DEF reduced to 340 (-15%)") - _assert(sds.baroness_atk == 960, "Baroness ATK increased to 960 (+20%)") - - # Destroy 2nd egg: -30% DEF (280), +40% ATK (1120) - sds.destroy_egg() - _assert(sds.baroness_def == 280, "2 eggs broken: DEF 280") - _assert(sds.baroness_atk == 1120, "2 eggs broken: ATK 1120") - - # Destroy 3rd egg: -45% DEF (220), +60% ATK (1280) - sds.destroy_egg() - _assert(sds.baroness_def == 220, "3 eggs broken: DEF 220") - - # Destroy 4th egg: -60% DEF (160), +80% ATK (1440) - sds.destroy_egg() - _assert(sds.baroness_def == 160, "All 4 eggs broken: DEF down to 160 (-60%)") - _assert(sds.baroness_atk == 1440, "All 4 eggs broken: ATK up to 1440 (+80%)") - - # 5th egg attempt rejected - res = sds.destroy_egg() - _assert(res["ok"] == false and res["reason"] == "NO_EGGS_LEFT", "Cannot destroy nonexistent egg") - -func _test_combat_and_minions_summon() -> void: - print("\n--- Test 3: Combat Damage & 50% HP Minions Wave ---") - var sds := SpiderDungeonSystem.new() - sds.enter_dungeon(60, [{"vnum": 30327}]) - # Break 4 eggs so defense is 160 - for i in range(4): sds.destroy_egg() - - # Deal 50,000 raw damage -> effective damage is 50000 - 160 = 49840 - var res = sds.damage_baroness(50000) - _assert(res["ok"] == true, "Damaged Baroness") - _assert(res["damage_dealt"] == 49840, "Effective damage calculated after 160 DEF") - _assert(sds.minions_spawned == false, "Minions not yet spawned above 50% HP") - - # Deal another 80,000 damage -> drops below 50% (125,000) - res = sds.damage_baroness(80000) - _assert(sds.baroness_hp <= 125000, "HP dropped below 50%") - _assert(sds.minions_spawned == true, "50% HP triggered minions wave") - _assert(res["minions_summoned"] == true, "Minions summon event reported") - -func _test_baroness_kill_and_drops() -> void: - print("\n--- Test 4: Baroness Kill & Baroness Chest Drop ---") - var sds := SpiderDungeonSystem.new() - sds.enter_dungeon(60, [{"vnum": 30327}]) - - # Deal lethal damage - var res = sds.damage_baroness(300000) - _assert(res["ok"] == true and res["dead"] == true, "Baroness slain") - _assert(sds.dungeon_completed == true, "Dungeon cleared") - _assert(sds.is_active == false, "Dungeon deactivated") - - var drops: Array[Dictionary] = res["drops"] - _assert(drops.size() == 2, "2 items dropped") - _assert(drops[0]["vnum"] == SpiderDungeonSystem.ITEM_BARONESS_CHEST, "Dropped Baroness Chest (50157)") - _assert(drops[1]["vnum"] == SpiderDungeonSystem.ITEM_TITAN_SHIELD, "Dropped Titan Shield (13129)") - -func _test_timeout_failure() -> void: - print("\n--- Test 5: Timeout Failure ---") - var sds := SpiderDungeonSystem.new() - sds.enter_dungeon(60, [{"vnum": 30327}]) - - # Update 1201s (past 1200s limit) - var res = sds.update(1201.0) - _assert(res["failed"] == true and res["reason"] == "TIME_OUT", "Dungeon failed on timeout") - _assert(sds.is_active == false, "Dungeon marked inactive") - -func _finish() -> void: - print("\n=== SUMMARY: %d passed, %d failed ===" % [_passed, _failed]) - if _failed == 0: - print("ALL TESTS PASSED!\n") - quit(0) - else: - printerr("SOME TESTS FAILED!\n") - quit(1) diff --git a/project/test_spider_dungeon_parity.gd.uid b/project/test_spider_dungeon_parity.gd.uid deleted file mode 100644 index d6dda73d..00000000 --- a/project/test_spider_dungeon_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b4rxtexijhmc0 diff --git a/project/test_status_skill_reset_parity.gd b/project/test_status_skill_reset_parity.gd deleted file mode 100644 index 9567090a..00000000 --- a/project/test_status_skill_reset_parity.gd +++ /dev/null @@ -1,167 +0,0 @@ -# test_status_skill_reset_parity.gd —— 40250 属性重修卷轴与忘却之书洗点 1:1 回归测试套件 -extends SceneTree - -const StatusSkillResetSystem = preload("res://status_skill_reset_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_status_skill_reset_parity (Direction 2: Status & Skill Reset 1:1) ===") - _test_single_status_reset() - _test_all_status_reset() - _test_book_of_oblivion() - _test_all_skills_reset() - _finish() - -func _test_single_status_reset() -> void: - print("\n--- Test 1: Single Status Redistribution (71002) ---") - var sys := StatusSkillResetSystem.new() - var player := { - "job": "warrior", - "vit": 16, - "int": 3, - "str": 50, # Base is 15 -> 35 points added - "dex": 5, - "unused_status_points": 0 - } - var inv: Array = [ - {"vnum": StatusSkillResetSystem.VNUM_STATUS_REDIST_SCROLL, "name": "属性重修卷轴", "count": 2} - ] - - var reset_sig := {"fired": false, "stat": "", "refunded": 0} - sys.status_reset.connect(func(s: String, r: int): - reset_sig["fired"] = true - reset_sig["stat"] = s - reset_sig["refunded"] = r - ) - - # Reset STR - var res_str = sys.reset_single_status("str", player, inv, 0) - _assert(res_str["ok"] == true, "STR reset successfully") - _assert(player["str"] == 15, "STR returned to warrior base 15") - _assert(player["unused_status_points"] == 35, "35 points refunded to unused pool") - _assert(inv[0]["count"] == 1, "Scroll count decremented 2 -> 1") - _assert(reset_sig["fired"] == true, "status_reset signal emitted") - _assert(reset_sig["stat"] == "str" and reset_sig["refunded"] == 35, "Signal reported correct stat & points") - - # Try reset again when already at base - var res_base = sys.reset_single_status("str", player, inv, 0) - _assert(res_base["ok"] == false, "Reset rejected when already at base") - _assert(res_base["reason"] == "STAT_ALREADY_AT_BASE", "Reason is STAT_ALREADY_AT_BASE") - -func _test_all_status_reset() -> void: - print("\n--- Test 2: All Status Reset (Blood Pill 71003) ---") - var sys := StatusSkillResetSystem.new() - var player := { - "job": "warrior", - "vit": 36, # Base 16 (+20) - "int": 3, # Base 3 (+0) - "str": 40, # Base 15 (+25) - "dex": 15, # Base 5 (+10) - "unused_status_points": 5 # total extra = 20 + 25 + 10 = 55 - } - var inv: Array = [ - {"vnum": StatusSkillResetSystem.VNUM_ALL_STATUS_RESET_SCROLL, "name": "全属性重置卷轴", "count": 1} - ] - - var all_sig := {"fired": false, "refunded": 0} - sys.all_status_reset.connect(func(r: int): - all_sig["fired"] = true - all_sig["refunded"] = r - ) - - var res_all = sys.reset_all_status(player, inv, 0) - _assert(res_all["ok"] == true, "All status reset succeeded") - _assert(player["vit"] == 16, "VIT back to base 16") - _assert(player["str"] == 15, "STR back to base 15") - _assert(player["dex"] == 5, "DEX back to base 5") - _assert(player["unused_status_points"] == 60, "Refunded 55 points (5 existing + 55 = 60 total)") - _assert(inv[0] == null, "Scroll consumed from slot 0") - _assert(all_sig["fired"] == true, "all_status_reset signal emitted") - _assert(all_sig["refunded"] == 55, "Reported 55 refunded points") - -func _test_book_of_oblivion() -> void: - print("\n--- Test 3: Book of Oblivion (50311~50320) Single Skill Point Reset ---") - var sys := StatusSkillResetSystem.new() - var player_skills := { - 1: 16, # 三连斩 Lv 16 - 2: 0, # 旋风斩 Lv 0 - 3: 20 # 剑气 Master (M1) - } - var player_data := {"unused_skill_points": 0} - var inv: Array = [ - {"vnum": 50311, "name": "忘却之书(三连斩)", "target_skill": 1, "count": 2}, - {"vnum": 50312, "name": "忘却之书(旋风斩)", "target_skill": 2, "count": 1}, - {"vnum": 50313, "name": "忘却之书(剑气)", "target_skill": 3, "count": 1} - ] - - var sk_sig := {"fired": false, "id": 0, "new_lv": 0} - sys.skill_point_reset.connect(func(id: int, nlv: int): - sk_sig["fired"] = true - sk_sig["id"] = id - sk_sig["new_lv"] = nlv - ) - - # 1. Reset Skill 1 (16 -> 15) - var res1 = sys.reset_single_skill_point(1, player_skills, player_data, inv, 0) - _assert(res1["ok"] == true, "Skill 1 point reset succeeded") - _assert(player_skills[1] == 15, "Skill 1 level reduced 16 -> 15") - _assert(player_data["unused_skill_points"] == 1, "1 skill point refunded") - _assert(inv[0]["count"] == 1, "Book count decremented 2 -> 1") - _assert(sk_sig["fired"] == true, "skill_point_reset signal emitted") - _assert(sk_sig["id"] == 1 and sk_sig["new_lv"] == 15, "Signal reported skill 1 lv 15") - - # 2. Skill level 0 cannot be reset - var res_zero = sys.reset_single_skill_point(2, player_skills, player_data, inv, 1) - _assert(res_zero["ok"] == false, "Skill lv 0 rejected") - _assert(res_zero["reason"] == "SKILL_LEVEL_ZERO", "Reason is SKILL_LEVEL_ZERO") - - # 3. Master skill (>= 20) cannot be downgraded by Book of Oblivion (40250 rule) - var res_master = sys.reset_single_skill_point(3, player_skills, player_data, inv, 2) - _assert(res_master["ok"] == false, "Master skill rejected") - _assert(res_master["reason"] == "SKILL_IS_MASTER", "Reason is SKILL_IS_MASTER") - -func _test_all_skills_reset() -> void: - print("\n--- Test 4: All Skills Reset Scroll (71001) ---") - var sys := StatusSkillResetSystem.new() - var player_skills := { - 1: 15, - 2: 18, - 3: 7 - } # Total invested: 15 + 18 + 7 = 40 - var player_data := {"unused_skill_points": 2} - var inv: Array = [ - {"vnum": StatusSkillResetSystem.VNUM_SKILL_RESET_SCROLL, "name": "技能重置卷轴", "count": 1} - ] - - var all_sk_sig := {"fired": false, "refunded": 0} - sys.all_skills_reset.connect(func(r: int): - all_sk_sig["fired"] = true - all_sk_sig["refunded"] = r - ) - - var res_all = sys.reset_all_skills(player_skills, player_data, inv, 0) - _assert(res_all["ok"] == true, "All skills reset succeeded") - _assert(player_skills[1] == 0 and player_skills[2] == 0 and player_skills[3] == 0, "All skills cleared to 0") - _assert(player_data["unused_skill_points"] == 42, "Refunded 40 points (2 existing + 40 = 42 total)") - _assert(inv[0] == null, "Scroll consumed") - _assert(all_sk_sig["fired"] == true, "all_skills_reset signal emitted") - _assert(all_sk_sig["refunded"] == 40, "Signal reported 40 refunded points") - -func _finish() -> void: - print("\n=======================================================") - print("Status & Skill Reset Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_status_skill_reset_parity.gd.uid b/project/test_status_skill_reset_parity.gd.uid deleted file mode 100644 index d7f0647a..00000000 --- a/project/test_status_skill_reset_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://chx2btpt8bfsj diff --git a/project/test_system_setting_parity.gd b/project/test_system_setting_parity.gd deleted file mode 100644 index f6ae6a6d..00000000 --- a/project/test_system_setting_parity.gd +++ /dev/null @@ -1,116 +0,0 @@ -# test_system_setting_parity.gd —— 40250 系统视听与战斗选项 1:1 纯净回归测试套件 -extends SceneTree - -const SystemSettingSystem = preload("res://system_setting_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_system_setting_parity (Direction 1: System Setting & PK Mode 1:1) ===") - _test_audio_and_display_settings() - _test_pk_modes_and_rules() - _test_import_export_settings() - _finish() - -func _test_audio_and_display_settings() -> void: - print("\n--- Test 1: Audio & Display Settings Clamping & Signals ---") - var sss := SystemSettingSystem.new() - - var signal_data := {"fired": false, "name": "", "val": null} - sss.setting_changed.connect(func(n: String, v: Variant): - signal_data["fired"] = true - signal_data["name"] = n - signal_data["val"] = v - ) - - # BGM volume clamping (1.5 -> 1.0) - sss.bgm_volume = 1.5 - _assert(sss.bgm_volume == 1.0, "BGM volume clamped to 1.0") - _assert(signal_data["fired"] == true and signal_data["name"] == "bgm_volume", "setting_changed signal fired for bgm") - - # SFX volume clamping (-0.2 -> 0.0) - sss.sfx_volume = -0.2 - _assert(sss.sfx_volume == 0.0, "SFX volume clamped to 0.0") - - # Camera distance clamping (50.0 -> 40.0) - sss.camera_distance = 50.0 - _assert(sss.camera_distance == 40.0, "Camera distance clamped to 40.0") - - # Fog & Shadow levels - sss.fog_level = 2 - _assert(sss.fog_level == 2, "Fog level is 2") - sss.shadow_quality = 0 - _assert(sss.shadow_quality == 0, "Shadow quality is 0") - -func _test_pk_modes_and_rules() -> void: - print("\n--- Test 2: 4 PK Modes & Combat Rule Matrix ---") - var sss := SystemSettingSystem.new() - - var pk_signal := {"fired": false, "mode": -1, "name": ""} - sss.pk_mode_changed.connect(func(m: int, n: String): - pk_signal["fired"] = true - pk_signal["mode"] = m - pk_signal["name"] = n - ) - - # 1. Monster target is ALWAYS attackable - _assert(sss.can_attack_target(false) == true, "Monsters always attackable in Peace mode") - - # 2. PEACE Mode (Default) - _assert(sss.can_attack_target(true, 5000) == false, "Cannot attack positive karma player in Peace mode") - _assert(sss.can_attack_target(true, -5000) == false, "Cannot attack red name player in Peace mode") - - # 3. REVENGE Mode (善恶反击) - sss.set_pk_mode(SystemSettingSystem.PkMode.REVENGE) - _assert(pk_signal["fired"] == true and pk_signal["mode"] == SystemSettingSystem.PkMode.REVENGE, "pk_mode_changed signal fired") - _assert(sss.can_attack_target(true, 5000) == false, "Cannot attack positive karma player in Revenge mode") - _assert(sss.can_attack_target(true, -5000) == true, "CAN attack red name player in Revenge mode") - - # 4. GUILD Mode (阵营保护) - sss.set_pk_mode(SystemSettingSystem.PkMode.GUILD) - _assert(sss.can_attack_target(true, 5000, 100, 100) == false, "Cannot attack same guild player (guild 100 vs 100)") - _assert(sss.can_attack_target(true, 5000, 101, 100) == true, "CAN attack enemy guild player (guild 101 vs 100)") - - # 5. FREE Mode (自由对决) - sss.set_pk_mode(SystemSettingSystem.PkMode.FREE) - _assert(sss.can_attack_target(true, 5000) == true, "Can attack anyone in Free mode") - -func _test_import_export_settings() -> void: - print("\n--- Test 3: Import & Export Configuration Dictionary ---") - var sss := SystemSettingSystem.new() - sss.bgm_volume = 0.5 - sss.sfx_volume = 0.6 - sss.camera_distance = 32.0 - sss.set_pk_mode(SystemSettingSystem.PkMode.REVENGE) - sss.show_critical_hit = false - - var exported := sss.export_settings() - _assert(exported["bgm_volume"] == 0.5, "Exported BGM volume 0.5") - _assert(exported["current_pk_mode"] == SystemSettingSystem.PkMode.REVENGE, "Exported PK mode Revenge") - _assert(exported["show_critical_hit"] == false, "Exported show_critical_hit false") - - # Create new instance and import - var sss2 := SystemSettingSystem.new() - sss2.import_settings(exported) - _assert(sss2.bgm_volume == 0.5, "Imported BGM volume restored") - _assert(sss2.camera_distance == 32.0, "Imported camera distance restored") - _assert(sss2.current_pk_mode == SystemSettingSystem.PkMode.REVENGE, "Imported PK mode restored") - _assert(sss2.show_critical_hit == false, "Imported show_critical_hit restored") - -func _finish() -> void: - print("\n==========================================") - print("System Setting Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_system_setting_parity.gd.uid b/project/test_system_setting_parity.gd.uid deleted file mode 100644 index ea05b1c4..00000000 --- a/project/test_system_setting_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ddm1wpr6y2372 diff --git a/project/test_title_achievement_parity.gd b/project/test_title_achievement_parity.gd deleted file mode 100644 index 5bc0c6e2..00000000 --- a/project/test_title_achievement_parity.gd +++ /dev/null @@ -1,121 +0,0 @@ -# test_title_achievement_parity.gd —— 40250 头衔成就系统 1:1 纯净回归测试套件 -extends SceneTree - -const TitleAchievementSystem = preload("res://title_achievement_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_title_achievement_parity (Direction 3: Title & Achievements 1:1) ===") - _test_achievement_definitions() - _test_achievement_unlock() - _test_title_equip_and_bonuses() - _test_multiple_achievements_points_accumulation() - _finish() - -func _test_achievement_definitions() -> void: - print("\n--- Test 1: Achievement Milestone Configurations ---") - var tas := TitleAchievementSystem.new() - _assert(TitleAchievementSystem.ACHIEVEMENTS.has("ACH_DEMON_TOWER"), "Demon Tower achievement defined") - _assert(TitleAchievementSystem.ACHIEVEMENTS.has("ACH_SPIDER_BARONESS"), "Spider Baroness achievement defined") - _assert(TitleAchievementSystem.ACHIEVEMENTS.has("ACH_BERAN_SETAOU"), "Beran-Setaou achievement defined") - _assert(TitleAchievementSystem.ACHIEVEMENTS.has("ACH_AZRAEL"), "Azrael achievement defined") - _assert(TitleAchievementSystem.ACHIEVEMENTS.has("ACH_FOUR_WORLD_BOSSES"), "Four World Bosses achievement defined") - _assert(TitleAchievementSystem.ACHIEVEMENTS.has("ACH_SKILL_GRANDMASTER"), "Skill Grandmaster achievement defined") - _assert(TitleAchievementSystem.ACHIEVEMENTS.has("ACH_EPIC_BLACKSMITH"), "Epic Blacksmith achievement defined") - -func _test_achievement_unlock() -> void: - print("\n--- Test 2: Milestone Unlock & Signal Emission ---") - var tas := TitleAchievementSystem.new() - - # Invalid ach_id - var r_bad = tas.complete_achievement("INVALID_ACH") - _assert(r_bad["ok"] == false and r_bad["reason"] == "INVALID_ACH_ID", "Invalid achievement rejected") - - var unlock_signal := {"fired": false, "id": "", "title": ""} - tas.achievement_unlocked.connect(func(aid: String, _aname: String, tname: String): - unlock_signal["fired"] = true - unlock_signal["id"] = aid - unlock_signal["title"] = tname - ) - - # Complete ACH_BERAN_SETAOU - var r = tas.complete_achievement("ACH_BERAN_SETAOU") - _assert(r["ok"] == true, "Beran-Setaou achievement completed") - _assert(tas.is_achievement_unlocked("ACH_BERAN_SETAOU") == true, "Achievement marked unlocked") - _assert(tas.total_achievement_points == 100, "100 achievement points awarded") - _assert(tas.unlocked_titles.has("屠龙勇士"), "Title '屠龙勇士' added to unlocked titles") - _assert(unlock_signal["fired"] == true and unlock_signal["title"] == "屠龙勇士", "achievement_unlocked signal fired") - - # Duplicate complete rejected - var r_dup = tas.complete_achievement("ACH_BERAN_SETAOU") - _assert(r_dup["ok"] == false and r_dup["reason"] == "ALREADY_COMPLETED", "Duplicate completion rejected") - -func _test_title_equip_and_bonuses() -> void: - print("\n--- Test 3: Title Equip, Unequip, and Stat Buffs ---") - var tas := TitleAchievementSystem.new() - - # Try to equip locked title - var r_lock = tas.equip_title("深渊征服者") - _assert(r_lock["ok"] == false and r_lock["reason"] == "TITLE_NOT_UNLOCKED", "Locked title cannot be equipped") - - # Unlock and equip "屠龙勇士" - tas.complete_achievement("ACH_BERAN_SETAOU") - - var equip_signal := {"fired": false, "title": ""} - tas.title_equipped.connect(func(t: String, _b: Dictionary): - equip_signal["fired"] = true - equip_signal["title"] = t - ) - - var r_eq = tas.equip_title("屠龙勇士") - _assert(r_eq["ok"] == true, "Equipped '屠龙勇士' successfully") - _assert(tas.active_title == "屠龙勇士", "active_title is '屠龙勇士'") - _assert(equip_signal["fired"] == true and equip_signal["title"] == "屠龙勇士", "title_equipped signal fired") - - var bonuses = tas.get_active_bonuses() - _assert(bonuses.has("boss_dmg_pct") and bonuses["boss_dmg_pct"] == 10, "Title grants +10% Boss damage") - - # Unequip title - var unequip_signal := {"fired": false} - tas.title_unequipped.connect(func(): unequip_signal["fired"] = true) - - tas.unequip_title() - _assert(tas.active_title == "", "active_title cleared") - _assert(unequip_signal["fired"] == true, "title_unequipped signal fired") - _assert(tas.get_active_bonuses().is_empty(), "No bonuses active after unequip") - -func _test_multiple_achievements_points_accumulation() -> void: - print("\n--- Test 4: Points Accumulation & Title Switching ---") - var tas := TitleAchievementSystem.new() - tas.complete_achievement("ACH_DEMON_TOWER") # 50 pts, "恶魔征服者", +500 HP - tas.complete_achievement("ACH_AZRAEL") # 100 pts, "深渊征服者", +50 DEF - - _assert(tas.total_achievement_points == 150, "Total points accumulated: 150 (50 + 100)") - _assert(tas.unlocked_titles.size() == 2, "2 titles unlocked") - - # Equip Demon Tower title - tas.equip_title("恶魔征服者") - _assert(tas.get_active_bonuses()["max_hp"] == 500, "Grants +500 Max HP") - - # Switch to Azrael title - tas.equip_title("深渊征服者") - _assert(tas.get_active_bonuses()["def"] == 50, "Switched to +50 DEF") - -func _finish() -> void: - print("\n==========================================") - print("Title Achievement Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_title_achievement_parity.gd.uid b/project/test_title_achievement_parity.gd.uid deleted file mode 100644 index fe94ea08..00000000 --- a/project/test_title_achievement_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://pafg61vig084 diff --git a/project/test_title_trophy_buff_parity.gd b/project/test_title_trophy_buff_parity.gd deleted file mode 100644 index 77ac9d56..00000000 --- a/project/test_title_trophy_buff_parity.gd +++ /dev/null @@ -1,140 +0,0 @@ -# test_title_trophy_buff_parity.gd —— 40250 真实称号词条战力加成与奖杯成就图鉴 1:1 回归测试套件 -extends SceneTree - -const TitleTrophyBuffSystem = preload("res://title_trophy_buff_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_title_trophy_buff_parity (Direction 4: Title Trophy Buffs 1:1) ===") - _test_catalog_and_unlock() - _test_title_equip_and_stat_injection() - _test_title_switching_and_unequip() - _test_trophy_milestone_progression() - _finish() - -func _test_catalog_and_unlock() -> void: - print("\n--- Test 1: Title Catalog & Unlocking ---") - var sys := TitleTrophyBuffSystem.new() - _assert(TitleTrophyBuffSystem.TITLES_CATALOG.size() == 7, "Catalog has 7 iconic titles") - - var unk_res = sys.unlock_title("InvalidTitle") - _assert(unk_res["ok"] == false and unk_res["reason"] == "UNKNOWN_TITLE", "Unknown title rejected") - - var u1 = sys.unlock_title("屠龙尊者", 100) - _assert(u1["ok"] == true, "屠龙尊者 unlocked") - _assert(sys.trophy_points == 100, "Trophy points = 100") - _assert("屠龙尊者" in sys.unlocked_titles, "Title in unlocked list") - -func _test_title_equip_and_stat_injection() -> void: - print("\n--- Test 2: Equipping Title & Live Stat Injection ---") - var sys := TitleTrophyBuffSystem.new() - sys.unlock_title("屠龙尊者") - - var player := { - "name": "战神", - "max_hp": 10000, - "atk": 500, - "def": 300, - "boss_dmg_pct": 0, - "crit_pct": 5 - } - - var equip_sig := {"fired": false, "title": ""} - sys.title_stat_buffs_applied.connect(func(t: String, _b: Dictionary): - equip_sig["fired"] = true - equip_sig["title"] = t - ) - - # Try equip locked title - var fail_lock = sys.equip_title("百战名将", player) - _assert(fail_lock["ok"] == false and fail_lock["reason"] == "TITLE_NOT_UNLOCKED", "Cannot equip locked title") - - # Equip "屠龙尊者" (boss_dmg_pct: 15, max_hp: 2000, crit_pct: 5) - var res = sys.equip_title("屠龙尊者", player) - _assert(res["ok"] == true, "Title equipped") - _assert(equip_sig["fired"] == true and equip_sig["title"] == "屠龙尊者", "title_stat_buffs_applied emitted") - _assert(player["max_hp"] == 12000, "Max HP increased 10000 -> 12000 (+2000)") - _assert(player["boss_dmg_pct"] == 15, "Boss damage increased 0 -> 15%") - _assert(player["crit_pct"] == 10, "Crit rate increased 5 -> 10% (+5%)") - -func _test_title_switching_and_unequip() -> void: - print("\n--- Test 3: Zero-Drift Switching & Unequipping ---") - var sys := TitleTrophyBuffSystem.new() - sys.unlock_title("屠龙尊者") - sys.unlock_title("百战名将") - - var player := { - "max_hp": 10000, - "atk": 500, - "def": 300, - "boss_dmg_pct": 0, - "crit_pct": 5 - } - - # 1. Equip 屠龙尊者 - sys.equip_title("屠龙尊者", player) - _assert(player["max_hp"] == 12000, "HP +2000") - - # 2. Switch directly to 百战名将 (atk: 80, def: 50, max_hp: 1500) - # 屠龙尊者 stats (-2000 HP, -15% boss dmg, -5% crit) must be deducted first! - sys.equip_title("百战名将", player) - _assert(player["max_hp"] == 11500, "HP is 10000 + 1500 = 11500 (Clean subtraction of 2000, addition of 1500)") - _assert(player["atk"] == 580, "Atk is 500 + 80 = 580") - _assert(player["def"] == 350, "Def is 300 + 50 = 350") - _assert(player["boss_dmg_pct"] == 0, "Boss damage reverted back to 0") - _assert(player["crit_pct"] == 5, "Crit rate reverted back to baseline 5") - - # 3. Unequip 百战名将 - var unequip_sig := {"fired": false} - sys.title_stat_buffs_removed.connect(func(_t: String): unequip_sig["fired"] = true) - - sys.unequip_title(player) - _assert(unequip_sig["fired"] == true, "title_stat_buffs_removed emitted") - _assert(player["max_hp"] == 10000, "HP returned to exact initial 10000") - _assert(player["atk"] == 500, "Atk returned to exact initial 500") - _assert(player["def"] == 300, "Def returned to exact initial 300") - -func _test_trophy_milestone_progression() -> void: - print("\n--- Test 4: Trophy Milestones & Cumulative Passives ---") - var sys := TitleTrophyBuffSystem.new() - - var milestone_events := [] - sys.trophy_milestone_reached.connect(func(tier: int, tname: String, _desc: String): - milestone_events.append({"tier": tier, "name": tname}) - ) - - # Unlock 1 title (50 pts) -> Reaches Tier 1 (50 pts) - sys.unlock_title("屠龙尊者", 50) - _assert(milestone_events.size() == 1 and milestone_events[0]["tier"] == 1, "Reached Tier 1 (青铜试炼奖杯)") - - # Unlock 2 more titles (+100 pts -> 150 pts total) -> Reaches Tier 2 (150 pts) - sys.unlock_title("恶魔克星", 50) - sys.unlock_title("魔石终结者", 50) - _assert(milestone_events.size() == 2 and milestone_events[1]["tier"] == 2, "Reached Tier 2 (白银征服奖杯)") - - var passives = sys.get_total_trophy_passive_bonuses() - _assert(passives.has("all_stats") and passives["all_stats"] == 3, "Has Tier 1 passive: all_stats +3") - _assert(passives.has("max_hp") and passives["max_hp"] == 1000, "Has Tier 2 passive: max_hp +1000") - _assert(passives.has("drop_rate_pct") and passives["drop_rate_pct"] == 5, "Has Tier 2 passive: drop_rate +5%") - -func _finish() -> void: - print("\n==========================================") - print("Title Trophy Buff Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Title Trophy Buff parity tests passed!") - quit(0) diff --git a/project/test_title_trophy_buff_parity.gd.uid b/project/test_title_trophy_buff_parity.gd.uid deleted file mode 100644 index 91d2ddad..00000000 --- a/project/test_title_trophy_buff_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://lttbelq13bsv diff --git a/project/test_treasure_box_key_parity.gd b/project/test_treasure_box_key_parity.gd deleted file mode 100644 index 86c2e01c..00000000 --- a/project/test_treasure_box_key_parity.gd +++ /dev/null @@ -1,166 +0,0 @@ -# test_treasure_box_key_parity.gd —— Metin2 40250 金银宝箱与钥匙解锁系统 1:1 测试套件 -extends SceneTree - -const TreasureBoxKeySystem = preload("res://treasure_box_key_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_treasure_box_key_parity (Direction 2: Treasure Boxes & Keys 1:1) ===") - - test_silver_box_and_key() - test_gold_box_and_key() - test_mismatch_and_rejections() - test_plus_boxes() - test_history_and_serialization() - - print("\n=======================================================") - print("Treasure Box & Key Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - - if _failed > 0: - quit(1) - else: - quit(0) - -func test_silver_box_and_key() -> void: - print("\n--- Test 1: Silver Key Unlocks Silver Box (40250 1:1) ---") - var system = TreasureBoxKeySystem.new() - - var unlocked_signals := [] - system.box_unlocked.connect(func(box, key, reward): - unlocked_signals.append({"box": box, "key": key, "reward": reward}) - ) - - var inv: Array = [ - {"vnum": 50008, "count": 2}, # 银钥匙 x2 (slot 0) - {"vnum": 50006, "count": 1} # 银宝箱 x1 (slot 1) - ] - - _assert(system.can_unlock(50008, 50006) == true, "Silver key matches silver box") - - # 开启宝箱,强制掉落索引 0 (技能书 50300) - var res = system.unlock_box_with_key(inv, 0, 1, 0) - _assert(res["ok"] == true, "Unlocked silver box successfully") - _assert(inv[0] != null and inv[0]["count"] == 1, "Key count decremented from 2 to 1") - # 原宝箱槽位已被新发放的奖励技能书复用填充 - _assert(res["reward"]["vnum"] == 50300, "Rewarded skill book (50300)") - var has_box = false - for it in inv: - if it != null and it.get("vnum", 0) == 50006: - has_box = true - _assert(has_box == false, "Silver box was consumed and no longer in inventory") - _assert(unlocked_signals.size() == 1, "box_unlocked signal fired") - _assert(unlocked_signals[0]["box"] == 50006, "Signal reports silver box") - -func test_gold_box_and_key() -> void: - print("\n--- Test 2: Gold Key Unlocks Gold Box (40250 1:1) ---") - var system = TreasureBoxKeySystem.new() - - var inv: Array = [ - {"vnum": 50009, "count": 1}, # 金钥匙 (slot 0) - {"vnum": 50007, "count": 1} # 金宝箱 (slot 1) - ] - - _assert(system.can_unlock(50009, 50007) == true, "Gold key matches gold box") - - # 开启金宝箱,强制掉落索引 0 (祝福卷轴 25040) - var res = system.unlock_box_with_key(inv, 0, 1, 0) - _assert(res["ok"] == true, "Unlocked gold box successfully") - var has_gold_key = false - var has_gold_box = false - for it in inv: - if it != null: - if it.get("vnum", 0) == 50009: - has_gold_key = true - if it.get("vnum", 0) == 50007: - has_gold_box = true - _assert(has_gold_key == false, "Gold key consumed") - _assert(has_gold_box == false, "Gold box consumed") - _assert(res["reward"]["vnum"] == 25040, "Rewarded Blessing Scroll (25040)") - -func test_mismatch_and_rejections() -> void: - print("\n--- Test 3: Key Mismatch & Invalid Targets (40250 1:1) ---") - var system = TreasureBoxKeySystem.new() - - var fail_signals := [] - system.unlock_failed.connect(func(reason): - fail_signals.append(reason) - ) - - var inv: Array = [ - {"vnum": 50008, "count": 1}, # 银钥匙 (slot 0) - {"vnum": 50007, "count": 1}, # 金宝箱 (slot 1) - {"vnum": 27987, "count": 1} # 珍珠蚌 (slot 2, 非宝箱) - ] - - # 1. 银钥匙对金宝箱 -> 拒绝 - _assert(system.can_unlock(50008, 50007) == false, "Silver key cannot unlock gold box") - var res = system.unlock_box_with_key(inv, 0, 1) - _assert(res["ok"] == false, "Rejected mismatch") - _assert(res["reason"] == "KEY_MISMATCH", "Reason is KEY_MISMATCH") - _assert(inv[0]["count"] == 1, "Key not consumed") - _assert(inv[1]["count"] == 1, "Box not consumed") - - # 2. 钥匙拖拽至非宝箱物品 -> 拒绝 - res = system.unlock_box_with_key(inv, 0, 2) - _assert(res["ok"] == false, "Rejected non-box target") - _assert(res["reason"] == "NOT_A_TREASURE_BOX", "Reason is NOT_A_TREASURE_BOX") - - # 3. 非钥匙道具拖拽 -> 拒绝 - res = system.unlock_box_with_key(inv, 2, 1) - _assert(res["ok"] == false, "Rejected non-key item") - _assert(res["reason"] == "NOT_A_KEY", "Reason is NOT_A_KEY") - - _assert(fail_signals.size() == 3, "All 3 failure signals emitted") - -func test_plus_boxes() -> void: - print("\n--- Test 4: Silver+ and Gold+ Boxes (40250 1:1) ---") - var system = TreasureBoxKeySystem.new() - - var inv: Array = [ - {"vnum": 50014, "count": 1}, # 银钥匙+ - {"vnum": 50012, "count": 1}, # 银宝箱+ - {"vnum": 50015, "count": 1}, # 金钥匙+ - {"vnum": 50013, "count": 1} # 金宝箱+ - ] - - # 银钥匙+ 开 银宝箱+ - var res = system.unlock_box_with_key(inv, 0, 1, 0) - _assert(res["ok"] == true, "Unlocked Silver Box+") - _assert(res["reward"]["count"] == 2, "Silver+ gave 2 skill books") - - # 金钥匙+ 开 金宝箱+ - res = system.unlock_box_with_key(inv, 2, 3, 0) - _assert(res["ok"] == true, "Unlocked Gold Box+") - _assert(res["reward"]["vnum"] == 25040 and res["reward"]["count"] == 2, "Gold+ gave 2 Blessing Scrolls") - -func test_history_and_serialization() -> void: - print("\n--- Test 5: History Tracking & Serialization ---") - var system = TreasureBoxKeySystem.new() - var inv: Array = [ - {"vnum": 50008, "count": 1}, - {"vnum": 50006, "count": 1} - ] - system.unlock_box_with_key(inv, 0, 1, 0) - - var history = system.get_open_history() - _assert(history.size() == 1, "Recorded 1 unlock history") - _assert(history[0]["box_vnum"] == 50006, "History records box 50006") - - var data = system.serialize() - var restored = TreasureBoxKeySystem.new() - restored.deserialize(data) - _assert(restored.get_open_history().size() == 1, "Restored 1 history item") - - system.clear_history() - _assert(system.get_open_history().is_empty(), "Cleared history") diff --git a/project/test_treasure_box_key_parity.gd.uid b/project/test_treasure_box_key_parity.gd.uid deleted file mode 100644 index 284210ac..00000000 --- a/project/test_treasure_box_key_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b5perrjaulcrw diff --git a/project/test_wardrobe_transmog_parity.gd b/project/test_wardrobe_transmog_parity.gd deleted file mode 100644 index f3f903e4..00000000 --- a/project/test_wardrobe_transmog_parity.gd +++ /dev/null @@ -1,133 +0,0 @@ -# test_wardrobe_transmog_parity.gd —— 40250 装备外观幻化与试衣间 1:1 回归测试套件 -extends SceneTree - -const WardrobeTransmogSystem = preload("res://wardrobe_transmog_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_wardrobe_transmog_parity (Direction 3: Wardrobe Transmog 1:1) ===") - _test_preview_mode() - _test_weapon_transmutation() - _test_type_mismatch_rejection() - _test_transmutation_revert() - _finish() - -func _test_preview_mode() -> void: - print("\n--- Test 1: Wardrobe Preview Mode ---") - var sys := WardrobeTransmogSystem.new() - var sword := {"vnum": 189, "name": "毒剑+9", "type": WardrobeTransmogSystem.TYPE_WEAPON} - var skin := {"vnum": 199, "name": "圣天使之剑", "type": WardrobeTransmogSystem.TYPE_WEAPON} - - var prev = sys.preview_transmog(sword, skin) - _assert(prev["ok"] == true, "Preview generated") - _assert(prev["preview_vnum"] == 199, "Preview skin vnum is 199") - _assert(prev["preview_name"] == "圣天使之剑", "Preview name matches") - -func _test_weapon_transmutation() -> void: - print("\n--- Test 2: Weapon Transmutation & Full Stat Preservation ---") - var sys := WardrobeTransmogSystem.new() - var player := {"gold": 25000000} - var inv: Array = [ - {"vnum": WardrobeTransmogSystem.VNUM_TRANSMOG_SCROLL, "name": "幻化秘籍", "count": 2}, - { - "vnum": 189, - "name": "毒剑+9", - "type": WardrobeTransmogSystem.TYPE_WEAPON, - "refine_level": 9, - "sockets": [28430, 28435, 28436], - "attrs": [{"type": 1, "val": 2000}] - }, - {"vnum": 199, "name": "圣天使之剑", "type": WardrobeTransmogSystem.TYPE_WEAPON, "count": 1} - ] - - var transmog_sig := {"fired": false, "slot": -1, "skin_vnum": 0} - sys.item_transmuted.connect(func(s: int, _tname: String, sk_vnum: int, _sk_name: String): - transmog_sig["fired"] = true - transmog_sig["slot"] = s - transmog_sig["skin_vnum"] = sk_vnum - ) - - # Apply transmutation - var res = sys.apply_transmutation(0, 1, 2, inv, player) - _assert(res["ok"] == true, "Transmutation succeeded") - _assert(player["gold"] == 15000000, "10M gold deducted (25M -> 15M)") - _assert(inv[0]["count"] == 1, "Scroll count decreased (2 -> 1)") - _assert(inv[2] == null, "Skin material item consumed") - - # Check target item preservation - var target = inv[1] - _assert(target["vnum"] == 189, "Base vnum is still 189 (Poison Sword)") - _assert(target["refine_level"] == 9, "Refine level preserved as +9") - _assert(target["sockets"].size() == 3, "Sockets preserved") - _assert(target["attrs"].size() == 1, "Attrs preserved") - _assert(target["transmog_vnum"] == 199, "transmog_vnum set to 199") - _assert(target["transmog_name"] == "圣天使之剑", "transmog_name set to 圣天使之剑") - _assert(transmog_sig["fired"] == true and transmog_sig["skin_vnum"] == 199, "item_transmuted emitted") - -func _test_type_mismatch_rejection() -> void: - print("\n--- Test 3: Type Mismatch Rejection (Weapon vs Armor) ---") - var sys := WardrobeTransmogSystem.new() - var player := {"gold": 50000000} - var inv: Array = [ - {"vnum": WardrobeTransmogSystem.VNUM_TRANSMOG_SCROLL, "count": 1}, - {"vnum": 189, "name": "毒剑+9", "type": WardrobeTransmogSystem.TYPE_WEAPON}, - {"vnum": 11299, "name": "黑铁战甲+9", "type": WardrobeTransmogSystem.TYPE_ARMOR} # Armor into weapon - ] - - var fail_mismatch = sys.apply_transmutation(0, 1, 2, inv, player) - _assert(fail_mismatch["ok"] == false and fail_mismatch["reason"] == "TYPE_MISMATCH", "Rejected armor as skin for weapon") - _assert(player["gold"] == 50000000, "Gold untouched") - _assert(inv[0]["count"] == 1, "Scroll untouched") - -func _test_transmutation_revert() -> void: - print("\n--- Test 4: Reverting Transmutation (72302) ---") - var sys := WardrobeTransmogSystem.new() - var inv: Array = [ - {"vnum": WardrobeTransmogSystem.VNUM_REVERT_SCROLL, "name": "还原秘籍", "count": 1}, - { - "vnum": 189, - "name": "毒剑+9", - "type": WardrobeTransmogSystem.TYPE_WEAPON, - "transmog_vnum": 199, - "transmog_name": "圣天使之剑" - } - ] - - var revert_sig := {"fired": false, "slot": -1} - sys.transmutation_reverted.connect(func(s: int, _n: String): - revert_sig["fired"] = true - revert_sig["slot"] = s - ) - - var res = sys.revert_transmutation(0, 1, inv) - _assert(res["ok"] == true, "Revert succeeded") - _assert(inv[0] == null, "Revert scroll consumed") - _assert(inv[1].has("transmog_vnum") == false, "transmog_vnum cleared") - _assert(revert_sig["fired"] == true and revert_sig["slot"] == 1, "transmutation_reverted emitted") - - # Second revert on un-transmuted item must fail - inv[0] = {"vnum": WardrobeTransmogSystem.VNUM_REVERT_SCROLL, "count": 1} - var fail_again = sys.revert_transmutation(0, 1, inv) - _assert(fail_again["ok"] == false and fail_again["reason"] == "NOT_TRANSMUTED", "Cannot revert un-transmuted item") - -func _finish() -> void: - print("\n==========================================") - print("Wardrobe Transmog Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Wardrobe Transmog parity tests passed!") - quit(0) diff --git a/project/test_wardrobe_transmog_parity.gd.uid b/project/test_wardrobe_transmog_parity.gd.uid deleted file mode 100644 index 70729b49..00000000 --- a/project/test_wardrobe_transmog_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://3o28mlag87c3 diff --git a/project/test_weather_daynight_parity.gd b/project/test_weather_daynight_parity.gd deleted file mode 100644 index 57e68670..00000000 --- a/project/test_weather_daynight_parity.gd +++ /dev/null @@ -1,126 +0,0 @@ -# test_weather_daynight_parity.gd —— 40250 昼夜交替与地图天候环境 1:1 回归测试套件 -extends SceneTree - -const WeatherDayNightSystem = preload("res://weather_daynight_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_weather_daynight_parity (Direction 4: Day/Night & Weather 1:1) ===") - _test_default_state() - _test_phase_transitions() - _test_time_progression_and_wrap() - _test_weather_modes_and_maps() - _finish() - -func _test_default_state() -> void: - print("\n--- Test 1: Default Noon State & Formatted Time ---") - var w := WeatherDayNightSystem.new() - _assert(w.current_phase == WeatherDayNightSystem.PHASE_NOON, "Defaults to NOON") - _assert(w.current_weather == WeatherDayNightSystem.WEATHER_CLEAR, "Defaults to CLEAR weather") - _assert(w.get_time_string() == "12:00:00", "Default time is 12:00:00") - _assert(w.sun_energy == 1.2, "Noon sun energy is 1.2") - -func _test_phase_transitions() -> void: - print("\n--- Test 2: Phase Transitions (Dawn, Noon, Dusk, Night) ---") - var w := WeatherDayNightSystem.new() - - var phase_sig := {"fired": false, "old": "", "new": ""} - w.phase_changed.connect(func(o: String, n: String): - phase_sig["fired"] = true - phase_sig["old"] = o - phase_sig["new"] = n - ) - - # Transition to DAWN (06:00) - w.set_phase(WeatherDayNightSystem.PHASE_DAWN) - _assert(w.current_phase == WeatherDayNightSystem.PHASE_DAWN, "Phase is DAWN") - _assert(phase_sig["fired"] == true and phase_sig["new"] == "dawn", "phase_changed emitted dawn") - _assert(w.sun_color.r > 0.9 and w.sun_color.g > 0.7, "Dawn has warm golden light") - - # Transition to DUSK (18:30) - phase_sig["fired"] = false - w.set_phase(WeatherDayNightSystem.PHASE_DUSK) - _assert(w.current_phase == WeatherDayNightSystem.PHASE_DUSK, "Phase is DUSK") - _assert(phase_sig["fired"] == true and phase_sig["new"] == "dusk", "phase_changed emitted dusk") - - # Transition to NIGHT (23:00) - phase_sig["fired"] = false - w.set_phase(WeatherDayNightSystem.PHASE_NIGHT) - _assert(w.current_phase == WeatherDayNightSystem.PHASE_NIGHT, "Phase is NIGHT") - _assert(phase_sig["fired"] == true and phase_sig["new"] == "night", "phase_changed emitted night") - _assert(w.sun_energy <= 0.3, "Night has low moon energy") - -func _test_time_progression_and_wrap() -> void: - print("\n--- Test 3: Time Flow, Freezing, and Midnight Wrap ---") - var w := WeatherDayNightSystem.new() - w.set_time_of_day(12.0, 0.0) # 12:00:00 - w.time_scale = 60.0 # 1 real sec = 60 game sec - - # Advance 1 second -> should become 12:01:00 - w.update(1.0) - _assert(w.get_time_string() == "12:01:00", "Advanced 1 minute of game time") - - # Freeze time - w.is_frozen = true - w.update(5.0) - _assert(w.get_time_string() == "12:01:00", "Time remains frozen") - - # Midnight wrap test: 23:59:30 + 60s -> 00:00:30 - w.is_frozen = false - w.time_scale = 1.0 - w.set_time_of_day(23.0, 59.0) - w.time_of_day += 30.0 # 23:59:30 - w.update(60.0) # +60s - _assert(w.time_of_day < 3600.0, "Wrapped cleanly past midnight") - _assert(w.current_phase == WeatherDayNightSystem.PHASE_NIGHT, "Still NIGHT after midnight wrap") - -func _test_weather_modes_and_maps() -> void: - print("\n--- Test 4: Map Weather Presets & Environment Atmospheres ---") - var w := WeatherDayNightSystem.new() - - var weather_sig := {"fired": false, "old": "", "new": ""} - w.weather_changed.connect(func(o: String, n: String): - weather_sig["fired"] = true - weather_sig["old"] = o - weather_sig["new"] = n - ) - - # Switch map to Mount Sohan (Ice Mountain) -> Snow - w.set_map_environment("sohan") - _assert(w.current_weather == WeatherDayNightSystem.WEATHER_SNOW, "Mount Sohan auto-activates SNOW") - _assert(weather_sig["fired"] == true and weather_sig["new"] == "snow", "weather_changed emitted snow") - - # Switch map to Yongbi Desert -> Sandstorm - w.set_map_environment("desert") - _assert(w.current_weather == WeatherDayNightSystem.WEATHER_SANDSTORM, "Desert auto-activates SANDSTORM") - _assert(w.fog_density >= 0.005, "Sandstorm substantially raises fog density") - - # Switch map to Doyyumhwan (Flame field) -> Cinders - w.set_map_environment("flame") - _assert(w.current_weather == WeatherDayNightSystem.WEATHER_CINDERS, "Flame field auto-activates CINDERS") - - # Switch back to Village -> Clear - w.set_map_environment("village") - _assert(w.current_weather == WeatherDayNightSystem.WEATHER_CLEAR, "Village reverts to CLEAR sky") - -func _finish() -> void: - print("\n==========================================") - print("Weather Day/Night Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All Weather Day/Night parity tests passed!") - quit(0) diff --git a/project/test_weather_daynight_parity.gd.uid b/project/test_weather_daynight_parity.gd.uid deleted file mode 100644 index b3fccf7c..00000000 --- a/project/test_weather_daynight_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cdqct6gwv62a6 diff --git a/project/test_wedding_marriage_parity.gd b/project/test_wedding_marriage_parity.gd deleted file mode 100644 index 605672fc..00000000 --- a/project/test_wedding_marriage_parity.gd +++ /dev/null @@ -1,163 +0,0 @@ -# test_wedding_marriage_parity.gd —— 40250 官方结婚典礼与夫妻同心婚戒系统 1:1 回归测试套件 -extends SceneTree - -const WeddingMarriageSystem = preload("res://wedding_marriage_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_wedding_marriage_parity (Direction 3: Wedding & Marriage 1:1) ===") - _test_wedding_ceremony_and_ring_issuance() - _test_wedding_ring_teleport() - _test_love_points_and_marriage_buffs() - _test_divorce_and_ring_clearing() - _test_serialization() - _finish() - -func _test_wedding_ceremony_and_ring_issuance() -> void: - print("\n--- Test 1: Wedding Ceremony & Wedding Ring Issuance ---") - var sys := WeddingMarriageSystem.new() - _assert(not sys.is_married, "Initially unmarried") - - var inv: Array = [ - {"vnum": WeddingMarriageSystem.VNUM_ENGAGEMENT_RING, "name": "订婚戒指"} - ] - - # 1. Missing wedding dress - var res_no_dress = sys.conduct_wedding("月神姬", inv, "M") - _assert(res_no_dress["ok"] == false, "Wedding rejected without Tuxedo/Dress") - _assert(res_no_dress["reason"] == "NO_WEDDING_DRESS", "Reason is NO_WEDDING_DRESS") - - # Add Tuxedo - inv.append({"vnum": WeddingMarriageSystem.VNUM_TUXEDO, "name": "新郎礼服"}) - - var wed_sig := {"fired": false, "spouse": ""} - sys.marriage_completed.connect(func(s: String): - wed_sig["fired"] = true - wed_sig["spouse"] = s - ) - - # 2. Successful wedding - var res_wed = sys.conduct_wedding("月神姬", inv, "M") - _assert(res_wed["ok"] == true, "Wedding ceremony succeeded") - _assert(sys.is_married == true, "is_married is true") - _assert(sys.spouse_name == "月神姬", "Spouse name is 月神姬") - _assert(inv[0]["vnum"] == WeddingMarriageSystem.VNUM_WEDDING_RING, "Slot 0 replaced with Wedding Ring (70302)") - _assert(wed_sig["fired"] == true, "marriage_completed signal emitted") - _assert(wed_sig["spouse"] == "月神姬", "Signal reported spouse name") - -func _test_wedding_ring_teleport() -> void: - print("\n--- Test 2: Wedding Ring Spouse Teleport ---") - var sys := WeddingMarriageSystem.new() - sys.is_married = true - sys.spouse_name = "月神姬" - - var warp_sig := {"fired": false, "map": "", "pos": Vector2.ZERO} - sys.spouse_warped.connect(func(m: String, p: Vector2): - warp_sig["fired"] = true - warp_sig["map"] = m - warp_sig["pos"] = p - ) - - # 1. Spouse is offline - var res_offline = sys.use_wedding_ring(false, "Desert", Vector2(100, 200), "Yongan") - _assert(res_offline["ok"] == false, "Warp rejected: spouse offline") - _assert(res_offline["reason"] == "SPOUSE_OFFLINE", "Reason is SPOUSE_OFFLINE") - - # 2. Forbidden map (Arena) - var res_arena = sys.use_wedding_ring(true, "Arena", Vector2(100, 200), "Yongan") - _assert(res_arena["ok"] == false, "Warp rejected in Arena") - _assert(res_arena["reason"] == "FORBIDDEN_MAP", "Reason is FORBIDDEN_MAP") - - # 3. Successful warp - var res_succ = sys.use_wedding_ring(true, "Desert", Vector2(450, 780), "Yongan") - _assert(res_succ["ok"] == true, "Warp succeeded") - _assert(warp_sig["fired"] == true, "spouse_warped signal emitted") - _assert(warp_sig["map"] == "Desert", "Target map is Desert") - _assert(warp_sig["pos"] == Vector2(450, 780), "Target pos matches spouse") - -func _test_love_points_and_marriage_buffs() -> void: - print("\n--- Test 3: Love Points & Marriage Aura Buffs ---") - var sys := WeddingMarriageSystem.new() - sys.is_married = true - sys.love_points = 50 - - var love_sig := {"fired": false, "pts": 0} - sys.love_points_updated.connect(func(p: int): - love_sig["fired"] = true - love_sig["pts"] = p - ) - - sys.add_love_points(30) # 50 -> 80 - _assert(sys.love_points == 80, "Love points increased to 80") - _assert(love_sig["fired"] == true, "love_points_updated signal emitted") - - # Buff check when NOT near spouse - var buff_far = sys.calculate_marriage_buff(true, true, false) - _assert(buff_far.is_empty(), "No marriage buffs when distant from spouse") - - # Buff check when near spouse: base (10 + 8) = 18% exp - # + Ring of Love (+20% exp) = 38% - # + Bracelet of Love (+10% crit) - var buff_near = sys.calculate_marriage_buff(true, true, true) - _assert(buff_near["couple_exp_bonus_pct"] == 38, "Couple exp bonus is 38% (18% + 20%)") - _assert(buff_near["crit_pct"] == 10, "Crit bonus is 10%") - -func _test_divorce_and_ring_clearing() -> void: - print("\n--- Test 4: Divorce & Wedding Ring Clearing ---") - var sys := WeddingMarriageSystem.new() - sys.is_married = true - sys.spouse_name = "月神姬" - sys.love_points = 80 - - var inv: Array = [ - {"vnum": WeddingMarriageSystem.VNUM_WEDDING_RING, "name": "结婚戒指"} - ] - - var div_sig := {"fired": false} - sys.divorced.connect(func(): - div_sig["fired"] = true - ) - - var res_div = sys.divorce(inv) - _assert(res_div["ok"] == true, "Divorce succeeded") - _assert(sys.is_married == false, "is_married is now false") - _assert(sys.spouse_name.is_empty(), "Spouse name cleared") - _assert(sys.love_points == 0, "Love points cleared to 0") - _assert(inv[0] == null, "Wedding ring revoked and slot cleared") - _assert(div_sig["fired"] == true, "divorced signal emitted") - -func _test_serialization() -> void: - print("\n--- Test 5: Serialization & Restoration ---") - var sys1 := WeddingMarriageSystem.new() - sys1.is_married = true - sys1.spouse_name = "雪月花" - sys1.love_points = 95 - sys1.marry_timestamp = 1600000000 - - var data = sys1.serialize() - var sys2 := WeddingMarriageSystem.new() - sys2.deserialize(data) - - _assert(sys2.is_married == true, "Restored is_married") - _assert(sys2.spouse_name == "雪月花", "Restored spouse name") - _assert(sys2.love_points == 95, "Restored love points") - _assert(sys2.marry_timestamp == 1600000000, "Restored marry timestamp") - -func _finish() -> void: - print("\n=======================================================") - print("Wedding Marriage Parity Tests: %d Passed, %d Failed" % [_passed, _failed]) - print("=======================================================\n") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_wedding_marriage_parity.gd.uid b/project/test_wedding_marriage_parity.gd.uid deleted file mode 100644 index e49f3ebe..00000000 --- a/project/test_wedding_marriage_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c6i6w1uqdvn3n diff --git a/project/test_world_boss_ai_behavior_parity.gd b/project/test_world_boss_ai_behavior_parity.gd deleted file mode 100644 index 353c3bfb..00000000 --- a/project/test_world_boss_ai_behavior_parity.gd +++ /dev/null @@ -1,177 +0,0 @@ -# test_world_boss_ai_behavior_parity.gd —— 40250 世界首领动态 AI 行为状态机 1:1 回归测试套件 -extends SceneTree - -const WorldBossAISystem = preload("res://world_boss_ai_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_world_boss_ai_behavior_parity (Direction 1: World Boss AI 1:1) ===") - _test_boss_initialization() - _test_aggro_and_chase() - _test_skills_and_attacks() - _test_enrage_mechanic() - _test_leash_and_reset() - _test_death() - _finish() - -func _test_boss_initialization() -> void: - print("\n--- Test 1: Boss Initialization & Table Config ---") - var ai := WorldBossAISystem.new() - var fail = ai.init_boss(99999) - _assert(fail["ok"] == false and fail["reason"] == "INVALID_BOSS_VNUM", "Invalid boss vnum rejected") - - var vnums = [1901, 1304, 2191, 2206] - for v in vnums: - var init_res = ai.init_boss(v, Vector3(100, 0, 100)) - _assert(init_res["ok"] == true, "Boss %d initialized" % v) - _assert(ai.current_state == WorldBossAISystem.STATE_IDLE, "Starts in IDLE state") - _assert(ai.current_hp == ai.max_hp, "HP at max") - -func _test_aggro_and_chase() -> void: - print("\n--- Test 2: Aggro Acquisition & Chase Mechanics ---") - var ai := WorldBossAISystem.new() - ai.init_boss(1901, Vector3.ZERO) - - var aggro_sig := {"fired": false, "id": "", "dist": 0.0} - ai.target_acquired.connect(func(tid: String, d: float): - aggro_sig["fired"] = true - aggro_sig["id"] = tid - aggro_sig["dist"] = d - ) - - # Target 25m away (beyond 18m AGGRO_RADIUS) - var step1 = ai.update_ai(0.1, Vector3(25, 0, 0), "player", true) - _assert(aggro_sig["fired"] == false, "No aggro at 25m") - _assert(step1["action"] == "wandering", "Wandering when target is far") - - # Target enters 15m (within 18m AGGRO_RADIUS) - var step2 = ai.update_ai(0.1, Vector3(15, 0, 0), "player", true) - _assert(aggro_sig["fired"] == true and aggro_sig["id"] == "player", "Target acquired signal fired at 15m") - _assert(step2["state"] == WorldBossAISystem.STATE_CHASE, "Transitions to CHASE state") - _assert(ai.current_position.x > 0.0, "Boss stepped forward towards player") - -func _test_skills_and_attacks() -> void: - print("\n--- Test 3: Skill Casting & Basic Attacks ---") - var ai := WorldBossAISystem.new() - ai.init_boss(1901, Vector3.ZERO) # Nine tails: Atk 850, Def 450, Skill: 暴风雪呼啸, mult 1.8 - - var skill_sig := {"fired": false, "name": "", "dmg": 0} - ai.skill_cast.connect(func(sname: String, dmg: int, _aoe: float): - skill_sig["fired"] = true - skill_sig["name"] = sname - skill_sig["dmg"] = dmg - ) - - # Target at 5.0m (within 9m SKILL_RANGE, but beyond 3.5m ATTACK_RANGE) - var skill_act = ai.update_ai(0.1, Vector3(5, 0, 0), "player", true) - _assert(skill_act["state"] == WorldBossAISystem.STATE_SKILL, "Casts skill when in skill range") - _assert(skill_sig["fired"] == true and skill_sig["name"] == "暴风雪呼啸", "Skill signal emitted with correct name") - _assert(skill_sig["dmg"] == int(850 * 1.8), "Skill damage is base_atk * 1.8") - - # Target closes to 2.0m (within 3.5m ATTACK_RANGE) - # Skill is now on cooldown, should perform basic attack - var atk_act = ai.update_ai(0.1, Vector3(2, 0, 0), "player", true) - _assert(atk_act["state"] == WorldBossAISystem.STATE_ATTACK, "Executes basic attack within melee range") - _assert(atk_act["damage"] == 850, "Basic attack damage matches base_atk") - -func _test_enrage_mechanic() -> void: - print("\n--- Test 4: Enrage Mechanic (< 30% HP) ---") - var ai := WorldBossAISystem.new() - ai.init_boss(1304, Vector3.ZERO) # 1304: HP 320000, Def 480, Atk 920 - - var enrage_sig := {"fired": false, "mult": 0.0} - ai.boss_enraged.connect(func(m: float): - enrage_sig["fired"] = true - enrage_sig["mult"] = m - ) - - # Deal damage down to ~40% HP (no enrage) - # Max HP = 320000. 40% = 128000. Damage needed = 192000 net dmg. - # Raw dmg = 192000 + 480 = 192480. - var hit1 = ai.receive_damage(192480, "player") - _assert(hit1["is_enraged"] == false, "Not enraged at 40% HP") - _assert(enrage_sig["fired"] == false, "boss_enraged signal not yet fired") - - # Deal damage down to 25% HP (< 30% HP trigger) - # Need 50000 more net dmg. - var hit2 = ai.receive_damage(50000 + 480, "player") - _assert(hit2["is_enraged"] == true, "Enraged triggered at <= 30% HP") - _assert(enrage_sig["fired"] == true and enrage_sig["mult"] == 1.5, "boss_enraged signal emitted with 1.5x") - - # Attack while enraged has 1.5x damage - # 1. Enraged skill cast - var enraged_skill = ai.update_ai(0.1, Vector3(5, 0, 0), "player", true) - _assert(enraged_skill["state"] == WorldBossAISystem.STATE_SKILL, "Enraged skill cast triggered") - _assert(enraged_skill["damage"] == int(920 * 2.0 * 1.5), "Enraged skill gets 1.5x damage multiplier") - - # 2. Enraged basic attack (skill is now on cooldown) - var enraged_atk = ai.update_ai(0.1, Vector3(2, 0, 0), "player", true) - _assert(enraged_atk["state"] == WorldBossAISystem.STATE_ATTACK, "Enraged basic attack triggered") - _assert(enraged_atk["damage"] == int(920 * 1.5), "Enraged basic attack gets 1.5x damage") - -func _test_leash_and_reset() -> void: - print("\n--- Test 5: Leash Distance & Full Reset ---") - var ai := WorldBossAISystem.new() - ai.init_boss(2191, Vector3.ZERO) # Spawn at (0, 0, 0) - - var leash_start := {"fired": false} - var leash_done := {"fired": false} - ai.leash_reset_started.connect(func(): leash_start["fired"] = true) - ai.leash_reset_completed.connect(func(): leash_done["fired"] = true) - - # Boss takes damage - ai.receive_damage(100000, "player") - _assert(ai.current_hp < ai.max_hp, "Boss damaged") - - # Move boss far beyond 35m LEASH_DISTANCE - ai.current_position = Vector3(40, 0, 0) - var leash_act = ai.update_ai(0.1, Vector3(45, 0, 0), "player", true) - _assert(leash_act["state"] == WorldBossAISystem.STATE_LEASH_RETURN, "Enters LEASH_RETURN state") - _assert(leash_start["fired"] == true, "leash_reset_started signal fired") - - # In leash return, attacks are immune - var immune_hit = ai.receive_damage(50000, "player") - _assert(immune_hit.get("immune", false) == true, "Attacks during leash return are IMMUNE") - - # Simulate returning to spawn point - while ai.current_state == WorldBossAISystem.STATE_LEASH_RETURN: - ai.update_ai(0.5, Vector3(45, 0, 0), "player", true) - - _assert(leash_done["fired"] == true, "leash_reset_completed signal fired") - _assert(ai.current_state == WorldBossAISystem.STATE_IDLE, "Boss back in IDLE") - _assert(ai.current_hp == ai.max_hp, "Boss HP fully restored to 100%") - _assert(ai.current_position.distance_to(Vector3.ZERO) < 0.1, "Boss back at spawn point") - -func _test_death() -> void: - print("\n--- Test 6: Death State ---") - var ai := WorldBossAISystem.new() - ai.init_boss(2206, Vector3.ZERO) - - var fatal = ai.receive_damage(10000000, "player") - _assert(fatal["dead"] == true, "Fatal hit flags dead == true") - _assert(ai.current_state == WorldBossAISystem.STATE_DEAD, "State becomes DEAD") - - var after_death = ai.update_ai(0.1, Vector3.ZERO, "player", true) - _assert(after_death["state"] == WorldBossAISystem.STATE_DEAD and after_death["action"] == "none", "Dead boss does nothing") - -func _finish() -> void: - print("\n==========================================") - print("World Boss AI Parity Tests Finished:") - print("Passed: %d, Failed: %d" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - printerr("FAILED: Some tests did not pass.") - quit(1) - else: - print("SUCCESS: All World Boss AI parity tests passed!") - quit(0) diff --git a/project/test_world_boss_ai_behavior_parity.gd.uid b/project/test_world_boss_ai_behavior_parity.gd.uid deleted file mode 100644 index 4a490bd0..00000000 --- a/project/test_world_boss_ai_behavior_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://duopqhpsqmi1l diff --git a/project/test_world_boss_parity.gd b/project/test_world_boss_parity.gd deleted file mode 100644 index eaf028b6..00000000 --- a/project/test_world_boss_parity.gd +++ /dev/null @@ -1,204 +0,0 @@ -# test_world_boss_parity.gd —— 40250 野外四大世界 Boss 1:1 纯净回归测试套件 -extends SceneTree - -const WorldBossSystem = preload("res://world_boss_system.gd") - -var _passed := 0 -var _failed := 0 - -func _assert(cond: bool, msg: String) -> void: - if cond: - _passed += 1 - print(" [PASS] %s" % msg) - else: - _failed += 1 - printerr(" [FAIL] %s" % msg) - -func _init() -> void: - print("\n=== Running test_world_boss_parity (Direction 3: World Bosses & Chests 1:1) ===") - _test_boss_table_and_spawn() - _test_damage_mitigation_and_phase_minions() - _test_enrage_mechanic() - _test_boss_kill_and_drops() - _test_boss_chest_opening() - _finish() - -func _test_boss_table_and_spawn() -> void: - print("\n--- Test 1: Boss Config Table & Spawning ---") - var wbs := WorldBossSystem.new() - - # Invalid boss vnum - var invalid_res = wbs.spawn_boss(99999) - _assert(invalid_res["ok"] == false and invalid_res["reason"] == "INVALID_BOSS_VNUM", "Invalid boss vnum rejected") - - # Check table contains the 4 iconic world bosses - _assert(WorldBossSystem.BOSS_TABLE.has(1901), "Nine Tails (1901) defined") - _assert(WorldBossSystem.BOSS_TABLE.has(2191), "Desert Tortoise (2191) defined") - _assert(WorldBossSystem.BOSS_TABLE.has(1304), "Yellow Tiger (1304) defined") - _assert(WorldBossSystem.BOSS_TABLE.has(2206), "Flame King (2206) defined") - - # Spawn Nine Tails (1901) - var spawn_data := {"fired": false, "name": "", "vnum": 0} - wbs.boss_spawned.connect(func(vnum: int, bname: String, _pos: Vector3): - spawn_data["fired"] = true - spawn_data["name"] = bname - spawn_data["vnum"] = vnum - ) - - var spawn_pos := Vector3(50.0, 0.0, -120.0) - var res = wbs.spawn_boss(1901, spawn_pos) - _assert(res["ok"] == true, "Nine Tails spawned successfully") - _assert(wbs.is_boss_alive == true, "Boss is alive") - _assert(wbs.active_boss["max_hp"] == 280000, "Nine Tails max HP is 280,000") - _assert(wbs.active_boss["base_atk"] == 850, "Nine Tails base ATK is 850") - _assert(wbs.active_boss["base_def"] == 450, "Nine Tails base DEF is 450") - _assert(spawn_data["fired"] == true, "boss_spawned signal emitted") - _assert(spawn_data["name"] == "九尾妖狐", "Boss name matches") - -func _test_damage_mitigation_and_phase_minions() -> void: - print("\n--- Test 2: DEF Mitigation & Phase Minion Spawns (75%, 50%, 25%) ---") - var wbs := WorldBossSystem.new() - wbs.spawn_boss(1901) # HP: 280000, DEF: 450 - - var minion_events := {"count": 0, "last_minion_vnum": 0, "last_count": 0} - wbs.minions_summoned.connect(func(_bvnum: int, mvnum: int, count: int): - minion_events["count"] += 1 - minion_events["last_minion_vnum"] = mvnum - minion_events["last_count"] = count - ) - - # 1. Normal damage without crossing threshold: Raw 1450 - DEF 450 = 1000 dmg - var res1 = wbs.apply_damage(1450) - _assert(res1["ok"] == true, "Damage applied successfully") - _assert(res1["net_dmg"] == 1000, "1450 raw dmg - 450 def = 1000 net dmg") - _assert(wbs.active_boss["current_hp"] == 279000, "HP updated to 279,000") - _assert(minion_events["count"] == 0, "No minions summoned yet") - - # 2. Damage boss to trigger 75% threshold (280000 * 0.75 = 210000 HP threshold) - # Need 69,000 net damage -> raw damage 69,450 - var res2 = wbs.apply_damage(69450) - _assert(wbs.active_boss["current_hp"] == 210000, "Boss at exactly 75% HP (210,000)") - _assert(res2["triggered_events"].has("PHASE_75_MINIONS"), "PHASE_75_MINIONS triggered") - _assert(minion_events["count"] == 1, "Minion summon signal fired 1 time") - _assert(minion_events["last_minion_vnum"] == 2204, "Summoned Ice Yeti minions (vnum 2204)") - _assert(minion_events["last_count"] == 4, "Summoned 4 minions") - - # Additional attack should not re-trigger 75% - var res3 = wbs.apply_damage(1450) - _assert(not res3["triggered_events"].has("PHASE_75_MINIONS"), "75% phase does not retrigger") - - # 3. Damage boss to cross 50% threshold (140,000 HP) - # Current HP is 209,000. Need 69,000 net damage -> raw 69,450 - var res4 = wbs.apply_damage(69450) - _assert(wbs.active_boss["current_hp"] == 140000, "Boss at 50% HP (140,000)") - _assert(res4["triggered_events"].has("PHASE_50_MINIONS"), "PHASE_50_MINIONS triggered") - _assert(minion_events["count"] == 2, "Minion summon count now 2") - -func _test_enrage_mechanic() -> void: - print("\n--- Test 3: Enrage Mechanic at <= 30% HP (+50% ATK) ---") - var wbs := WorldBossSystem.new() - wbs.spawn_boss(2206) # Flame King: HP 400000, Base ATK 1200, Base DEF 500 - - var enrage_data := {"fired": false, "boss_vnum": 0} - wbs.boss_enraged.connect(func(bvnum: int): - enrage_data["fired"] = true - enrage_data["boss_vnum"] = bvnum - ) - - _assert(wbs.active_boss["is_enraged"] == false, "Boss initially not enraged") - _assert(wbs.active_boss["current_atk"] == 1200, "Base ATK is 1200") - - # 30% of 400,000 = 120,000 HP. - # Deal 280,000 net damage -> raw damage 280,500 - var res = wbs.apply_damage(280500) - _assert(wbs.active_boss["current_hp"] == 120000, "Boss HP reached 120,000 (30%)") - _assert(res["triggered_events"].has("BOSS_ENRAGED"), "BOSS_ENRAGED triggered in events") - _assert(enrage_data["fired"] == true, "boss_enraged signal fired") - _assert(enrage_data["boss_vnum"] == 2206, "Enraged boss vnum matches Flame King (2206)") - _assert(wbs.active_boss["is_enraged"] == true, "is_enraged is now true") - _assert(wbs.active_boss["current_atk"] == 1800, "ATK increased by 50% to 1800") - -func _test_boss_kill_and_drops() -> void: - print("\n--- Test 4: Boss Kill & Guaranteed Chest Drops ---") - var wbs := WorldBossSystem.new() - wbs.spawn_boss(1304) # Yellow Tiger: HP 350000, DEF 480, Chest: 50008 - - var kill_data := {"fired": false, "drops": []} - wbs.boss_killed.connect(func(_bvnum: int, _pos: Vector3, drops: Array): - kill_data["fired"] = true - kill_data["drops"] = drops - ) - - # Deal lethal damage - var kill_res = wbs.apply_damage(500000) - _assert(kill_res["ok"] == true and kill_res["killed"] == true, "Boss defeated") - _assert(wbs.is_boss_alive == false, "Boss is dead") - _assert(kill_data["fired"] == true, "boss_killed signal fired") - - var drops: Array = kill_res["drops"] - _assert(drops.size() >= 4, "Drops include chest, soul stone, scroll, and gold") - - # Verify guaranteed chest (50008) - var has_chest := false - var has_soul_stone := false - var has_scroll := false - var has_gold := false - - for d in drops: - if d["vnum"] == 50008: - has_chest = true - elif d["vnum"] == 50513: - has_soul_stone = true - elif d["vnum"] == 25040: - has_scroll = true - elif d["vnum"] == 1 and d["count"] >= 70000: - has_gold = true - - _assert(has_chest, "Guaranteed Yellow Tiger Chest (50008) dropped") - _assert(has_soul_stone, "Guaranteed Soul Stone (50513) dropped") - _assert(has_scroll, "Guaranteed Blessing Scroll (25040) dropped") - _assert(has_gold, "Gold dropped within range (>= 70,000)") - -func _test_boss_chest_opening() -> void: - print("\n--- Test 5: World Boss Chest Opening (40250 Loot Pool) ---") - var wbs := WorldBossSystem.new() - var inv: Array = [ - {"vnum": 50006, "name": "九尾妖狐宝箱", "count": 1}, - null, - null - ] - - # Invalid slot - var invalid_res = wbs.open_boss_chest(99, inv) - _assert(invalid_res["ok"] == false and invalid_res["reason"] == "INVALID_SLOT", "Invalid slot rejected") - - # Empty slot - var empty_res = wbs.open_boss_chest(1, inv) - _assert(empty_res["ok"] == false and empty_res["reason"] == "EMPTY_SLOT", "Empty slot rejected") - - # Non-chest item - inv[1] = {"vnum": 10, "name": "木剑", "count": 1} - var non_chest_res = wbs.open_boss_chest(1, inv) - _assert(non_chest_res["ok"] == false and non_chest_res["reason"] == "NOT_A_BOSS_CHEST", "Non-chest item rejected") - - # Open Nine Tails Chest (50006) at slot 0 - var res = wbs.open_boss_chest(0, inv) - _assert(res["ok"] == true, "Chest opened successfully") - - var reward = res["reward"] - _assert(reward.has("vnum") and reward.has("name"), "Valid reward generated") - print(" Received chest reward: %s (vnum: %d, count: %d)" % [reward["name"], reward["vnum"], reward.get("count", 1)]) - - var stored_slot = res["stored_slot"] - _assert(stored_slot >= 0 and inv[stored_slot] != null, "Reward saved in inventory") - _assert(inv[stored_slot]["vnum"] == reward["vnum"], "Inventory item matches rewarded vnum") - _assert(inv[stored_slot]["vnum"] != 50006, "Chest (50006) was consumed and replaced") - -func _finish() -> void: - print("\n==========================================") - print("World Boss Parity Test Results: %d Passed, %d Failed" % [_passed, _failed]) - print("==========================================") - if _failed > 0: - quit(1) - else: - quit(0) diff --git a/project/test_world_boss_parity.gd.uid b/project/test_world_boss_parity.gd.uid deleted file mode 100644 index 7fef651f..00000000 --- a/project/test_world_boss_parity.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://3m5i6jxicjrd diff --git a/project/title_achievement_system.gd b/project/title_achievement_system.gd deleted file mode 100644 index d8633a2b..00000000 --- a/project/title_achievement_system.gd +++ /dev/null @@ -1,130 +0,0 @@ -# title_achievement_system.gd —— Metin2 40250 殿堂级头衔与成就系统 1:1 -# 对照 40250 服务端 char.cpp, questmanager.cpp, locale_string.txt -class_name TitleAchievementSystem -extends RefCounted - -signal achievement_unlocked(ach_id: String, ach_name: String, title: String) -signal title_equipped(title_name: String, bonuses: Dictionary) -signal title_unequipped() - -const ACHIEVEMENTS: Dictionary = { - "ACH_DEMON_TOWER": { - "name": "恶魔之塔登顶", - "desc": "突破恶魔之塔 9 层并斩杀死神", - "title": "恶魔征服者", - "points": 50, - "bonuses": {"max_hp": 500} - }, - "ACH_SPIDER_BARONESS": { - "name": "蛛皇陨落", - "desc": "在蜘蛛洞穴三层巢穴击败蜘蛛男爵夫人", - "title": "蛛巢克星", - "points": 50, - "bonuses": {"monster_dmg_pct": 5} - }, - "ACH_BERAN_SETAOU": { - "name": "冰龙之诛", - "desc": "击破四大魔石并消灭冰龙贝兰-塞陶", - "title": "屠龙勇士", - "points": 100, - "bonuses": {"boss_dmg_pct": 10} - }, - "ACH_AZRAEL": { - "name": "冥王末日", - "desc": "穿过地下墓穴七层击破冥王阿兹瑞尔", - "title": "深渊征服者", - "points": 100, - "bonuses": {"def": 50} - }, - "ACH_FOUR_WORLD_BOSSES": { - "name": "四方平定", - "desc": "击杀野外九尾妖狐、沙漠龟、黄虎之魂与烈焰领主", - "title": "大陆守护者", - "points": 150, - "bonuses": {"critical_pct": 5} - }, - "ACH_SKILL_GRANDMASTER": { - "name": "大巧不工", - "desc": "将任意核心战斗技能修炼至 P 级完美境界", - "title": "武学宗师", - "points": 80, - "bonuses": {"cast_speed": 10} - }, - "ACH_EPIC_BLACKSMITH": { - "name": "神兵天成", - "desc": "成功完成修元平史诗武器或蓝钢神甲升阶转换", - "title": "神工锻造师", - "points": 80, - "bonuses": {"att": 50} - } -} - -var unlocked_achievements: Dictionary = {} # ach_id -> bool -var unlocked_titles: Array = [] # list of title strings -var total_achievement_points: int = 0 -var active_title: String = "" - -# 追踪并达成成就 -func complete_achievement(ach_id: String) -> Dictionary: - if not ACHIEVEMENTS.has(ach_id): - return {"ok": false, "reason": "INVALID_ACH_ID", "msg": "未知的成就标识!"} - - if unlocked_achievements.get(ach_id, false): - return {"ok": false, "reason": "ALREADY_COMPLETED", "msg": "该成就已达成!"} - - var cfg: Dictionary = ACHIEVEMENTS[ach_id] - unlocked_achievements[ach_id] = true - total_achievement_points += int(cfg["points"]) - - var title_name: String = cfg["title"] - if not unlocked_titles.has(title_name): - unlocked_titles.append(title_name) - - achievement_unlocked.emit(ach_id, cfg["name"], title_name) - - return { - "ok": true, - "ach_id": ach_id, - "name": cfg["name"], - "title": title_name, - "points_awarded": cfg["points"], - "total_points": total_achievement_points, - "msg": "达成里程碑成就【%s】!解锁荣耀称号【%s】!" % [cfg["name"], title_name] - } - -# 佩戴称号 -func equip_title(title_name: String) -> Dictionary: - if not unlocked_titles.has(title_name): - return {"ok": false, "reason": "TITLE_NOT_UNLOCKED", "msg": "你尚未解锁该头衔称号!"} - - active_title = title_name - var bonuses := get_active_bonuses() - - title_equipped.emit(title_name, bonuses) - - return { - "ok": true, - "active_title": active_title, - "bonuses": bonuses, - "msg": "已成功佩戴头衔【%s】!战力属性已生效!" % title_name - } - -# 卸下称号 -func unequip_title() -> void: - active_title = "" - title_unequipped.emit() - -# 获取当前佩戴称号的属性加成 -func get_active_bonuses() -> Dictionary: - if active_title.is_empty(): - return {} - - for cfg in ACHIEVEMENTS.values(): - if cfg["title"] == active_title: - return cfg["bonuses"].duplicate() - - return {} - -# 查询成就状态 -func is_achievement_unlocked(ach_id: String) -> bool: - return unlocked_achievements.get(ach_id, false) diff --git a/project/title_achievement_system.gd.uid b/project/title_achievement_system.gd.uid deleted file mode 100644 index d471e3ef..00000000 --- a/project/title_achievement_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c5nyy71wgaie6 diff --git a/project/title_trophy_buff_system.gd b/project/title_trophy_buff_system.gd deleted file mode 100644 index 752408aa..00000000 --- a/project/title_trophy_buff_system.gd +++ /dev/null @@ -1,173 +0,0 @@ -# title_trophy_buff_system.gd —— Metin2 40250 真实称号词条战力加成与奖杯成就图鉴 1:1 -# 对照 40250 服务端 affect.h, cmd_general.cpp, title.cpp, char_affect.cpp -class_name TitleTrophyBuffSystem -extends RefCounted - -signal trophy_milestone_reached(tier: int, trophy_name: String, bonus_desc: String) -signal title_stat_buffs_applied(title_name: String, buffs: Dictionary) -signal title_stat_buffs_removed(title_name: String) - -# 经典称号与专属真实战力词条池 -const TITLES_CATALOG: Dictionary = { - "屠龙尊者": { - "desc": "贝雷斯水龙终结者,威震天界之峰", - "buffs": { - "boss_dmg_pct": 15, - "max_hp": 2000, - "crit_pct": 5 - } - }, - "恶魔克星": { - "desc": "诛灭死神与阿兹瑞尔,冥府幽界之灾", - "buffs": { - "undead_dmg_pct": 15, - "magic_resist": 10, - "def": 60 - } - }, - "魔石终结者": { - "desc": "踏平八荒八档魔石,破除虚空陨铁", - "buffs": { - "metin_dmg_pct": 12, - "atk": 50, - "penetration_pct": 5 - } - }, - "地牢速通王": { - "desc": "创下各大地牢副本最速纪录的疾风勇者", - "buffs": { - "move_speed": 15, - "cast_speed": 10, - "max_sp": 1000 - } - }, - "百战名将": { - "desc": "力克四大野外世界首领,声震三界", - "buffs": { - "atk": 80, - "def": 50, - "max_hp": 1500 - } - }, - "商海巨擘": { - "desc": "富甲一方,日进斗金的广场交易领主", - "buffs": { - "gold_bonus_pct": 20, - "max_hp": 500 - } - }, - "武学至尊": { - "desc": "多门绝技臻至 P 级圆满的盖世宗师", - "buffs": { - "skill_dmg_pct": 10, - "cast_speed": 15, - "atk": 60 - } - } -} - -# 奖杯累积成就里程碑 (永久被动战力加成) -const TROPHY_MILESTONES: Dictionary = { - 1: {"pts": 50, "name": "青铜试炼奖杯", "bonus": {"all_stats": 3}, "desc": "全属性 +3"}, - 2: {"pts": 150, "name": "白银征服奖杯", "bonus": {"max_hp": 1000, "drop_rate_pct": 5}, "desc": "生命上限 +1000,掉宝率 +5%"}, - 3: {"pts": 300, "name": "黄金霸主奖杯", "bonus": {"atk": 50, "crit_pct": 5}, "desc": "攻击力 +50,暴击率 +5%"}, - 4: {"pts": 500, "name": "铂金封神奖杯", "bonus": {"boss_dmg_pct": 10, "exp_bonus_pct": 10}, "desc": "首领伤害 +10%,经验获取 +10%"} -} - -var active_title: String = "" -var active_title_buffs: Dictionary = {} -var unlocked_titles: Array = [] -var trophy_points: int = 0 -var reached_milestones: Array = [] - -# 解锁新称号 -func unlock_title(title_name: String, awarded_points: int = 50) -> Dictionary: - if not TITLES_CATALOG.has(title_name): - return {"ok": false, "reason": "UNKNOWN_TITLE", "msg": "未知的称号!"} - - if not (title_name in unlocked_titles): - unlocked_titles.append(title_name) - trophy_points += awarded_points - _check_trophy_milestones() - return {"ok": true, "title": title_name, "points": trophy_points, "msg": "解锁了殿堂称号【%s】!" % title_name} - - return {"ok": true, "already_unlocked": true, "title": title_name} - -# 佩戴指定称号并动态注入角色属性面板 -func equip_title(title_name: String, player_data: Dictionary) -> Dictionary: - if not (title_name in unlocked_titles): - return {"ok": false, "reason": "TITLE_NOT_UNLOCKED", "msg": "尚未解锁该称号!"} - - # 先卸下旧称号属性 - if not active_title.is_empty(): - _remove_title_buffs(player_data) - - active_title = title_name - var cfg = TITLES_CATALOG[title_name] - active_title_buffs = cfg["buffs"].duplicate(true) - - _apply_buffs_to_player(player_data, active_title_buffs) - title_stat_buffs_applied.emit(active_title, active_title_buffs) - - return { - "ok": true, - "title": active_title, - "buffs": active_title_buffs, - "msg": "已佩戴称号【%s】!获得了专属战力加成。" % active_title - } - -# 卸下当前称号 -func unequip_title(player_data: Dictionary) -> Dictionary: - if active_title.is_empty(): - return {"ok": false, "reason": "NO_TITLE_EQUIPPED"} - - var old = active_title - _remove_title_buffs(player_data) - active_title = "" - active_title_buffs.clear() - - title_stat_buffs_removed.emit(old) - - return {"ok": true, "msg": "已卸下称号【%s】。" % old} - -# 将 Buff 加成数值安全叠加入 player_data -func _apply_buffs_to_player(player_data: Dictionary, buffs: Dictionary) -> void: - for stat_key in buffs.keys(): - var val = buffs[stat_key] - player_data[stat_key] = player_data.get(stat_key, 0) + val - -# 从 player_data 中精准反扣旧 Buff 加成数值 -func _remove_title_buffs(player_data: Dictionary) -> void: - for stat_key in active_title_buffs.keys(): - var val = active_title_buffs[stat_key] - player_data[stat_key] = player_data.get(stat_key, 0) - val - -# 检测奖杯里程碑晋阶 -func _check_trophy_milestones() -> void: - for tier in [1, 2, 3, 4]: - if not (tier in reached_milestones): - var req = TROPHY_MILESTONES[tier]["pts"] - if trophy_points >= req: - reached_milestones.append(tier) - var m = TROPHY_MILESTONES[tier] - trophy_milestone_reached.emit(tier, m["name"], m["desc"]) - -# 获取所有奖杯被动被动加成总计 -func get_total_trophy_passive_bonuses() -> Dictionary: - var total: Dictionary = {} - for tier in reached_milestones: - var b = TROPHY_MILESTONES[tier]["bonus"] - for k in b.keys(): - total[k] = total.get(k, 0) + b[k] - return total - -# 获取当前全部战力属性摘要 -func get_title_status() -> Dictionary: - return { - "active_title": active_title, - "active_buffs": active_title_buffs, - "unlocked_count": unlocked_titles.size(), - "trophy_points": trophy_points, - "reached_milestones": reached_milestones, - "passive_bonuses": get_total_trophy_passive_bonuses() - } diff --git a/project/title_trophy_buff_system.gd.uid b/project/title_trophy_buff_system.gd.uid deleted file mode 100644 index a1a6eab0..00000000 --- a/project/title_trophy_buff_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c6ydp08s8sf2k diff --git a/project/treasure_box_key_system.gd b/project/treasure_box_key_system.gd deleted file mode 100644 index c391053d..00000000 --- a/project/treasure_box_key_system.gd +++ /dev/null @@ -1,248 +0,0 @@ -# treasure_box_key_system.gd —— Metin2 40250 官方金银宝箱与专属钥匙拖拽开启系统 1:1 -# 100% 对照 40250 服务端 char_item.cpp (ITEM_TREASURE_BOX, ITEM_TREASURE_KEY), special_item_group.txt, item_proto.txt -class_name TreasureBoxKeySystem -extends RefCounted - -signal box_unlocked(box_vnum: int, key_vnum: int, reward: Dictionary) -signal unlock_failed(reason: String) - -# 宝箱类型分类常数 (对照 item->GetValue(0)) -const BOX_TYPE_SILVER := 1 # 银系列 (Silver) -const BOX_TYPE_GOLD := 2 # 金系列 (Gold) - -# 40250 官方宝箱资料表 (ITEM_TREASURE_BOX) -const BOXES: Dictionary = { - 50006: { - "name": "银宝箱 (Silver Treasure Box)", - "type_id": BOX_TYPE_SILVER, - "tr_name": "Gümüş Define Sandığı" - }, - 50007: { - "name": "金宝箱 (Gold Treasure Box)", - "type_id": BOX_TYPE_GOLD, - "tr_name": "Altın Define Sandığı" - }, - 50012: { - "name": "银宝箱+ (Silver Treasure Box+)", - "type_id": BOX_TYPE_SILVER, - "tr_name": "Gümüş Define Sandığı+" - }, - 50013: { - "name": "金宝箱+ (Gold Treasure Box+)", - "type_id": BOX_TYPE_GOLD, - "tr_name": "Altın Define Sandığı+" - } -} - -# 40250 官方钥匙资料表 (ITEM_TREASURE_KEY) -const KEYS: Dictionary = { - 50008: { - "name": "银钥匙 (Silver Key)", - "type_id": BOX_TYPE_SILVER, - "tr_name": "Gümüş Anahtar" - }, - 50009: { - "name": "金钥匙 (Gold Key)", - "type_id": BOX_TYPE_GOLD, - "tr_name": "Altın Anahtar" - }, - 50014: { - "name": "银钥匙+ (Silver Key+)", - "type_id": BOX_TYPE_SILVER, - "tr_name": "Gümüş Anahtar+" - }, - 50015: { - "name": "金钥匙+ (Gold Key+)", - "type_id": BOX_TYPE_GOLD, - "tr_name": "Altın Anahtar+" - } -} - -# 40250 special_item_group.txt 官方掉落池 -const DROP_TABLES: Dictionary = { - 50006: [ # 银宝箱 - {"type": "item", "vnum": 50300, "count": 1, "name": "技能书 (Skill Book)", "prob": 100}, - {"type": "item", "vnum": 25040, "count": 1, "name": "祝福卷轴 (Blessing Scroll)", "prob": 40}, - {"type": "gold", "amount": 80000, "name": "80,000 金币", "prob": 30}, - {"type": "exp", "amount": 30000, "name": "30,000 经验值", "prob": 30}, - {"type": "item", "vnum": 30047, "count": 1, "name": "诅咒之书 (Curse Book)", "prob": 20}, - {"type": "item", "vnum": 30050, "count": 1, "name": "冰之碎片 (Ice Piece)", "prob": 20}, - {"type": "item", "vnum": 70008, "count": 1, "name": "温暖葡萄酒", "prob": 15} - ], - 50007: [ # 金宝箱 - {"type": "item", "vnum": 25040, "count": 1, "name": "祝福卷轴 (Blessing Scroll)", "prob": 60}, - {"type": "item", "vnum": 70024, "count": 1, "name": "经验指环 (Ring of Experience)", "prob": 30}, - {"type": "item", "vnum": 70003, "count": 1, "name": "小偷手套 (Thief Glove)", "prob": 30}, - {"type": "gold", "amount": 150000, "name": "150,000 金币", "prob": 40}, - {"type": "exp", "amount": 60000, "name": "60,000 经验值", "prob": 40}, - {"type": "item", "vnum": 27003, "count": 50, "name": "红色大生命药水 x50", "prob": 50}, - {"type": "item", "vnum": 27006, "count": 50, "name": "蓝色大魔法药水 x50", "prob": 50} - ], - 50012: [ # 银宝箱+ - {"type": "item", "vnum": 50300, "count": 2, "name": "技能书 x2", "prob": 100}, - {"type": "item", "vnum": 25040, "count": 1, "name": "祝福卷轴", "prob": 50}, - {"type": "gold", "amount": 120000, "name": "120,000 金币", "prob": 40}, - {"type": "item", "vnum": 70048, "count": 1, "name": "变装斗篷", "prob": 25} - ], - 50013: [ # 金宝箱+ - {"type": "item", "vnum": 25040, "count": 2, "name": "祝福卷轴 x2", "prob": 80}, - {"type": "item", "vnum": 70024, "count": 1, "name": "经验指环", "prob": 45}, - {"type": "gold", "amount": 250000, "name": "250,000 金币", "prob": 50}, - {"type": "exp", "amount": 100000, "name": "100,000 经验值", "prob": 50} - ] -} - -# 开启历史记录 -var _open_history: Array[Dictionary] = [] - -# 判断是否是宝箱 -func is_treasure_box(vnum: int) -> bool: - return BOXES.has(vnum) - -# 判断是否是钥匙 -func is_treasure_key(vnum: int) -> bool: - return KEYS.has(vnum) - -# 校验钥匙与宝箱是否能够配对开启 (item->GetValue(0) == item2->GetValue(0)) -func can_unlock(key_vnum: int, box_vnum: int) -> bool: - if not KEYS.has(key_vnum) or not BOXES.has(box_vnum): - return false - return int(KEYS[key_vnum]["type_id"]) == int(BOXES[box_vnum]["type_id"]) - -# 拖拽钥匙至宝箱进行解锁 -func unlock_box_with_key( - inventory: Array, - key_slot: int, - box_slot: int, - forced_drop_idx: int = -1 -) -> Dictionary: - if key_slot < 0 or key_slot >= inventory.size() or inventory[key_slot] == null: - unlock_failed.emit("INVALID_KEY_SLOT") - return {"ok": false, "reason": "INVALID_KEY_SLOT", "msg": "未找到指定的钥匙!"} - - if box_slot < 0 or box_slot >= inventory.size() or inventory[box_slot] == null: - unlock_failed.emit("INVALID_BOX_SLOT") - return {"ok": false, "reason": "INVALID_BOX_SLOT", "msg": "未找到指定的宝箱!"} - - var key_item = inventory[key_slot] - var box_item = inventory[box_slot] - - var key_vnum = int(key_item.get("vnum", 0)) - var box_vnum = int(box_item.get("vnum", 0)) - - # 检查物品分类是否为钥匙与宝箱 - if not is_treasure_key(key_vnum): - unlock_failed.emit("NOT_A_KEY") - return {"ok": false, "reason": "NOT_A_KEY", "msg": "被拖拽的物品并不是一把宝箱钥匙!"} - - if not is_treasure_box(box_vnum): - unlock_failed.emit("NOT_A_TREASURE_BOX") - # 40250 char_item.cpp:1948 "그 열쇠로는 열 수 없는 것 같군." - return {"ok": false, "reason": "NOT_A_TREASURE_BOX", "msg": "这把钥匙似乎无法用来开启这个物品。"} - - # 检查金银分类匹配 - if not can_unlock(key_vnum, box_vnum): - unlock_failed.emit("KEY_MISMATCH") - return { - "ok": false, - "reason": "KEY_MISMATCH", - "msg": "钥匙型号不吻合!%s 无法打开 %s。" % [KEYS[key_vnum]["name"], BOXES[box_vnum]["name"]] - } - - # 配对成功:双双扣除 1 个数量 - _consume_item_one(inventory, key_slot) - _consume_item_one(inventory, box_slot) - - # 计算奖励 - var drops: Array = DROP_TABLES.get(box_vnum, []) - if drops.is_empty(): - drops = DROP_TABLES[50006] - - var picked_reward: Dictionary = {} - if forced_drop_idx >= 0 and forced_drop_idx < drops.size(): - picked_reward = drops[forced_drop_idx] - else: - # 加权随机选取 - var total_weight := 0 - for d in drops: - total_weight += int(d.get("prob", 10)) - var r = randi_range(1, total_weight) - var current := 0 - for d in drops: - current += int(d.get("prob", 10)) - if r <= current: - picked_reward = d - break - - # 发放奖励 - var reward_type = picked_reward.get("type", "item") - match reward_type: - "item": - _give_or_stack_item(inventory, int(picked_reward["vnum"]), int(picked_reward.get("count", 1))) - "gold": - # 金币奖励 - pass - "exp": - # 经验奖励 - pass - - var record = { - "box_vnum": box_vnum, - "key_vnum": key_vnum, - "reward": picked_reward - } - _open_history.append(record) - box_unlocked.emit(box_vnum, key_vnum, picked_reward) - - return { - "ok": true, - "box_name": BOXES[box_vnum]["name"], - "key_name": KEYS[key_vnum]["name"], - "reward": picked_reward, - "msg": "咔哒一声脆响!宝箱锁扣开启,你从箱中获得了【%s】!" % picked_reward.get("name", "宝物") - } - -# 获取宝箱完整掉落表 -func get_box_drop_table(box_vnum: int) -> Array: - return DROP_TABLES.get(box_vnum, []) - -# 开启历史记录 -func get_open_history() -> Array[Dictionary]: - return _open_history - -func clear_history() -> void: - _open_history.clear() - -# 内部消耗与堆叠工具函数 -func _consume_item_one(inventory: Array, slot: int) -> void: - if slot < 0 or slot >= inventory.size() or inventory[slot] == null: - return - var item = inventory[slot] - var count = int(item.get("count", 1)) - if count > 1: - item["count"] = count - 1 - else: - inventory[slot] = null - -func _give_or_stack_item(inventory: Array, vnum: int, count: int) -> void: - for it in inventory: - if it != null and int(it.get("vnum", 0)) == vnum: - it["count"] = int(it.get("count", 1)) + count - return - for i in range(inventory.size()): - if inventory[i] == null: - inventory[i] = {"vnum": vnum, "count": count} - return - inventory.append({"vnum": vnum, "count": count}) - -# 序列化与反序列化 -func serialize() -> Dictionary: - return { - "open_history": _open_history.duplicate(true) - } - -func deserialize(data: Dictionary) -> void: - _open_history.clear() - var raw = data.get("open_history", []) - for item in raw: - _open_history.append(item.duplicate(true)) diff --git a/project/treasure_box_key_system.gd.uid b/project/treasure_box_key_system.gd.uid deleted file mode 100644 index 7b272627..00000000 --- a/project/treasure_box_key_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://buudr3506irfn diff --git a/project/wardrobe_transmog_system.gd b/project/wardrobe_transmog_system.gd deleted file mode 100644 index d4898762..00000000 --- a/project/wardrobe_transmog_system.gd +++ /dev/null @@ -1,146 +0,0 @@ -# wardrobe_transmog_system.gd —— Metin2 40250 装备外观幻化重塑与试衣间系统 1:1 -# 对照 40250 服务端 item_transmutation.cpp, char_item.cpp -class_name WardrobeTransmogSystem -extends RefCounted - -signal item_transmuted(target_slot: int, target_name: String, new_look_vnum: int, new_look_name: String) -signal transmutation_reverted(target_slot: int, item_name: String) - -const VNUM_TRANSMOG_SCROLL := 72301 # 幻化秘籍 -const VNUM_REVERT_SCROLL := 72302 # 幻化还原秘籍 -const TRANSMOG_COST_GOLD := 10000000 # 1000 万金币 - -# 装备大类 -const TYPE_WEAPON := 1 -const TYPE_ARMOR := 2 -const TYPE_COSTUME := 28 - -# 试衣间预览外观效果 (不消耗物品) -func preview_transmog(target_item: Dictionary, skin_item: Dictionary) -> Dictionary: - var val = _validate_compatibility(target_item, skin_item) - if not val["ok"]: - return val - - return { - "ok": true, - "base_vnum": target_item.get("vnum", 0), - "base_name": target_item.get("name", "神装"), - "preview_vnum": skin_item.get("vnum", 0), - "preview_name": skin_item.get("name", "幻化外观"), - "msg": "试衣间预览生成完毕!" - } - -# 执行幻化外观重塑 -func apply_transmutation(scroll_slot: int, target_slot: int, skin_slot: int, inventory: Array, player_data: Dictionary) -> Dictionary: - if scroll_slot < 0 or scroll_slot >= inventory.size() or inventory[scroll_slot] == null: - return {"ok": false, "reason": "INVALID_SCROLL_SLOT"} - if target_slot < 0 or target_slot >= inventory.size() or inventory[target_slot] == null: - return {"ok": false, "reason": "INVALID_TARGET_SLOT"} - if skin_slot < 0 or skin_slot >= inventory.size() or inventory[skin_slot] == null: - return {"ok": false, "reason": "INVALID_SKIN_SLOT"} - - if target_slot == skin_slot: - return {"ok": false, "reason": "SAME_SLOT", "msg": "目标装备与外观装备不能为同一格子!"} - - var scroll = inventory[scroll_slot] - if int(scroll.get("vnum", 0)) != VNUM_TRANSMOG_SCROLL: - return {"ok": false, "reason": "NOT_A_TRANSMOG_SCROLL", "msg": "需要使用【幻化秘籍】(72301)!"} - - var target = inventory[target_slot] - var skin = inventory[skin_slot] - - var comp_check = _validate_compatibility(target, skin) - if not comp_check["ok"]: - return comp_check - - var gold = int(player_data.get("gold", 0)) - if gold < TRANSMOG_COST_GOLD: - return {"ok": false, "reason": "NOT_ENOUGH_GOLD", "msg": "幻化所需金币不足!需要 10,000,000 金币。"} - - # 扣除金币与卷轴 - player_data["gold"] = gold - TRANSMOG_COST_GOLD - var sc_cnt = int(scroll.get("count", 1)) - if sc_cnt > 1: - scroll["count"] = sc_cnt - 1 - else: - inventory[scroll_slot] = null - - # 消耗外观材料装备 - var skin_vnum = int(skin.get("vnum", 0)) - var skin_name = skin.get("name", "外观") - var sk_cnt = int(skin.get("count", 1)) - if sk_cnt > 1: - skin["count"] = sk_cnt - 1 - else: - inventory[skin_slot] = null - - # 赋予目标装备幻化外观 (所有基础属性、强化与灵石 100% 保留) - target["transmog_vnum"] = skin_vnum - target["transmog_name"] = skin_name - - var tname: String = target.get("name", "装备") - item_transmuted.emit(target_slot, tname, skin_vnum, skin_name) - - return { - "ok": true, - "target_slot": target_slot, - "target_name": tname, - "transmog_vnum": skin_vnum, - "transmog_name": skin_name, - "msg": "幻化成功!【%s】披上了【%s】的华丽外观!" % [tname, skin_name] - } - -# 还原装备原生外观 -func revert_transmutation(scroll_slot: int, target_slot: int, inventory: Array) -> Dictionary: - if scroll_slot < 0 or scroll_slot >= inventory.size() or inventory[scroll_slot] == null: - return {"ok": false, "reason": "INVALID_SCROLL_SLOT"} - if target_slot < 0 or target_slot >= inventory.size() or inventory[target_slot] == null: - return {"ok": false, "reason": "INVALID_TARGET_SLOT"} - - var scroll = inventory[scroll_slot] - if int(scroll.get("vnum", 0)) != VNUM_REVERT_SCROLL: - return {"ok": false, "reason": "NOT_A_REVERT_SCROLL", "msg": "需要使用【幻化还原秘籍】(72302)!"} - - var target = inventory[target_slot] - if not target.has("transmog_vnum") or int(target.get("transmog_vnum", 0)) == 0: - return {"ok": false, "reason": "NOT_TRANSMUTED", "msg": "该装备未曾幻化,无需还原!"} - - # 消耗还原卷轴 - var cnt = int(scroll.get("count", 1)) - if cnt > 1: - scroll["count"] = cnt - 1 - else: - inventory[scroll_slot] = null - - target.erase("transmog_vnum") - target.erase("transmog_name") - - var tname: String = target.get("name", "装备") - transmutation_reverted.emit(target_slot, tname) - - return { - "ok": true, - "target_slot": target_slot, - "item_name": tname, - "msg": "幻化已抹除!【%s】恢复为原生经典外观。" % tname - } - -# 校验外观类型兼容性 -func _validate_compatibility(target: Dictionary, skin: Dictionary) -> Dictionary: - if target.is_empty() or skin.is_empty(): - return {"ok": false, "reason": "EMPTY_DATA"} - - var t_type = int(target.get("type", 1)) - var s_type = int(skin.get("type", 1)) - - # 武器只能幻化武器或时装武器外观;防具只能幻化防具或时装衣服 - if t_type == TYPE_WEAPON: - if s_type != TYPE_WEAPON and s_type != TYPE_COSTUME: - return {"ok": false, "reason": "TYPE_MISMATCH", "msg": "武器只能幻化武器或时装外观!"} - elif t_type == TYPE_ARMOR: - if s_type != TYPE_ARMOR and s_type != TYPE_COSTUME: - return {"ok": false, "reason": "TYPE_MISMATCH", "msg": "防具衣服只能幻化衣服防具外观!"} - else: - return {"ok": false, "reason": "UNSUPPORTED_TYPE", "msg": "该装备部位暂不支持幻化外观。"} - - return {"ok": true} diff --git a/project/wardrobe_transmog_system.gd.uid b/project/wardrobe_transmog_system.gd.uid deleted file mode 100644 index ff750cf1..00000000 --- a/project/wardrobe_transmog_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dxudepkqnmxni diff --git a/project/weather_daynight_system.gd b/project/weather_daynight_system.gd deleted file mode 100644 index a19c5b58..00000000 --- a/project/weather_daynight_system.gd +++ /dev/null @@ -1,168 +0,0 @@ -# weather_daynight_system.gd —— Metin2 40250 单机本地天候昼夜交替与地图环境特效系统 1:1 -# 对照 40250 客户端 PythonBackground.cpp, game.py, environment.h -class_name WeatherDayNightSystem -extends RefCounted - -signal phase_changed(old_phase: String, new_phase: String) -signal weather_changed(old_weather: String, new_weather: String) -signal lighting_updated(sun_color: Color, sun_energy: float, ambient_color: Color, fog_density: float) - -const PHASE_DAWN := "dawn" # 黎明晨曦 (05:00 ~ 08:00) -const PHASE_NOON := "noon" # 正午晴空 (08:00 ~ 17:00) -const PHASE_DUSK := "dusk" # 黄昏落日 (17:00 ~ 20:00) -const PHASE_NIGHT := "night" # 寂静黑夜 (20:00 ~ 05:00) - -const WEATHER_CLEAR := "clear" # 晴朗明净 -const WEATHER_RAIN := "rain" # 潇潇细雨 -const WEATHER_SNOW := "snow" # 冰山暴雪 (Mount Sohan) -const WEATHER_SANDSTORM := "sandstorm" # 狂沙飞舞 (Yongbi Desert) -const WEATHER_CINDERS := "cinders" # 烈焰余烬 (Doyyumhwan) - -const SECONDS_PER_DAY: float = 86400.0 -const DEFAULT_TIME_SCALE: float = 60.0 # 默认 1 秒流逝 60 秒游戏时间 (24 分钟一昼夜) - -const MAP_DEFAULT_WEATHER: Dictionary = { - "village": WEATHER_CLEAR, - "sohan": WEATHER_SNOW, - "desert": WEATHER_SANDSTORM, - "flame": WEATHER_CINDERS, - "temple": WEATHER_CLEAR, - "catacomb": WEATHER_CLEAR -} - -var time_of_day: float = 43200.0 # 默认正午 12:00:00 (12 * 3600) -var time_scale: float = DEFAULT_TIME_SCALE -var is_frozen: bool = false - -var current_phase: String = PHASE_NOON -var current_weather: String = WEATHER_CLEAR -var current_map: String = "village" - -# 光影参数 -var sun_color: Color = Color(1.0, 0.98, 0.9) -var sun_energy: float = 1.2 -var ambient_color: Color = Color(0.9, 0.9, 0.95) -var fog_density: float = 0.001 - -func _init() -> void: - _update_phase_and_lighting() - -# 帧心跳更新天候流逝 -func update(delta: float) -> void: - if not is_frozen: - time_of_day = fmod(time_of_day + delta * time_scale, SECONDS_PER_DAY) - if time_of_day < 0.0: - time_of_day += SECONDS_PER_DAY - - _update_phase_and_lighting() - -# 手动设置指定时间 (小时 0~24,分钟 0~60) -func set_time_of_day(hour: float, minute: float = 0.0) -> void: - var clamped_h = clampf(hour, 0.0, 23.99) - var clamped_m = clampf(minute, 0.0, 59.99) - time_of_day = clamped_h * 3600.0 + clamped_m * 60.0 - _update_phase_and_lighting() - -# 直接快速切换至指定时相 -func set_phase(target_phase: String) -> void: - match target_phase: - PHASE_DAWN: - set_time_of_day(6.0, 0.0) - PHASE_NOON: - set_time_of_day(12.0, 0.0) - PHASE_DUSK: - set_time_of_day(18.5, 0.0) - PHASE_NIGHT: - set_time_of_day(23.0, 0.0) - -# 设置天气模式 -func set_weather(weather_type: String) -> Dictionary: - var valid_weathers = [WEATHER_CLEAR, WEATHER_RAIN, WEATHER_SNOW, WEATHER_SANDSTORM, WEATHER_CINDERS] - if not (weather_type in valid_weathers): - return {"ok": false, "reason": "INVALID_WEATHER_TYPE"} - - if current_weather != weather_type: - var old = current_weather - current_weather = weather_type - weather_changed.emit(old, weather_type) - - _update_phase_and_lighting() - return {"ok": true, "weather": current_weather} - -# 设置当前地图并自动匹配环境天气 -func set_map_environment(map_name: String) -> void: - current_map = map_name - var def_weather = MAP_DEFAULT_WEATHER.get(map_name, WEATHER_CLEAR) - set_weather(def_weather) - -# 核心昼夜时相与光照参数计算 -func _update_phase_and_lighting() -> void: - var hours = time_of_day / 3600.0 - var new_phase = PHASE_NOON - - if hours >= 5.0 and hours < 8.0: - new_phase = PHASE_DAWN - sun_color = Color(1.0, 0.8, 0.6) # 晨曦暖金 - sun_energy = 0.75 - ambient_color = Color(0.85, 0.75, 0.65) - fog_density = 0.002 - elif hours >= 8.0 and hours < 17.0: - new_phase = PHASE_NOON - sun_color = Color(1.0, 0.98, 0.92) # 正午白炽日光 - sun_energy = 1.2 - ambient_color = Color(0.9, 0.9, 0.95) - fog_density = 0.001 - elif hours >= 17.0 and hours < 20.0: - new_phase = PHASE_DUSK - sun_color = Color(0.95, 0.45, 0.3) # 晚霞橙红 - sun_energy = 0.65 - ambient_color = Color(0.7, 0.45, 0.4) - fog_density = 0.003 - else: - new_phase = PHASE_NIGHT - sun_color = Color(0.3, 0.4, 0.7) # 冷冽月光 - sun_energy = 0.25 - ambient_color = Color(0.15, 0.2, 0.35) - fog_density = 0.004 - - # 天气修正 - match current_weather: - WEATHER_RAIN: - sun_energy *= 0.6 - fog_density += 0.003 - WEATHER_SNOW: - sun_color = sun_color.lerp(Color(0.85, 0.9, 1.0), 0.5) - fog_density += 0.002 - WEATHER_SANDSTORM: - sun_color = sun_color.lerp(Color(0.85, 0.7, 0.4), 0.6) - fog_density += 0.008 - WEATHER_CINDERS: - sun_color = sun_color.lerp(Color(1.0, 0.35, 0.2), 0.5) - fog_density += 0.004 - - if current_phase != new_phase: - var old = current_phase - current_phase = new_phase - phase_changed.emit(old, new_phase) - - lighting_updated.emit(sun_color, sun_energy, ambient_color, fog_density) - -# 获取格式化时间字符串 "HH:MM:SS" -func get_time_string() -> String: - var total_sec = int(time_of_day) - var h = int(total_sec / 3600) - var m = int((total_sec % 3600) / 60) - var s = total_sec % 60 - return "%02d:%02d:%02d" % [h, m, s] - -# 获取当前光照参数字典 -func get_current_lighting() -> Dictionary: - return { - "phase": current_phase, - "weather": current_weather, - "time_string": get_time_string(), - "sun_color": sun_color, - "sun_energy": sun_energy, - "ambient_color": ambient_color, - "fog_density": fog_density - } diff --git a/project/weather_daynight_system.gd.uid b/project/weather_daynight_system.gd.uid deleted file mode 100644 index ca346bbe..00000000 --- a/project/weather_daynight_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dqixlsp4mtv22 diff --git a/project/wedding_marriage_system.gd b/project/wedding_marriage_system.gd deleted file mode 100644 index f4c25030..00000000 --- a/project/wedding_marriage_system.gd +++ /dev/null @@ -1,168 +0,0 @@ -# wedding_marriage_system.gd —— Metin2 40250 官方结婚典礼与夫妻同心传送婚戒系统 1:1 -# 对照 40250 服务端 wedding.cpp, wedding.h, marriage.h, char_item.cpp -class_name WeddingMarriageSystem -extends RefCounted - -signal wedding_started(spouse_name: String) -signal marriage_completed(spouse_name: String) -signal spouse_warped(target_map: String, target_pos: Vector2) -signal love_points_updated(new_points: int) -signal divorced() - -const NPC_OLD_WOMAN := 9006 # 老妪/月老花仙子 (Old Woman) -const WEDDING_MAP_INDEX := 81 # 官方婚礼殿堂地图 (Wedding Map) - -const VNUM_ENGAGEMENT_RING := 70301 # 订婚戒指 -const VNUM_WEDDING_RING := 70302 # 结婚戒指 (支持一键夫妻瞬间传送) -const VNUM_TUXEDO := 11901 # 新郎礼服 -const VNUM_WEDDING_DRESS := 11903 # 新娘婚纱 - -const VNUM_RING_OF_LOVE := 71070 # 爱情之戒 (夫妻经验加成) -const VNUM_BRACELET_OF_LOVE:= 71073 # 爱情手镯 (夫妻暴击加成) - -# 婚姻状态 -var is_married: bool = false -var spouse_name: String = "" -var love_points: int = 0 # 爱情度 (0~100) -var marry_timestamp: int = 0 - -# 举行结婚典礼 (40250 WeddingManager::Request) -func conduct_wedding(partner_name: String, inventory: Array, player_gender: String = "M") -> Dictionary: - if is_married: - return {"ok": false, "reason": "ALREADY_MARRIED", "msg": "已有婚姻关系,无法重复结婚!"} - - if partner_name.strip_edges().is_empty(): - return {"ok": false, "reason": "INVALID_PARTNER_NAME", "msg": "伴侣角色名称无效!"} - - # 检查背包是否拥有订婚戒指 (70301) - var ring_slot := -1 - for i in range(inventory.size()): - if inventory[i] != null and int(inventory[i].get("vnum", 0)) == VNUM_ENGAGEMENT_RING: - ring_slot = i - break - - if ring_slot == -1: - return {"ok": false, "reason": "NO_ENGAGEMENT_RING", "msg": "需要持有【订婚戒指】(70301)方可举办婚礼!"} - - # 检查是否穿戴礼服或背包携带礼服 - var required_dress := VNUM_TUXEDO if player_gender == "M" else VNUM_WEDDING_DRESS - var has_dress := false - for i in range(inventory.size()): - if inventory[i] != null and int(inventory[i].get("vnum", 0)) == required_dress: - has_dress = true - break - - if not has_dress: - var dress_name := "新郎礼服" if player_gender == "M" else "新娘婚纱" - return {"ok": false, "reason": "NO_WEDDING_DRESS", "msg": "举办婚礼需身着【%s】!" % dress_name} - - # 消耗订婚戒指,替换为正式的【结婚戒指】(70302) - inventory[ring_slot] = { - "vnum": VNUM_WEDDING_RING, - "name": "结婚戒指", - "spouse": partner_name, - "count": 1 - } - - is_married = true - spouse_name = partner_name - love_points = 50 # 初始爱情度 50 - marry_timestamp = int(Time.get_unix_time_from_system()) - - wedding_started.emit(spouse_name) - marriage_completed.emit(spouse_name) - - return { - "ok": true, - "spouse_name": spouse_name, - "love_points": love_points, - "wedding_ring_slot": ring_slot, - "msg": "恭喜!您与【%s】在月老见证下喜结良缘,获得【结婚戒指】!" % spouse_name - } - -# 使用结婚戒指瞬间传送至配偶身旁 (40250 char_item.cpp: Wedding Ring Warp) -func use_wedding_ring(spouse_online: bool, spouse_map: String, spouse_pos: Vector2, current_map: String) -> Dictionary: - if not is_married: - return {"ok": false, "reason": "NOT_MARRIED", "msg": "您当前未婚,无法使用结婚戒指!"} - - if not spouse_online: - return {"ok": false, "reason": "SPOUSE_OFFLINE", "msg": "您的配偶当前不在线!"} - - # 禁止传送的特殊限制地图 (如竞技场、武斗场) - if current_map == "Arena" or spouse_map == "Arena": - return {"ok": false, "reason": "FORBIDDEN_MAP", "msg": "比武擂台区域禁止使用结婚戒指传送!"} - - spouse_warped.emit(spouse_map, spouse_pos) - - return { - "ok": true, - "target_map": spouse_map, - "target_pos": spouse_pos, - "msg": "心灵相通!已跨越山海传送至配偶【%s】身侧。" % spouse_name - } - -# 夫妻相伴增加爱情度 (40250 marriage.h: love_point) -func add_love_points(amount: int) -> void: - if not is_married: - return - love_points = clampi(love_points + amount, 0, 100) - love_points_updated.emit(love_points) - -# 计算夫妻同心定情信物光环加成 -func calculate_marriage_buff(has_ring_of_love: bool, has_bracelet_of_love: bool, is_near_spouse: bool) -> Dictionary: - if not is_married: - return {} - - var buffs: Dictionary = {} - if is_near_spouse: - # 夫妻在 10 米范围内享有同心基础增益 - buffs["couple_exp_bonus_pct"] = 10 + int(love_points / 10) - - if has_ring_of_love: - buffs["couple_exp_bonus_pct"] += 20 # 爱情之戒再加 20% - - if has_bracelet_of_love: - buffs["crit_pct"] = 10 # 爱情手镯增加 10% 暴击 - - return buffs - -# 协议离婚 (40250 WeddingManager: End Marriage) -func divorce(inventory: Array) -> Dictionary: - if not is_married: - return {"ok": false, "reason": "NOT_MARRIED"} - - # 回收/作废背包中的结婚戒指 - for i in range(inventory.size()): - if inventory[i] != null and int(inventory[i].get("vnum", 0)) == VNUM_WEDDING_RING: - inventory[i] = null - break - - var old_spouse := spouse_name - is_married = false - spouse_name = "" - love_points = 0 - marry_timestamp = 0 - - divorced.emit() - - return { - "ok": true, - "old_spouse": old_spouse, - "msg": "已与【%s】解除婚姻关系,收回结婚戒指。" % old_spouse - } - -# 序列化 -func serialize() -> Dictionary: - return { - "is_married": is_married, - "spouse_name": spouse_name, - "love_points": love_points, - "marry_timestamp": marry_timestamp - } - -# 反序列化 -func deserialize(data: Dictionary) -> void: - is_married = bool(data.get("is_married", false)) - spouse_name = str(data.get("spouse_name", "")) - love_points = int(data.get("love_points", 0)) - marry_timestamp = int(data.get("marry_timestamp", 0)) diff --git a/project/wedding_marriage_system.gd.uid b/project/wedding_marriage_system.gd.uid deleted file mode 100644 index 7ff02ff4..00000000 --- a/project/wedding_marriage_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b4idmloekgb1e diff --git a/project/world_boss_ai_system.gd b/project/world_boss_ai_system.gd deleted file mode 100644 index bbb8276e..00000000 --- a/project/world_boss_ai_system.gd +++ /dev/null @@ -1,298 +0,0 @@ -# world_boss_ai_system.gd —— Metin2 40250 野外世界首领动态巡逻与仇恨反击 AI 1:1 -# 对照 40250 服务端 char_battle.cpp, battle.cpp, mob_manager.cpp, char_state.cpp -class_name WorldBossAISystem -extends RefCounted - -signal state_changed(old_state: String, new_state: String) -signal target_acquired(target_id: String, distance: float) -signal skill_cast(skill_name: String, damage: int, aoe_radius: float) -signal boss_enraged(atk_multiplier: float) -signal leash_reset_started() -signal leash_reset_completed() - -const STATE_IDLE := "idle" -const STATE_PATROL := "patrol" -const STATE_CHASE := "chase" -const STATE_ATTACK := "attack" -const STATE_SKILL := "skill" -const STATE_ENRAGED := "enraged" -const STATE_LEASH_RETURN := "leash_return" -const STATE_DEAD := "dead" - -const AGGRO_RADIUS: float = 18.0 # 索敌警戒范围 (米) -const ATTACK_RANGE: float = 3.5 # 普通近战攻击距离 (米) -const SKILL_RANGE: float = 9.0 # 首领大招攻击距离 (米) -const LEASH_DISTANCE: float = 35.0 # 脱战最远距离 (米) -const PATROL_RADIUS: float = 12.0 # 巡逻游荡半径 (米) -const SKILL_COOLDOWN: float = 8.0 # 技能冷却时间 (秒) -const ENRAGE_HP_RATIO: float = 0.30 # 30% 残血狂暴触发线 - -const BOSS_CONFIG: Dictionary = { - 1901: { - "name": "九尾妖狐", - "level": 72, - "max_hp": 280000, - "atk": 850, - "def": 450, - "skill_name": "暴风雪呼啸", - "skill_mult": 1.8, - "skill_aoe": 8.0 - }, - 1304: { - "name": "黄虎领主", - "level": 75, - "max_hp": 320000, - "atk": 920, - "def": 480, - "skill_name": "神虎震地啸", - "skill_mult": 2.0, - "skill_aoe": 7.5 - }, - 2191: { - "name": "巨龟仙人", - "level": 67, - "max_hp": 350000, - "atk": 780, - "def": 600, - "skill_name": "地刺岩崩", - "skill_mult": 1.6, - "skill_aoe": 9.0 - }, - 2206: { - "name": "烈焰之王", - "level": 73, - "max_hp": 300000, - "atk": 980, - "def": 420, - "skill_name": "地狱火海", - "skill_mult": 2.2, - "skill_aoe": 8.5 - } -} - -var vnum: int = 0 -var boss_name: String = "" -var level: int = 1 -var max_hp: int = 100000 -var current_hp: int = 100000 -var base_atk: int = 500 -var base_def: int = 300 -var skill_name: String = "" -var skill_mult: float = 1.8 -var skill_aoe: float = 8.0 - -var spawn_position: Vector3 = Vector3.ZERO -var current_position: Vector3 = Vector3.ZERO -var patrol_destination: Vector3 = Vector3.ZERO - -var current_state: String = STATE_IDLE -var is_enraged: bool = false -var skill_timer: float = 0.0 -var patrol_wait_timer: float = 0.0 -var current_target_id: String = "" -var aggro_table: Dictionary = {} # target_id -> threat_points - -# 初始化世界首领 -func init_boss(boss_vnum: int, pos: Vector3 = Vector3.ZERO) -> Dictionary: - if not BOSS_CONFIG.has(boss_vnum): - return {"ok": false, "reason": "INVALID_BOSS_VNUM", "msg": "未知的世界 Boss 代码!"} - - var cfg = BOSS_CONFIG[boss_vnum] - vnum = boss_vnum - boss_name = cfg["name"] - level = cfg["level"] - max_hp = cfg["max_hp"] - current_hp = cfg["max_hp"] - base_atk = cfg["atk"] - base_def = cfg["def"] - skill_name = cfg["skill_name"] - skill_mult = cfg["skill_mult"] - skill_aoe = cfg["skill_aoe"] - - spawn_position = pos - current_position = pos - patrol_destination = pos - - current_state = STATE_IDLE - is_enraged = false - skill_timer = 0.0 - patrol_wait_timer = 2.0 - current_target_id = "" - aggro_table.clear() - - return { - "ok": true, - "vnum": vnum, - "name": boss_name, - "level": level, - "hp": current_hp, - "pos": current_position - } - -# 改变当前状态 -func _change_state(new_state: String) -> void: - if current_state == new_state: - return - var old = current_state - current_state = new_state - state_changed.emit(old, new_state) - -# 核心 AI 帧心跳更新驱动 (对照 40250 char_state.cpp) -func update_ai(delta: float, target_pos: Vector3, target_id: String = "player", is_target_alive: bool = true) -> Dictionary: - if current_state == STATE_DEAD: - return {"state": STATE_DEAD, "action": "none"} - - # 技能冷却计时 - if skill_timer > 0.0: - skill_timer = max(0.0, skill_timer - delta) - - # 1. 脱战判定:如果距离出生点超出 LEASH_DISTANCE,强制触发回拉 - var dist_from_spawn = current_position.distance_to(spawn_position) - if dist_from_spawn > LEASH_DISTANCE and current_state != STATE_LEASH_RETURN: - _start_leash_return() - return {"state": STATE_LEASH_RETURN, "action": "leashing", "msg": "超出脱战范围,首领无敌回满血并返回出生点!"} - - # 2. 回拉返回出生点过程 - if current_state == STATE_LEASH_RETURN: - var step = 12.0 * delta # 高速回跑 - current_position = current_position.move_toward(spawn_position, step) - if current_position.distance_to(spawn_position) < 0.5: - current_position = spawn_position - current_hp = max_hp - aggro_table.clear() - current_target_id = "" - is_enraged = false - _change_state(STATE_IDLE) - leash_reset_completed.emit() - return {"state": STATE_IDLE, "action": "reset_done"} - return {"state": STATE_LEASH_RETURN, "action": "moving_home"} - - # 3. 仇恨目标存活与索敌检测 - var dist_to_target = current_position.distance_to(target_pos) - var has_aggro = is_target_alive and (dist_to_target <= AGGRO_RADIUS or aggro_table.has(target_id)) - - if not has_aggro or not is_target_alive: - # 失去仇恨或目标死亡 -> 回归巡逻/待机 - current_target_id = "" - _handle_patrol_idle(delta) - return {"state": current_state, "action": "wandering"} - - # 确认锁定仇恨目标 - if current_target_id != target_id: - current_target_id = target_id - target_acquired.emit(target_id, dist_to_target) - - # 4. 处于战斗追击 / 攻击决策 - if dist_to_target > SKILL_RANGE: - # 距离过远,快速追赶 - _change_state(STATE_CHASE) - var move_speed = 6.0 if not is_enraged else 7.8 - current_position = current_position.move_toward(target_pos, move_speed * delta) - return {"state": STATE_CHASE, "action": "chasing", "dist": dist_to_target} - - # 5. 技能大招释放判定 - if skill_timer <= 0.0 and dist_to_target <= SKILL_RANGE: - skill_timer = SKILL_COOLDOWN - _change_state(STATE_SKILL) - var dmg_mult = skill_mult * (1.5 if is_enraged else 1.0) - var skill_dmg = int(float(base_atk) * dmg_mult) - skill_cast.emit(skill_name, skill_dmg, skill_aoe) - return { - "state": STATE_SKILL, - "action": "skill", - "skill_name": skill_name, - "damage": skill_dmg, - "aoe": skill_aoe - } - - # 6. 普通攻击判定 - if dist_to_target <= ATTACK_RANGE: - _change_state(STATE_ATTACK) - var atk_dmg = int(float(base_atk) * (1.5 if is_enraged else 1.0)) - return { - "state": STATE_ATTACK, - "action": "basic_attack", - "damage": atk_dmg - } - else: - # 在技能范围内但未到普攻范围 -> 继续靠近 - _change_state(STATE_CHASE) - var move_speed = 5.0 if not is_enraged else 6.5 - current_position = current_position.move_toward(target_pos, move_speed * delta) - return {"state": STATE_CHASE, "action": "closing_in", "dist": dist_to_target} - -# 待机与路点漫步巡逻处理 -func _handle_patrol_idle(delta: float) -> void: - if current_state != STATE_PATROL: - _change_state(STATE_PATROL) - - patrol_wait_timer -= delta - if patrol_wait_timer <= 0.0: - # 选取出生点周围随机路点 - var angle = randf_range(0, TAU) - var r = randf_range(2.0, PATROL_RADIUS) - patrol_destination = spawn_position + Vector3(cos(angle) * r, 0.0, sin(angle) * r) - patrol_wait_timer = randf_range(4.0, 7.0) - - if current_position.distance_to(patrol_destination) > 0.5: - current_position = current_position.move_toward(patrol_destination, 2.5 * delta) - -# 开始拉回重置 -func _start_leash_return() -> void: - _change_state(STATE_LEASH_RETURN) - leash_reset_started.emit() - -# 受到攻击与伤害结算 -func receive_damage(raw_dmg: int, attacker_id: String = "player") -> Dictionary: - if current_state == STATE_DEAD: - return {"ok": false, "reason": "ALREADY_DEAD"} - - if current_state == STATE_LEASH_RETURN: - # 脱战回跑期间无敌 - return {"ok": true, "immune": true, "net_dmg": 0, "msg": "首领脱战回归中,处于神圣免疫状态!"} - - var net_dmg = max(1, raw_dmg - base_def) - current_hp = max(0, current_hp - net_dmg) - - # 累计仇恨度 - aggro_table[attacker_id] = aggro_table.get(attacker_id, 0) + net_dmg - - # 狂暴判定:HP <= 30% - var hp_ratio = float(current_hp) / float(max_hp) - if hp_ratio <= ENRAGE_HP_RATIO and not is_enraged: - is_enraged = true - boss_enraged.emit(1.5) - - # 死亡判定 - if current_hp <= 0: - _change_state(STATE_DEAD) - return { - "ok": true, - "dead": true, - "net_dmg": net_dmg, - "remaining_hp": 0, - "msg": "【%s】发出震天哀鸣,轰然倒地!" % boss_name - } - - return { - "ok": true, - "dead": false, - "net_dmg": net_dmg, - "remaining_hp": current_hp, - "hp_ratio": hp_ratio, - "is_enraged": is_enraged - } - -# 获取当前首领状态摘要 -func get_boss_status() -> Dictionary: - return { - "vnum": vnum, - "name": boss_name, - "level": level, - "state": current_state, - "current_hp": current_hp, - "max_hp": max_hp, - "is_enraged": is_enraged, - "pos": current_position, - "spawn_pos": spawn_position - } diff --git a/project/world_boss_ai_system.gd.uid b/project/world_boss_ai_system.gd.uid deleted file mode 100644 index 18a40c90..00000000 --- a/project/world_boss_ai_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://du83e3m8ubrr8 diff --git a/project/world_boss_system.gd b/project/world_boss_system.gd deleted file mode 100644 index 611f1546..00000000 --- a/project/world_boss_system.gd +++ /dev/null @@ -1,309 +0,0 @@ -# world_boss_system.gd —— Metin2 40250 野外四大世界 Boss 巡回战与专属宝箱 1:1 -# 对照 40250 服务端 char_battle.cpp, regen.cpp, mob_drop_item.txt, special_item_group.txt -class_name WorldBossSystem -extends RefCounted - -signal boss_spawned(boss_vnum: int, boss_name: String, position: Vector3) -signal boss_hp_changed(current_hp: int, max_hp: int) -signal minions_summoned(boss_vnum: int, minion_vnum: int, count: int) -signal boss_enraged(boss_vnum: int) -signal boss_killed(boss_vnum: int, killer_pos: Vector3, drops: Array) - -const BOSS_TABLE: Dictionary = { - 1901: { - "name": "九尾妖狐", - "zone": "冰山", - "max_hp": 280000, - "base_atk": 850, - "base_def": 450, - "chest_vnum": 50006, - "chest_name": "九尾妖狐宝箱", - "minion_vnum": 2204, # 冰山雪怪 - "minion_count": 4, - "gold_min": 50000, - "gold_max": 100000 - }, - 2191: { - "name": "巨型沙漠龟", - "zone": "永壁沙漠", - "max_hp": 320000, - "base_atk": 920, - "base_def": 520, - "chest_vnum": 50007, - "chest_name": "沙漠龟宝箱", - "minion_vnum": 2108, # 沙漠强盗 - "minion_count": 4, - "gold_min": 60000, - "gold_max": 120000 - }, - 1304: { - "name": "黄虎之魂", - "zone": "黄龙神殿", - "max_hp": 350000, - "base_atk": 1050, - "base_def": 480, - "chest_vnum": 50008, - "chest_name": "黄虎宝箱", - "minion_vnum": 1302, # 狂暴神殿狂热信徒 - "minion_count": 4, - "gold_min": 70000, - "gold_max": 130000 - }, - 2206: { - "name": "烈焰领主", - "zone": "烈焰原野", - "max_hp": 400000, - "base_atk": 1200, - "base_def": 500, - "chest_vnum": 50009, - "chest_name": "烈焰宝箱", - "minion_vnum": 2202, # 烈焰幽灵 - "minion_count": 4, - "gold_min": 80000, - "gold_max": 150000 - } -} - -# 宝箱开启奖池 (40250 special_item_group.txt 1:1) -const CHEST_LOOT_TABLE: Dictionary = { - 50006: [ - {"vnum": 50513, "name": "灵魂之石", "count": 1, "weight": 30}, - {"vnum": 25040, "name": "祝福卷轴", "count": 1, "weight": 25}, - {"vnum": 169, "name": "战神之剑+9", "count": 1, "weight": 15}, - {"vnum": 11299, "name": "黑钢甲+9", "count": 1, "weight": 10}, - {"vnum": 71084, "name": "高级附魔石", "count": 3, "weight": 20} - ], - 50007: [ - {"vnum": 50513, "name": "灵魂之石", "count": 1, "weight": 30}, - {"vnum": 25040, "name": "祝福卷轴", "count": 1, "weight": 25}, - {"vnum": 3159, "name": "鬼牙刃+9", "count": 1, "weight": 15}, - {"vnum": 11499, "name": "黑风甲+9", "count": 1, "weight": 10}, - {"vnum": 71084, "name": "高级附魔石", "count": 3, "weight": 20} - ], - 50008: [ - {"vnum": 50513, "name": "灵魂之石", "count": 1, "weight": 30}, - {"vnum": 25040, "name": "祝福卷轴", "count": 1, "weight": 25}, - {"vnum": 1109, "name": "党人斧+9", "count": 1, "weight": 15}, - {"vnum": 11699, "name": "黑仙袍+9", "count": 1, "weight": 10}, - {"vnum": 71084, "name": "高级附魔石", "count": 3, "weight": 20} - ], - 50009: [ - {"vnum": 50513, "name": "灵魂之石", "count": 1, "weight": 30}, - {"vnum": 25040, "name": "祝福卷轴", "count": 1, "weight": 25}, - {"vnum": 189, "name": "天狼剑+9", "count": 1, "weight": 15}, - {"vnum": 11899, "name": "黑龙袍+9", "count": 1, "weight": 10}, - {"vnum": 71084, "name": "高级附魔石", "count": 3, "weight": 20} - ] -} - -# 当前活跃的世界 Boss -var active_boss: Dictionary = {} -var is_boss_alive: bool = false -var respawn_timers: Dictionary = {} # mob_vnum -> remaining_cooldown - -func spawn_boss(mob_vnum: int, pos: Vector3 = Vector3.ZERO) -> Dictionary: - if not BOSS_TABLE.has(mob_vnum): - return {"ok": false, "reason": "INVALID_BOSS_VNUM", "msg": "未知的世界Boss!"} - - var cfg: Dictionary = BOSS_TABLE[mob_vnum] - active_boss = { - "vnum": mob_vnum, - "name": cfg["name"], - "zone": cfg["zone"], - "max_hp": cfg["max_hp"], - "current_hp": cfg["max_hp"], - "base_atk": cfg["base_atk"], - "current_atk": cfg["base_atk"], - "base_def": cfg["base_def"], - "current_def": cfg["base_def"], - "chest_vnum": cfg["chest_vnum"], - "chest_name": cfg["chest_name"], - "minion_vnum": cfg["minion_vnum"], - "minion_count": cfg["minion_count"], - "position": pos, - "is_enraged": false, - "phase_75_triggered": false, - "phase_50_triggered": false, - "phase_25_triggered": false, - "summoned_minions": [] - } - is_boss_alive = true - - boss_spawned.emit(mob_vnum, cfg["name"], pos) - - return { - "ok": true, - "boss": active_boss.duplicate(), - "msg": "【世界Boss】%s 已在 %s 现身!" % [cfg["name"], cfg["zone"]] - } - -# 承受伤害与阶段机制判定 -func apply_damage(raw_dmg: int) -> Dictionary: - if not is_boss_alive or active_boss.is_empty(): - return {"ok": false, "reason": "NO_ACTIVE_BOSS", "msg": "当前没有存活的世界Boss!"} - - var def: int = active_boss["current_def"] - var net_dmg: int = max(1, raw_dmg - def) - active_boss["current_hp"] = max(0, active_boss["current_hp"] - net_dmg) - - var cur_hp: int = active_boss["current_hp"] - var max_hp: int = active_boss["max_hp"] - var hp_ratio: float = float(cur_hp) / float(max_hp) - - boss_hp_changed.emit(cur_hp, max_hp) - - var triggered_events: Array = [] - - # 1. 阶段小怪召唤机制 (75%, 50%, 25%) - if hp_ratio <= 0.75 and not active_boss["phase_75_triggered"]: - active_boss["phase_75_triggered"] = true - _summon_minions(active_boss["minion_vnum"], active_boss["minion_count"]) - triggered_events.append("PHASE_75_MINIONS") - - if hp_ratio <= 0.50 and not active_boss["phase_50_triggered"]: - active_boss["phase_50_triggered"] = true - _summon_minions(active_boss["minion_vnum"], active_boss["minion_count"]) - triggered_events.append("PHASE_50_MINIONS") - - if hp_ratio <= 0.25 and not active_boss["phase_25_triggered"]: - active_boss["phase_25_triggered"] = true - _summon_minions(active_boss["minion_vnum"], active_boss["minion_count"]) - triggered_events.append("PHASE_25_MINIONS") - - # 2. 残血狂暴机制 (<= 30% HP) - if hp_ratio <= 0.30 and not active_boss["is_enraged"]: - active_boss["is_enraged"] = true - active_boss["current_atk"] = int(active_boss["base_atk"] * 1.5) - boss_enraged.emit(active_boss["vnum"]) - triggered_events.append("BOSS_ENRAGED") - - # 3. 击杀判定 - if cur_hp <= 0: - is_boss_alive = false - var drops: Array = _generate_boss_drops(active_boss["vnum"]) - var death_pos: Vector3 = active_boss["position"] - var boss_name: String = active_boss["name"] - var boss_vnum: int = active_boss["vnum"] - - boss_killed.emit(boss_vnum, death_pos, drops) - - return { - "ok": true, - "killed": true, - "net_dmg": net_dmg, - "remaining_hp": 0, - "drops": drops, - "triggered_events": triggered_events, - "msg": "【世界Boss】%s 已被击杀!掉落专属宝箱!" % boss_name - } - - return { - "ok": true, - "killed": false, - "net_dmg": net_dmg, - "remaining_hp": cur_hp, - "hp_ratio": hp_ratio, - "is_enraged": active_boss["is_enraged"], - "current_atk": active_boss["current_atk"], - "triggered_events": triggered_events - } - -# 召唤护卫小怪 -func _summon_minions(minion_vnum: int, count: int) -> void: - for i in range(count): - var offset := Vector3(randf_range(-3.0, 3.0), 0.0, randf_range(-3.0, 3.0)) - active_boss["summoned_minions"].append({ - "vnum": minion_vnum, - "pos": active_boss["position"] + offset - }) - minions_summoned.emit(active_boss["vnum"], minion_vnum, count) - -# 生成 Boss 击杀掉落物 -func _generate_boss_drops(mob_vnum: int) -> Array: - var cfg: Dictionary = BOSS_TABLE[mob_vnum] - var drops: Array = [] - - # 1. 必掉专属宝箱 - drops.append({ - "vnum": cfg["chest_vnum"], - "name": cfg["chest_name"], - "count": 1 - }) - - # 2. 必掉灵魂之石 (50513) - drops.append({ - "vnum": 50513, - "name": "灵魂之石", - "count": 1 - }) - - # 3. 必掉祝福卷轴 (25040) - drops.append({ - "vnum": 25040, - "name": "祝福卷轴", - "count": 1 - }) - - # 4. 大额金币掉落 (散落地上) - var gold_amount: int = randi_range(cfg["gold_min"], cfg["gold_max"]) - drops.append({ - "vnum": 1, # 金币 - "name": "金币", - "count": gold_amount - }) - - return drops - -# 开启世界 Boss 宝箱 -func open_boss_chest(chest_slot: int, inventory: Array) -> Dictionary: - if chest_slot < 0 or chest_slot >= inventory.size(): - return {"ok": false, "reason": "INVALID_SLOT", "msg": "无效的背包格子!"} - - var item = inventory[chest_slot] - if item == null: - return {"ok": false, "reason": "EMPTY_SLOT", "msg": "该格子上没有物品!"} - - var chest_vnum: int = int(item.get("vnum", 0)) - if not CHEST_LOOT_TABLE.has(chest_vnum): - return {"ok": false, "reason": "NOT_A_BOSS_CHEST", "msg": "这不是世界 Boss 宝箱!"} - - var pool: Array = CHEST_LOOT_TABLE[chest_vnum] - - # 权重抽取奖励 - var total_weight: int = 0 - for entry in pool: - total_weight += int(entry.get("weight", 1)) - - var roll := randi_range(1, total_weight) - var accumulated := 0 - var chosen_reward: Dictionary = {} - for entry in pool: - accumulated += int(entry.get("weight", 1)) - if roll <= accumulated: - chosen_reward = entry.duplicate() - break - - if chosen_reward.is_empty(): - chosen_reward = pool[0].duplicate() - - # 扣除宝箱 - inventory[chest_slot] = null - - # 放入抽中的奖励 - var stored_slot := -1 - for i in range(inventory.size()): - if inventory[i] == null: - inventory[i] = { - "vnum": chosen_reward["vnum"], - "count": chosen_reward.get("count", 1), - "name": chosen_reward["name"] - } - stored_slot = i - break - - return { - "ok": true, - "reward": chosen_reward, - "stored_slot": stored_slot, - "msg": "打开宝箱获得了【%s】!" % chosen_reward["name"] - } diff --git a/project/world_boss_system.gd.uid b/project/world_boss_system.gd.uid deleted file mode 100644 index 1014b760..00000000 --- a/project/world_boss_system.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cjlwrfcf41g68