1- from typing import Any , Dict , List
1+ from typing import Any , Dict , List , Optional
22
33from domains .gm .gm_service import GmService
44from domains .info .enemy_service import EnemyService
99 PlaySessionState ,
1010 RelationType ,
1111)
12+ from domains .play .dtos .player_dtos import ItemBase
1213from utils .logger import rule
1314
1415
16+ def _try_parse_int (value : Optional [str ]) -> Optional [int ]:
17+ if value is None :
18+ return None
19+ raw = str (value ).strip ()
20+ if not raw or not raw .isdigit ():
21+ return None
22+ return int (raw )
23+
24+
25+ def _is_combat_item_type (item_type : Optional [str ]) -> bool :
26+ if not item_type :
27+ return False
28+ normalized = str (item_type ).strip ().lower ()
29+ return normalized in {"무기" , "방어구" , "equipment" , "weapon" , "armor" }
30+
31+
32+ def _extract_effect_value (meta : Dict [str , Any ]) -> int :
33+ if not isinstance (meta , dict ):
34+ return 0
35+ for key in (
36+ "effect_value" ,
37+ "attack_bonus" ,
38+ "defense_bonus" ,
39+ "attack" ,
40+ "defense" ,
41+ "power" ,
42+ ):
43+ value = meta .get (key )
44+ if value is None :
45+ continue
46+ try :
47+ return int (value )
48+ except (TypeError , ValueError ):
49+ continue
50+ return 0
51+
52+
53+ async def _calculate_player_combat_item_effect (
54+ player_items : List [ItemBase ], item_service : ItemService , logs : List [str ]
55+ ) -> int :
56+ numeric_item_ids : List [int ] = []
57+ for item in player_items :
58+ parsed = _try_parse_int (item .item_id )
59+ if parsed is not None :
60+ numeric_item_ids .append (parsed )
61+
62+ effect_sum = 0
63+ resolved_numeric_ids : set [str ] = set ()
64+ if numeric_item_ids :
65+ items , _ = await item_service .get_items (
66+ item_ids = list (set (numeric_item_ids )), skip = 0 , limit = 100
67+ )
68+ for db_item in items :
69+ if _is_combat_item_type (db_item .get ("type" )):
70+ effect_sum += int (db_item .get ("effect_value" ) or 0 )
71+ db_item_id = db_item .get ("item_id" )
72+ if db_item_id is not None :
73+ resolved_numeric_ids .add (str (db_item_id ))
74+
75+ # scenario_item_id(문자열) 기반 인벤토리도 처리되도록 로컬 메타를 fallback으로 사용.
76+ for item in player_items :
77+ if item .item_id in resolved_numeric_ids :
78+ continue
79+ if not _is_combat_item_type (item .item_type ):
80+ continue
81+ effect_sum += _extract_effect_value (item .meta )
82+
83+ if effect_sum > 0 :
84+ logs .append (f"전투 보정치(아이템): { effect_sum } " )
85+ rule (f"전투 보정치(아이템): { effect_sum } " )
86+
87+ return effect_sum
88+
89+
1590async def combat_node (state : PlaySessionState ) -> Dict [str , Any ]:
1691 """
1792 전투 페이즈 로직을 처리합니다.
@@ -30,6 +105,7 @@ async def combat_node(state: PlaySessionState) -> Dict[str, Any]:
30105
31106 if not player_id or not player_state :
32107 logs .append ("전투 페이즈: 플레이어 정보를 찾을 수 없습니다." )
108+ rule ("전투 페이즈: 플레이어 정보를 찾을 수 없습니다." )
33109 return {
34110 "diffs" : state .diffs ,
35111 "relations" : state .relations ,
@@ -39,7 +115,7 @@ async def combat_node(state: PlaySessionState) -> Dict[str, Any]:
39115
40116 # 적 상세정보 조회
41117 combat_enemies_info = []
42- enemy_state_ids = {
118+ hostile_enemy_state_ids = {
43119 (
44120 rel .effect_entity_id
45121 if rel .cause_entity_id == player_id
@@ -50,9 +126,22 @@ async def combat_node(state: PlaySessionState) -> Dict[str, Any]:
50126 and (rel .cause_entity_id == player_id or rel .effect_entity_id == player_id )
51127 }
52128
129+ # Fallback: relation 정보가 비어도 요청에 포함된 enemy 엔티티를 전투 대상으로 사용.
130+ enemy_state_ids = (
131+ hostile_enemy_state_ids
132+ if hostile_enemy_state_ids
133+ else {e .state_entity_id for e in enemies }
134+ )
135+ if not hostile_enemy_state_ids and enemies :
136+ no_enemies_log = (
137+ "적대 관계 정보가 없어 요청 enemy 목록 전체를 전투 대상으로 사용합니다."
138+ )
139+ logs .append (no_enemies_log )
140+ rule (no_enemies_log )
141+
53142 enemy_id_map = {
54143 e .state_entity_id : e .entity_id
55- for e in entities_in_request
144+ for e in enemies
56145 if e .state_entity_id in enemy_state_ids and e .entity_id is not None
57146 }
58147
@@ -64,16 +153,26 @@ async def combat_node(state: PlaySessionState) -> Dict[str, Any]:
64153
65154 for state_id in enemy_state_ids :
66155 rdb_id = enemy_id_map .get (state_id )
67- if rdb_id :
156+ base_difficulty = None
157+ if rdb_id is not None :
68158 enemy_data = enemy_details_map .get (rdb_id )
69- if enemy_data and "base_difficulty" in enemy_data :
70- combat_enemies_info .append (
71- {
72- "rdb_id" : rdb_id ,
73- "state_id" : state_id ,
74- "base_difficulty" : enemy_data ["base_difficulty" ],
75- }
76- )
159+ if enemy_data and enemy_data .get ("base_difficulty" ) is not None :
160+ base_difficulty = int (enemy_data ["base_difficulty" ])
161+
162+ if base_difficulty is None :
163+ # 상세 조회 실패 시에도 전투 처리를 중단하지 않기 위한 기본 난이도
164+ base_difficulty = 6
165+ no_enemy_detail_log = f"적 상세정보 누락(state_id={ state_id } , rdb_id={ rdb_id } )으로 기본 난이도 6을 사용합니다."
166+ logs .append (no_enemy_detail_log )
167+ rule (no_enemy_detail_log )
168+
169+ combat_enemies_info .append (
170+ {
171+ "rdb_id" : rdb_id ,
172+ "state_id" : state_id ,
173+ "base_difficulty" : base_difficulty ,
174+ }
175+ )
77176
78177 # 플레이어 전투력 계산
79178 combat_logs : List [str ] = []
@@ -82,13 +181,9 @@ async def combat_node(state: PlaySessionState) -> Dict[str, Any]:
82181 rule (dice_result_log )
83182 combat_logs .append (dice_result_log )
84183
85- combat_items_effect = 0
86- item_ids = [int (item .item_id ) for item in player_state .player .items ]
87- if len (item_ids ) > 0 :
88- items , _ = await item_service .get_items (item_ids = item_ids , skip = 0 , limit = 100 )
89- combat_items_effect = sum (
90- item ["effect_value" ] for item in items if item ["type" ] in ("무기" , "방어구" )
91- )
184+ combat_items_effect = await _calculate_player_combat_item_effect (
185+ player_state .player .items , item_service , logs
186+ )
92187
93188 ability_score = 2 # Hardcoded
94189 player_combat_power = combat_items_effect + ability_score + dice .total
@@ -125,6 +220,7 @@ async def combat_node(state: PlaySessionState) -> Dict[str, Any]:
125220 )
126221 diffs .append (new_diff )
127222 logs .append (f"전투 승리! 적에게 { power_gap } 의 데미지를 입혔습니다." )
223+ rule (f"전투 승리! 적에게 { power_gap } 의 데미지를 입혔습니다." )
128224
129225 elif power_gap < 0 :
130226 is_success = False
@@ -134,8 +230,10 @@ async def combat_node(state: PlaySessionState) -> Dict[str, Any]:
134230 )
135231 diffs .append (new_diff )
136232 logs .append (f"전투 패배... 플레이어가 { - power_gap } 의 데미지를 입었습니다." )
233+ rule (f"전투 패배... 플레이어가 { - power_gap } 의 데미지를 입었습니다." )
137234 else :
138235 logs .append ("막상막하의 대결이었습니다!" )
236+ rule ("막상막하의 대결이었습니다!" )
139237
140238 return {
141239 "diffs" : diffs ,
0 commit comments