empire-core 0.7.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- empire_core/__init__.py +36 -0
- empire_core/_archive/actions.py +511 -0
- empire_core/_archive/automation/__init__.py +24 -0
- empire_core/_archive/automation/alliance_tools.py +266 -0
- empire_core/_archive/automation/battle_reports.py +196 -0
- empire_core/_archive/automation/building_queue.py +242 -0
- empire_core/_archive/automation/defense_manager.py +124 -0
- empire_core/_archive/automation/map_scanner.py +370 -0
- empire_core/_archive/automation/multi_account.py +296 -0
- empire_core/_archive/automation/quest_automation.py +94 -0
- empire_core/_archive/automation/resource_manager.py +380 -0
- empire_core/_archive/automation/target_finder.py +153 -0
- empire_core/_archive/automation/tasks.py +224 -0
- empire_core/_archive/automation/unit_production.py +719 -0
- empire_core/_archive/cli.py +68 -0
- empire_core/_archive/client_async.py +469 -0
- empire_core/_archive/commands.py +201 -0
- empire_core/_archive/connection_async.py +228 -0
- empire_core/_archive/defense.py +156 -0
- empire_core/_archive/events/__init__.py +35 -0
- empire_core/_archive/events/base.py +153 -0
- empire_core/_archive/events/manager.py +85 -0
- empire_core/accounts.py +190 -0
- empire_core/client/__init__.py +0 -0
- empire_core/client/client.py +459 -0
- empire_core/config.py +87 -0
- empire_core/exceptions.py +42 -0
- empire_core/network/__init__.py +0 -0
- empire_core/network/connection.py +378 -0
- empire_core/protocol/__init__.py +0 -0
- empire_core/protocol/models/__init__.py +339 -0
- empire_core/protocol/models/alliance.py +186 -0
- empire_core/protocol/models/army.py +444 -0
- empire_core/protocol/models/attack.py +229 -0
- empire_core/protocol/models/auth.py +216 -0
- empire_core/protocol/models/base.py +403 -0
- empire_core/protocol/models/building.py +455 -0
- empire_core/protocol/models/castle.py +317 -0
- empire_core/protocol/models/chat.py +150 -0
- empire_core/protocol/models/defense.py +300 -0
- empire_core/protocol/models/map.py +269 -0
- empire_core/protocol/packet.py +104 -0
- empire_core/services/__init__.py +31 -0
- empire_core/services/alliance.py +222 -0
- empire_core/services/base.py +107 -0
- empire_core/services/castle.py +221 -0
- empire_core/state/__init__.py +0 -0
- empire_core/state/manager.py +398 -0
- empire_core/state/models.py +215 -0
- empire_core/state/quest_models.py +60 -0
- empire_core/state/report_models.py +115 -0
- empire_core/state/unit_models.py +75 -0
- empire_core/state/world_models.py +269 -0
- empire_core/storage/__init__.py +1 -0
- empire_core/storage/database.py +237 -0
- empire_core/utils/__init__.py +0 -0
- empire_core/utils/battle_sim.py +172 -0
- empire_core/utils/calculations.py +170 -0
- empire_core/utils/crypto.py +8 -0
- empire_core/utils/decorators.py +69 -0
- empire_core/utils/enums.py +111 -0
- empire_core/utils/helpers.py +252 -0
- empire_core/utils/response_awaiter.py +153 -0
- empire_core/utils/troops.py +93 -0
- empire_core-0.7.3.dist-info/METADATA +197 -0
- empire_core-0.7.3.dist-info/RECORD +67 -0
- empire_core-0.7.3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Defense management and automation for castles.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import TYPE_CHECKING, Dict, Optional
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from empire_core.client.client import EmpireClient
|
|
12
|
+
from empire_core.state.models import Castle
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class DefensePreset:
|
|
19
|
+
"""A predefined defense configuration."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
wall_left_tools: Dict[int, int] = field(default_factory=dict)
|
|
23
|
+
wall_middle_tools: Dict[int, int] = field(default_factory=dict)
|
|
24
|
+
wall_right_tools: Dict[int, int] = field(default_factory=dict)
|
|
25
|
+
wall_left_units_up: int = 0
|
|
26
|
+
wall_left_units_count: int = 0
|
|
27
|
+
wall_middle_units_up: int = 0
|
|
28
|
+
wall_middle_units_count: int = 0
|
|
29
|
+
wall_right_units_up: int = 0
|
|
30
|
+
wall_right_units_count: int = 0
|
|
31
|
+
moat_left_slots: Dict[int, int] = field(default_factory=dict)
|
|
32
|
+
moat_middle_slots: Dict[int, int] = field(default_factory=dict)
|
|
33
|
+
moat_right_slots: Dict[int, int] = field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DefenseManager:
|
|
37
|
+
"""
|
|
38
|
+
Manages defense configurations for castles.
|
|
39
|
+
|
|
40
|
+
Features:
|
|
41
|
+
- Apply predefined defense presets to castles.
|
|
42
|
+
- Monitor current defense and apply changes.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, client: "EmpireClient"):
|
|
46
|
+
self.client = client
|
|
47
|
+
self.presets: Dict[str, DefensePreset] = {}
|
|
48
|
+
|
|
49
|
+
def add_preset(self, preset: DefensePreset):
|
|
50
|
+
"""Add a defense preset."""
|
|
51
|
+
self.presets[preset.name] = preset
|
|
52
|
+
logger.info(f"Defense preset '{preset.name}' added.")
|
|
53
|
+
|
|
54
|
+
def get_preset(self, name: str) -> Optional[DefensePreset]:
|
|
55
|
+
"""Get a defense preset by name."""
|
|
56
|
+
return self.presets.get(name)
|
|
57
|
+
|
|
58
|
+
async def apply_defense_preset(self, castle_id: int, preset_name: str) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
Apply a named defense preset to a specific castle.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
castle_id: The ID of the castle to apply the preset to.
|
|
64
|
+
preset_name: The name of the defense preset.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
bool: True if the preset was successfully applied, False otherwise.
|
|
68
|
+
"""
|
|
69
|
+
preset = self.get_preset(preset_name)
|
|
70
|
+
if not preset:
|
|
71
|
+
logger.warning(f"Defense preset '{preset_name}' not found.")
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
castle: Optional["Castle"] = (
|
|
75
|
+
self.client.state.local_player.castles.get(castle_id) if self.client.state.local_player else None
|
|
76
|
+
)
|
|
77
|
+
if not castle:
|
|
78
|
+
logger.error(f"Castle {castle_id} not found in state.")
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
logger.info(f"Applying defense preset '{preset_name}' to castle {castle.name}...")
|
|
82
|
+
|
|
83
|
+
# Apply wall defense
|
|
84
|
+
wall_success = await self.client.defense.set_wall_defense(
|
|
85
|
+
castle_id=castle.id,
|
|
86
|
+
castle_x=castle.x,
|
|
87
|
+
castle_y=castle.y,
|
|
88
|
+
left_tools=preset.wall_left_tools,
|
|
89
|
+
middle_tools=preset.wall_middle_tools,
|
|
90
|
+
right_tools=preset.wall_right_tools,
|
|
91
|
+
left_units_up=preset.wall_left_units_up,
|
|
92
|
+
left_units_count=preset.wall_left_units_count,
|
|
93
|
+
middle_units_up=preset.wall_middle_units_up,
|
|
94
|
+
middle_units_count=preset.wall_middle_units_count,
|
|
95
|
+
right_units_up=preset.wall_right_units_up,
|
|
96
|
+
right_units_count=preset.wall_right_units_count,
|
|
97
|
+
wait_for_response=True, # Always wait for critical defense commands
|
|
98
|
+
)
|
|
99
|
+
if not wall_success:
|
|
100
|
+
logger.error(f"Failed to apply wall defense for castle {castle_id}.")
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
await asyncio.sleep(0.5) # Rate limit
|
|
104
|
+
|
|
105
|
+
# Apply moat defense
|
|
106
|
+
moat_success = await self.client.defense.set_moat_defense(
|
|
107
|
+
castle_id=castle.id,
|
|
108
|
+
castle_x=castle.x,
|
|
109
|
+
castle_y=castle.y,
|
|
110
|
+
left_slots=preset.moat_left_slots,
|
|
111
|
+
middle_slots=preset.moat_middle_slots,
|
|
112
|
+
right_slots=preset.moat_right_slots,
|
|
113
|
+
wait_for_response=True, # Always wait for critical defense commands
|
|
114
|
+
)
|
|
115
|
+
if not moat_success:
|
|
116
|
+
logger.error(f"Failed to apply moat defense for castle {castle_id}.")
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
logger.info(f"Defense preset '{preset_name}' successfully applied to castle {castle.name}.")
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
# TODO: Add methods to read current defense configuration for verification
|
|
123
|
+
# TODO: Add logic for 'auto-defense' based on incoming attacks or threats
|
|
124
|
+
# TODO: Implement dfk (keep defense) if protocol is found
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Map scanning and exploration automation with asynchronous database persistence.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple
|
|
10
|
+
|
|
11
|
+
from empire_core.events.base import PacketEvent
|
|
12
|
+
from empire_core.state.world_models import MapObject
|
|
13
|
+
from empire_core.utils.calculations import calculate_distance
|
|
14
|
+
from empire_core.utils.enums import MapObjectType
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from empire_core.client.client import EmpireClient
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# The step between chunks is exactly 13 tiles (index 0-12 inclusive)
|
|
22
|
+
MAP_STEP = 13
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ScanResult:
|
|
27
|
+
"""Result of a map scan."""
|
|
28
|
+
|
|
29
|
+
kingdom_id: int
|
|
30
|
+
chunks_scanned: int
|
|
31
|
+
objects_found: int
|
|
32
|
+
duration: float
|
|
33
|
+
targets_by_type: Dict[str, int] = field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ScanProgress:
|
|
38
|
+
"""Progress of an ongoing scan."""
|
|
39
|
+
|
|
40
|
+
total_chunks: int
|
|
41
|
+
completed_chunks: int
|
|
42
|
+
current_x: int
|
|
43
|
+
current_y: int
|
|
44
|
+
objects_found: int
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def percent_complete(self) -> float:
|
|
48
|
+
if self.total_chunks == 0:
|
|
49
|
+
return 0.0
|
|
50
|
+
return (self.completed_chunks / self.total_chunks) * 100
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MapScanner:
|
|
54
|
+
"""
|
|
55
|
+
Automated map scanning with intelligent chunk management and persistence.
|
|
56
|
+
|
|
57
|
+
Features:
|
|
58
|
+
- High-speed spiral scan pattern aligned to 13x13 grid
|
|
59
|
+
- Persistent chunk caching in SQLite database (async)
|
|
60
|
+
- Real-time database persistence via packet events
|
|
61
|
+
- Progress callbacks for UI updates
|
|
62
|
+
- Database-backed target discovery
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, client: "EmpireClient"):
|
|
66
|
+
self.client = client
|
|
67
|
+
self._scanned_chunks: Dict[int, Set[Tuple[int, int]]] = {} # kingdom -> chunk_coordinates
|
|
68
|
+
self._progress_callbacks: List[Callable[[ScanProgress], None]] = []
|
|
69
|
+
self._running = False
|
|
70
|
+
self._stop_event = asyncio.Event()
|
|
71
|
+
|
|
72
|
+
async def initialize(self):
|
|
73
|
+
"""Initialize scanner and load cache from database."""
|
|
74
|
+
try:
|
|
75
|
+
for kid in [0, 1, 2, 3, 4]:
|
|
76
|
+
chunks = await self.client.db.get_scanned_chunks(kid)
|
|
77
|
+
if chunks:
|
|
78
|
+
self._scanned_chunks[kid] = chunks
|
|
79
|
+
logger.debug(f"MapScanner: Loaded cache for {len(self._scanned_chunks)} kingdoms from DB")
|
|
80
|
+
|
|
81
|
+
# Register real-time persistence handler
|
|
82
|
+
self.client.events.listen(self._on_gaa_packet)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.warning(f"MapScanner: Failed to initialize: {e}")
|
|
85
|
+
|
|
86
|
+
async def _on_gaa_packet(self, event: PacketEvent):
|
|
87
|
+
"""Handle incoming map data and persist immediately."""
|
|
88
|
+
if event.command_id == "gaa":
|
|
89
|
+
try:
|
|
90
|
+
objects = list(self.client.state.map_objects.values())
|
|
91
|
+
if objects:
|
|
92
|
+
await self.client.db.save_map_objects(objects)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(f"MapScanner: Failed to persist map objects from packet: {e}")
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def map_objects(self) -> Dict[int, MapObject]:
|
|
98
|
+
"""Get all discovered map objects in current session memory."""
|
|
99
|
+
return self.client.state.map_objects
|
|
100
|
+
|
|
101
|
+
def get_scanned_chunk_count(self, kingdom_id: int = 0) -> int:
|
|
102
|
+
"""Get number of scanned chunks in a kingdom."""
|
|
103
|
+
return len(self._scanned_chunks.get(kingdom_id, set()))
|
|
104
|
+
|
|
105
|
+
def is_chunk_scanned(self, kingdom_id: int, x: int, y: int) -> bool:
|
|
106
|
+
"""Check if a coordinate belongs to a scanned chunk."""
|
|
107
|
+
chunk_x = x // MAP_STEP
|
|
108
|
+
chunk_y = y // MAP_STEP
|
|
109
|
+
return (chunk_x, chunk_y) in self._scanned_chunks.get(kingdom_id, set())
|
|
110
|
+
|
|
111
|
+
def on_progress(self, callback: Callable[[ScanProgress], None]):
|
|
112
|
+
"""Register callback for scan progress updates."""
|
|
113
|
+
self._progress_callbacks.append(callback)
|
|
114
|
+
|
|
115
|
+
async def scan_area(
|
|
116
|
+
self,
|
|
117
|
+
center_x: int,
|
|
118
|
+
center_y: int,
|
|
119
|
+
radius: int = 5,
|
|
120
|
+
kingdom_id: int = 0,
|
|
121
|
+
rescan: bool = False,
|
|
122
|
+
quit_on_empty: Optional[int] = None,
|
|
123
|
+
) -> ScanResult:
|
|
124
|
+
"""
|
|
125
|
+
Scan an area around a center point at maximum speed.
|
|
126
|
+
"""
|
|
127
|
+
start_time = time.time()
|
|
128
|
+
objects_before = len(self.map_objects)
|
|
129
|
+
|
|
130
|
+
chunks = self._generate_spiral_pattern(center_x, center_y, radius)
|
|
131
|
+
total_chunks = len(chunks)
|
|
132
|
+
completed = 0
|
|
133
|
+
consecutive_empty = 0
|
|
134
|
+
|
|
135
|
+
if kingdom_id not in self._scanned_chunks:
|
|
136
|
+
self._scanned_chunks[kingdom_id] = set()
|
|
137
|
+
|
|
138
|
+
self._running = True
|
|
139
|
+
self._stop_event.clear()
|
|
140
|
+
|
|
141
|
+
for chunk_x, chunk_y in chunks:
|
|
142
|
+
if self._stop_event.is_set():
|
|
143
|
+
logger.info("Scan cancelled")
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
chunk_key = (chunk_x, chunk_y)
|
|
147
|
+
|
|
148
|
+
if not rescan and chunk_key in self._scanned_chunks[kingdom_id]:
|
|
149
|
+
completed += 1
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
# Calculate top-left tile coordinates for this chunk
|
|
153
|
+
tile_x = chunk_x * MAP_STEP
|
|
154
|
+
tile_y = chunk_y * MAP_STEP
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
# Ensure we are connected
|
|
158
|
+
if not self.client.connection.connected or not self.client.is_logged_in:
|
|
159
|
+
await self.client.wait_until_ready()
|
|
160
|
+
|
|
161
|
+
objs_before_chunk = len(self.map_objects)
|
|
162
|
+
|
|
163
|
+
# GAA command uses AX1, AY1, AX2, AY2.
|
|
164
|
+
# To match 13-tile step, we request size 12 chunks (step-1).
|
|
165
|
+
await self.client.get_map_chunk(kingdom_id, tile_x, tile_y)
|
|
166
|
+
|
|
167
|
+
# Small yield
|
|
168
|
+
await asyncio.sleep(0)
|
|
169
|
+
|
|
170
|
+
new_in_chunk = len(self.map_objects) - objs_before_chunk
|
|
171
|
+
if new_in_chunk > 0:
|
|
172
|
+
consecutive_empty = 0
|
|
173
|
+
else:
|
|
174
|
+
consecutive_empty += 1
|
|
175
|
+
|
|
176
|
+
# Update cache and DB for chunk
|
|
177
|
+
self._scanned_chunks[kingdom_id].add(chunk_key)
|
|
178
|
+
await self.client.db.mark_chunk_scanned(kingdom_id, chunk_x, chunk_y)
|
|
179
|
+
|
|
180
|
+
if quit_on_empty and consecutive_empty >= quit_on_empty:
|
|
181
|
+
logger.info(f"MapScanner: Stopping early after {consecutive_empty} empty chunks.")
|
|
182
|
+
break
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.warning(f"Failed to scan chunk ({chunk_x}, {chunk_y}): {e}")
|
|
186
|
+
|
|
187
|
+
completed += 1
|
|
188
|
+
|
|
189
|
+
# Notify progress
|
|
190
|
+
progress = ScanProgress(
|
|
191
|
+
total_chunks=total_chunks,
|
|
192
|
+
completed_chunks=completed,
|
|
193
|
+
current_x=tile_x,
|
|
194
|
+
current_y=tile_y,
|
|
195
|
+
objects_found=len(self.map_objects) - objects_before,
|
|
196
|
+
)
|
|
197
|
+
for callback in self._progress_callbacks:
|
|
198
|
+
try:
|
|
199
|
+
callback(progress)
|
|
200
|
+
except Exception as e:
|
|
201
|
+
logger.error(f"Progress callback error: {e}")
|
|
202
|
+
|
|
203
|
+
self._running = False
|
|
204
|
+
await asyncio.sleep(1.0)
|
|
205
|
+
|
|
206
|
+
duration = time.time() - start_time
|
|
207
|
+
objects_found = len(self.map_objects) - objects_before
|
|
208
|
+
summary = await self.get_scan_summary()
|
|
209
|
+
|
|
210
|
+
result = ScanResult(
|
|
211
|
+
kingdom_id=kingdom_id,
|
|
212
|
+
chunks_scanned=completed,
|
|
213
|
+
objects_found=objects_found,
|
|
214
|
+
duration=duration,
|
|
215
|
+
targets_by_type=summary["objects_by_type"],
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
logger.info(f"High-speed scan complete: {completed} chunks in {duration:.1f}s")
|
|
219
|
+
return result
|
|
220
|
+
|
|
221
|
+
async def scan_around_castles(
|
|
222
|
+
self, radius: int = 5, rescan: bool = False, quit_on_empty: Optional[int] = None
|
|
223
|
+
) -> List[ScanResult]:
|
|
224
|
+
"""Scan areas around all player castles."""
|
|
225
|
+
results: List[ScanResult] = []
|
|
226
|
+
player = self.client.state.local_player
|
|
227
|
+
if not player or not player.castles:
|
|
228
|
+
return results
|
|
229
|
+
|
|
230
|
+
for _castle_id, castle in player.castles.items():
|
|
231
|
+
result = await self.scan_area(
|
|
232
|
+
center_x=castle.x,
|
|
233
|
+
center_y=castle.y,
|
|
234
|
+
radius=radius,
|
|
235
|
+
kingdom_id=castle.KID,
|
|
236
|
+
rescan=rescan,
|
|
237
|
+
quit_on_empty=quit_on_empty,
|
|
238
|
+
)
|
|
239
|
+
results.append(result)
|
|
240
|
+
|
|
241
|
+
return results
|
|
242
|
+
|
|
243
|
+
async def find_nearby_targets(
|
|
244
|
+
self,
|
|
245
|
+
origin_x: int,
|
|
246
|
+
origin_y: int,
|
|
247
|
+
max_distance: float = 50.0,
|
|
248
|
+
target_types: Optional[List[MapObjectType]] = None,
|
|
249
|
+
max_level: int = 999,
|
|
250
|
+
exclude_player_ids: Optional[List[int]] = None,
|
|
251
|
+
use_db: bool = True,
|
|
252
|
+
) -> List[Tuple[Any, float]]:
|
|
253
|
+
"""
|
|
254
|
+
Find targets near a point, searching both memory and database.
|
|
255
|
+
"""
|
|
256
|
+
targets_dict: Dict[int, Tuple[Any, float]] = {}
|
|
257
|
+
exclude_ids = set(exclude_player_ids or [])
|
|
258
|
+
|
|
259
|
+
# 1. Search Database
|
|
260
|
+
if use_db:
|
|
261
|
+
db_types = [int(t) for t in target_types] if target_types else None
|
|
262
|
+
db_results = await self.client.db.find_targets(0, max_level=max_level, types=db_types)
|
|
263
|
+
for record in db_results:
|
|
264
|
+
if record.owner_id in exclude_ids:
|
|
265
|
+
continue
|
|
266
|
+
dist = calculate_distance(origin_x, origin_y, record.x, record.y)
|
|
267
|
+
if dist <= max_distance:
|
|
268
|
+
targets_dict[record.area_id] = (record, dist)
|
|
269
|
+
|
|
270
|
+
# 2. Search Memory
|
|
271
|
+
for obj in self.map_objects.values():
|
|
272
|
+
if target_types and obj.type not in target_types:
|
|
273
|
+
continue
|
|
274
|
+
if obj.level > max_level:
|
|
275
|
+
continue
|
|
276
|
+
if obj.owner_id in exclude_ids:
|
|
277
|
+
continue
|
|
278
|
+
dist = calculate_distance(origin_x, origin_y, obj.x, obj.y)
|
|
279
|
+
if dist <= max_distance:
|
|
280
|
+
targets_dict[obj.area_id] = (obj, dist)
|
|
281
|
+
|
|
282
|
+
results = list(targets_dict.values())
|
|
283
|
+
results.sort(key=lambda t: t[1])
|
|
284
|
+
return results
|
|
285
|
+
|
|
286
|
+
async def find_npc_targets(self, x: int, y: int, dist: float = 30.0) -> List[Tuple[Any, float]]:
|
|
287
|
+
"""Find permanent NPC targets (Robber Barons, etc)."""
|
|
288
|
+
npc_types = [MapObjectType.ROBBER_BARON_CASTLE, MapObjectType.DUNGEON, MapObjectType.BOSS_DUNGEON]
|
|
289
|
+
return await self.find_nearby_targets(x, y, max_distance=dist, target_types=npc_types)
|
|
290
|
+
|
|
291
|
+
async def find_player_targets(self, x: int, y: int, dist: float = 50.0) -> List[Tuple[Any, float]]:
|
|
292
|
+
"""Find player targets (Castles, Outposts)."""
|
|
293
|
+
player_types = [MapObjectType.CASTLE, MapObjectType.OUTPOST, MapObjectType.CAPITAL]
|
|
294
|
+
return await self.find_nearby_targets(x, y, max_distance=dist, target_types=player_types)
|
|
295
|
+
|
|
296
|
+
async def find_event_targets(self, x: int, y: int, dist: float = 40.0) -> List[Tuple[Any, float]]:
|
|
297
|
+
"""Find active event targets (Nomads, Samurai, Aliens)."""
|
|
298
|
+
event_types = [
|
|
299
|
+
MapObjectType.NOMAD_CAMP,
|
|
300
|
+
MapObjectType.SAMURAI_CAMP,
|
|
301
|
+
MapObjectType.ALIEN_CAMP,
|
|
302
|
+
MapObjectType.RED_ALIEN_CAMP,
|
|
303
|
+
]
|
|
304
|
+
return await self.find_nearby_targets(x, y, max_distance=dist, target_types=event_types)
|
|
305
|
+
|
|
306
|
+
async def get_scan_summary(self) -> Dict[str, Any]:
|
|
307
|
+
"""Get summary including database stats."""
|
|
308
|
+
mem_summary = {kid: len(chunks) for kid, chunks in self._scanned_chunks.items()}
|
|
309
|
+
db_count = await self.client.db.get_object_count()
|
|
310
|
+
db_types = await self.client.db.get_object_counts_by_type()
|
|
311
|
+
|
|
312
|
+
readable_types = {}
|
|
313
|
+
category_counts = {"Player": 0, "NPC": 0, "Event": 0, "Resource": 0, "Other": 0}
|
|
314
|
+
|
|
315
|
+
for type_id, count in db_types.items():
|
|
316
|
+
try:
|
|
317
|
+
enum_type = MapObjectType(type_id)
|
|
318
|
+
name = enum_type.name
|
|
319
|
+
if enum_type.is_player:
|
|
320
|
+
category_counts["Player"] += count
|
|
321
|
+
elif enum_type.is_npc:
|
|
322
|
+
category_counts["NPC"] += count
|
|
323
|
+
elif enum_type.is_event:
|
|
324
|
+
category_counts["Event"] += count
|
|
325
|
+
elif enum_type.is_resource:
|
|
326
|
+
category_counts["Resource"] += count
|
|
327
|
+
else:
|
|
328
|
+
category_counts["Other"] += count
|
|
329
|
+
except ValueError:
|
|
330
|
+
name = f"Unknown({type_id})"
|
|
331
|
+
category_counts["Other"] += count
|
|
332
|
+
|
|
333
|
+
readable_types[name] = count
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
"memory_objects": len(self.map_objects),
|
|
337
|
+
"database_objects": db_count,
|
|
338
|
+
"objects_by_type": readable_types,
|
|
339
|
+
"objects_by_category": category_counts,
|
|
340
|
+
"chunks_by_kingdom": mem_summary,
|
|
341
|
+
"total_chunks_scanned": sum(mem_summary.values()),
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
def _generate_spiral_pattern(self, center_x: int, center_y: int, radius: int) -> List[Tuple[int, int]]:
|
|
345
|
+
"""Generate spiral scan pattern from center outward, ensuring coordinates are positive."""
|
|
346
|
+
# Using MAP_STEP (13) for chunk grid
|
|
347
|
+
center_chunk_x = max(0, center_x // MAP_STEP)
|
|
348
|
+
center_chunk_y = max(0, center_y // MAP_STEP)
|
|
349
|
+
|
|
350
|
+
chunks = [(center_chunk_x, center_chunk_y)]
|
|
351
|
+
|
|
352
|
+
for r in range(1, radius + 1):
|
|
353
|
+
# Top row
|
|
354
|
+
for x in range(center_chunk_x - r, center_chunk_x + r + 1):
|
|
355
|
+
if x >= 0 and (center_chunk_y - r) >= 0:
|
|
356
|
+
chunks.append((x, center_chunk_y - r))
|
|
357
|
+
# Bottom row
|
|
358
|
+
for x in range(center_chunk_x - r, center_chunk_x + r + 1):
|
|
359
|
+
if x >= 0:
|
|
360
|
+
chunks.append((x, center_chunk_y + r))
|
|
361
|
+
# Left column
|
|
362
|
+
for y in range(center_chunk_y - r + 1, center_chunk_y + r):
|
|
363
|
+
if (center_chunk_x - r) >= 0 and y >= 0:
|
|
364
|
+
chunks.append((center_chunk_x - r, y))
|
|
365
|
+
# Right column
|
|
366
|
+
for y in range(center_chunk_y - r + 1, center_chunk_y + r):
|
|
367
|
+
if y >= 0:
|
|
368
|
+
chunks.append((center_chunk_x + r, y))
|
|
369
|
+
|
|
370
|
+
return chunks
|