pyscreeps-arena 0.5.7b0__py3-none-any.whl → 0.5.7.2__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.
- pyscreeps_arena/__init__.py +35 -3
- pyscreeps_arena/core/const.py +1 -1
- pyscreeps_arena/project.7z +0 -0
- pyscreeps_arena/ui/__init__.py +3 -1
- pyscreeps_arena/ui/map_render.py +705 -0
- pyscreeps_arena/ui/mapviewer.py +14 -0
- pyscreeps_arena/ui/qcreeplogic/qcreeplogic.py +82 -21
- pyscreeps_arena/ui/qmapv/__init__.py +3 -0
- pyscreeps_arena/ui/qmapv/qcinfo.py +567 -0
- pyscreeps_arena/ui/qmapv/qco.py +441 -0
- pyscreeps_arena/ui/qmapv/qmapv.py +728 -0
- pyscreeps_arena/ui/qmapv/test_array_drag.py +191 -0
- pyscreeps_arena/ui/qmapv/test_drag.py +107 -0
- pyscreeps_arena/ui/qmapv/test_qcinfo.py +169 -0
- pyscreeps_arena/ui/qmapv/test_qco_drag.py +7 -0
- pyscreeps_arena/ui/qmapv/test_qmapv.py +224 -0
- pyscreeps_arena/ui/qmapv/test_simple_array.py +303 -0
- {pyscreeps_arena-0.5.7b0.dist-info → pyscreeps_arena-0.5.7.2.dist-info}/METADATA +1 -1
- pyscreeps_arena-0.5.7.2.dist-info/RECORD +40 -0
- pyscreeps_arena-0.5.7b0.dist-info/RECORD +0 -28
- {pyscreeps_arena-0.5.7b0.dist-info → pyscreeps_arena-0.5.7.2.dist-info}/WHEEL +0 -0
- {pyscreeps_arena-0.5.7b0.dist-info → pyscreeps_arena-0.5.7.2.dist-info}/entry_points.txt +0 -0
- {pyscreeps_arena-0.5.7b0.dist-info → pyscreeps_arena-0.5.7.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import math
|
|
4
|
+
import importlib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageOps
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MapRender:
|
|
10
|
+
def __init__(self, map_path, cell_size=32):
|
|
11
|
+
self._map_path = map_path
|
|
12
|
+
self._cell_size = cell_size
|
|
13
|
+
self._map_data = None
|
|
14
|
+
|
|
15
|
+
# Updated terrain colors
|
|
16
|
+
self._terrain_colors = {
|
|
17
|
+
'X': (139, 126, 102), # terrain-wall #8B7E66
|
|
18
|
+
'2': (144, 238, 144), # plain #90EE90
|
|
19
|
+
'A': (205, 190, 112), # swamp #CDBE70
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Object rendering configurations with custom scale ratios
|
|
23
|
+
self._object_styles = {
|
|
24
|
+
'StructureSpawn': {
|
|
25
|
+
'shape': 'circle',
|
|
26
|
+
'fill': (255, 255, 0), # yellow
|
|
27
|
+
'outline': (200, 200, 0),
|
|
28
|
+
'size': 1.1 # 1.1x scale as requested
|
|
29
|
+
},
|
|
30
|
+
'StructureWall': {
|
|
31
|
+
'shape': 'rounded_square',
|
|
32
|
+
'fill': (128, 128, 128), # gray
|
|
33
|
+
'outline': (100, 100, 100),
|
|
34
|
+
'size': 0.8,
|
|
35
|
+
'radius': 0.2
|
|
36
|
+
},
|
|
37
|
+
'StructureContainer': {
|
|
38
|
+
'shape': 'square',
|
|
39
|
+
'fill': (254, 238, 0), # #FEEE00
|
|
40
|
+
'outline': (255, 255, 255), # white border
|
|
41
|
+
'outline_width': 2,
|
|
42
|
+
'size': 0.8
|
|
43
|
+
},
|
|
44
|
+
'StructureTower': {
|
|
45
|
+
'shape': 'tower', # circle + triangle
|
|
46
|
+
'circle_fill': (100, 100, 100),
|
|
47
|
+
'triangle_fill': (200, 200, 200),
|
|
48
|
+
'size': 1.1 # 1.1x scale as requested
|
|
49
|
+
},
|
|
50
|
+
'StructureRampart': {
|
|
51
|
+
'shape': 'ring',
|
|
52
|
+
'fill': None,
|
|
53
|
+
'outline': (150, 150, 150),
|
|
54
|
+
'outline_width': 3,
|
|
55
|
+
'size': 0.85
|
|
56
|
+
},
|
|
57
|
+
'StructureExtension': {
|
|
58
|
+
'shape': 'circle',
|
|
59
|
+
'fill': (200, 200, 200),
|
|
60
|
+
'outline': (150, 150, 150),
|
|
61
|
+
'size': 0.6
|
|
62
|
+
},
|
|
63
|
+
'StructureRoad': {
|
|
64
|
+
'shape': 'line',
|
|
65
|
+
'fill': (100, 100, 100),
|
|
66
|
+
'width': 3,
|
|
67
|
+
'size': 0.8
|
|
68
|
+
},
|
|
69
|
+
'Flag': {
|
|
70
|
+
'shape': 'flag', # pole + flag shape
|
|
71
|
+
'pole_color': (139, 69, 19), # brown
|
|
72
|
+
'flag_color': (255, 0, 0), # red
|
|
73
|
+
'size': 0.9
|
|
74
|
+
},
|
|
75
|
+
'Source': {
|
|
76
|
+
'shape': 'circle',
|
|
77
|
+
'fill': (255, 215, 0), # gold
|
|
78
|
+
'outline': (200, 170, 0),
|
|
79
|
+
'size': 0.7
|
|
80
|
+
},
|
|
81
|
+
'Creep': {
|
|
82
|
+
'shape': 'circle',
|
|
83
|
+
'fill': (0, 255, 255), # cyan
|
|
84
|
+
'outline': (0, 200, 200),
|
|
85
|
+
'size': 0.6
|
|
86
|
+
},
|
|
87
|
+
'Resource': {
|
|
88
|
+
'shape': 'diamond',
|
|
89
|
+
'fill': (255, 165, 0), # orange
|
|
90
|
+
'outline': (200, 130, 0),
|
|
91
|
+
'size': 0.5
|
|
92
|
+
},
|
|
93
|
+
'ConstructionSite': {
|
|
94
|
+
'shape': 'square',
|
|
95
|
+
'fill': (255, 165, 0), # orange
|
|
96
|
+
'outline': (200, 130, 0),
|
|
97
|
+
'size': 0.7
|
|
98
|
+
},
|
|
99
|
+
'Portal': {
|
|
100
|
+
'shape': 'ring',
|
|
101
|
+
'fill': None,
|
|
102
|
+
'outline': (128, 0, 128), # purple
|
|
103
|
+
'outline_width': 4,
|
|
104
|
+
'size': 0.8
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Resource management: Load local images
|
|
109
|
+
self._resources = {}
|
|
110
|
+
self._load_resources()
|
|
111
|
+
|
|
112
|
+
def _load_resources(self):
|
|
113
|
+
"""Load local resources from resources directory."""
|
|
114
|
+
# Define resources directory path
|
|
115
|
+
resources_dir = Path(__file__).parent.parent / 'resources'
|
|
116
|
+
|
|
117
|
+
if not resources_dir.exists():
|
|
118
|
+
print(f"[DEBUG] Resources directory not found: {resources_dir}")
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
# Scan all Python files in resources directory
|
|
122
|
+
for resource_file in resources_dir.glob('*.py'):
|
|
123
|
+
if resource_file.name.startswith('_'):
|
|
124
|
+
continue # Skip __init__.py and other hidden files
|
|
125
|
+
|
|
126
|
+
# Get resource name from filename (remove .py extension)
|
|
127
|
+
resource_name = resource_file.stem
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
# Import the module dynamically
|
|
131
|
+
module_name = f"pyscreeps_arena.resources.{resource_name}"
|
|
132
|
+
module = importlib.import_module(module_name)
|
|
133
|
+
|
|
134
|
+
# Check if toImage method exists
|
|
135
|
+
if hasattr(module, 'toImage'):
|
|
136
|
+
# Load the toImage method
|
|
137
|
+
self._resources[resource_name] = module.toImage
|
|
138
|
+
print(f"[DEBUG] Loaded resource: {resource_name}")
|
|
139
|
+
except Exception as e:
|
|
140
|
+
print(f"[DEBUG] Failed to load resource {resource_name}: {e}")
|
|
141
|
+
|
|
142
|
+
def _load_map(self):
|
|
143
|
+
"""Load map data from JSON file."""
|
|
144
|
+
try:
|
|
145
|
+
with open(self._map_path, 'r', encoding='utf-8') as f:
|
|
146
|
+
self._map_data = json.load(f)
|
|
147
|
+
print(f"[DEBUG] Loaded map data with {len(self._map_data['map'])} rows") # 调试输出:查看地图行数
|
|
148
|
+
except FileNotFoundError:
|
|
149
|
+
raise FileNotFoundError(f"Map file not found: {self._map_path}")
|
|
150
|
+
except json.JSONDecodeError as e:
|
|
151
|
+
raise ValueError(f"Invalid JSON in map file: {e}")
|
|
152
|
+
|
|
153
|
+
def _get_map_dimensions(self):
|
|
154
|
+
"""Get map width and height."""
|
|
155
|
+
if not self._map_data:
|
|
156
|
+
return 0, 0
|
|
157
|
+
|
|
158
|
+
height = len(self._map_data['map'])
|
|
159
|
+
width = len(self._map_data['map'][0]) if height > 0 else 0
|
|
160
|
+
print(f"[DEBUG] Map dimensions: {width}x{height}") # 调试输出:查看地图尺寸
|
|
161
|
+
return width, height
|
|
162
|
+
|
|
163
|
+
def _draw_terrain(self, draw, img_width, img_height):
|
|
164
|
+
"""Draw terrain tiles with inner borders for continuous regions."""
|
|
165
|
+
map_rows = self._map_data['map']
|
|
166
|
+
height = len(map_rows)
|
|
167
|
+
width = len(map_rows[0]) if height > 0 else 0
|
|
168
|
+
|
|
169
|
+
# First draw all terrain tiles
|
|
170
|
+
for y, row in enumerate(map_rows):
|
|
171
|
+
for x, char in enumerate(row):
|
|
172
|
+
if char in self._terrain_colors:
|
|
173
|
+
color = self._terrain_colors[char]
|
|
174
|
+
left = x * self._cell_size
|
|
175
|
+
top = y * self._cell_size
|
|
176
|
+
right = left + self._cell_size
|
|
177
|
+
bottom = top + self._cell_size
|
|
178
|
+
|
|
179
|
+
draw.rectangle([left, top, right, bottom], fill=color)
|
|
180
|
+
|
|
181
|
+
# Then draw inner borders for continuous terrain regions
|
|
182
|
+
self._draw_terrain_inner_borders(draw, map_rows, width, height)
|
|
183
|
+
|
|
184
|
+
def _draw_terrain_inner_borders(self, draw, map_rows, width, height):
|
|
185
|
+
"""Draw inner borders for continuous terrain regions."""
|
|
186
|
+
# Mark visited cells to avoid redundant processing
|
|
187
|
+
visited = [[False for _ in range(width)] for _ in range(height)]
|
|
188
|
+
|
|
189
|
+
# Iterate through all cells
|
|
190
|
+
for y in range(height):
|
|
191
|
+
for x in range(width):
|
|
192
|
+
if not visited[y][x]:
|
|
193
|
+
# Find continuous region
|
|
194
|
+
region = self._find_continuous_region(map_rows, x, y, visited)
|
|
195
|
+
if len(region) > 1: # Only draw borders for regions larger than 1 cell
|
|
196
|
+
self._draw_region_inner_borders(draw, region, map_rows[y][x])
|
|
197
|
+
|
|
198
|
+
def _find_continuous_region(self, map_rows, start_x, start_y, visited):
|
|
199
|
+
"""Find continuous terrain region starting from (start_x, start_y)."""
|
|
200
|
+
terrain_type = map_rows[start_y][start_x]
|
|
201
|
+
region = []
|
|
202
|
+
queue = [(start_x, start_y)]
|
|
203
|
+
visited[start_y][start_x] = True
|
|
204
|
+
|
|
205
|
+
# Directions: up, down, left, right (no diagonals)
|
|
206
|
+
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
|
|
207
|
+
|
|
208
|
+
while queue:
|
|
209
|
+
x, y = queue.pop(0)
|
|
210
|
+
region.append((x, y))
|
|
211
|
+
|
|
212
|
+
for dx, dy in directions:
|
|
213
|
+
nx, ny = x + dx, y + dy
|
|
214
|
+
# Check boundaries and if cell is same terrain type and not visited
|
|
215
|
+
if 0 <= nx < len(map_rows[0]) and 0 <= ny < len(map_rows):
|
|
216
|
+
if not visited[ny][nx] and map_rows[ny][nx] == terrain_type:
|
|
217
|
+
visited[ny][nx] = True
|
|
218
|
+
queue.append((nx, ny))
|
|
219
|
+
|
|
220
|
+
return region
|
|
221
|
+
|
|
222
|
+
def _draw_region_inner_borders(self, draw, region, terrain_type):
|
|
223
|
+
"""Draw inner borders for a continuous region."""
|
|
224
|
+
if terrain_type not in self._terrain_colors:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
# Calculate border color (50% opacity, 70% self color + 30% black)
|
|
228
|
+
base_color = self._terrain_colors[terrain_type]
|
|
229
|
+
# Mix 70% self color with 30% black
|
|
230
|
+
r = max(0, int(base_color[0] * 0.7))
|
|
231
|
+
g = max(0, int(base_color[1] * 0.7))
|
|
232
|
+
b = max(0, int(base_color[2] * 0.7))
|
|
233
|
+
border_color = (r, g, b, 128) # 128 = 50% opacity
|
|
234
|
+
|
|
235
|
+
# Fixed border width of 1 as requested
|
|
236
|
+
border_width = 1
|
|
237
|
+
|
|
238
|
+
# Create a set for quick lookup
|
|
239
|
+
region_set = set(region)
|
|
240
|
+
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
|
|
241
|
+
|
|
242
|
+
# Iterate through each cell in region and check adjacent cells
|
|
243
|
+
for x, y in region:
|
|
244
|
+
# Check all four directions
|
|
245
|
+
for dx, dy in directions:
|
|
246
|
+
nx, ny = x + dx, y + dy
|
|
247
|
+
# If neighbor is not in region, draw border on that side
|
|
248
|
+
if (nx, ny) not in region_set:
|
|
249
|
+
# Calculate border coordinates
|
|
250
|
+
left = x * self._cell_size
|
|
251
|
+
top = y * self._cell_size
|
|
252
|
+
right = left + self._cell_size
|
|
253
|
+
bottom = top + self._cell_size
|
|
254
|
+
|
|
255
|
+
# Draw border based on direction
|
|
256
|
+
if dx == 0 and dy == -1: # Top border
|
|
257
|
+
draw.rectangle([left, top, right, top + border_width], fill=border_color)
|
|
258
|
+
elif dx == 0 and dy == 1: # Bottom border
|
|
259
|
+
draw.rectangle([left, bottom - border_width, right, bottom], fill=border_color)
|
|
260
|
+
elif dx == -1 and dy == 0: # Left border
|
|
261
|
+
draw.rectangle([left, top, left + border_width, bottom], fill=border_color)
|
|
262
|
+
elif dx == 1 and dy == 0: # Right border
|
|
263
|
+
draw.rectangle([right - border_width, top, right, bottom], fill=border_color)
|
|
264
|
+
|
|
265
|
+
def _draw_shape(self, draw, x, y, style, overlay_color=None):
|
|
266
|
+
"""Draw a specific shape based on style configuration."""
|
|
267
|
+
center_x = x * self._cell_size + self._cell_size // 2
|
|
268
|
+
center_y = y * self._cell_size + self._cell_size // 2
|
|
269
|
+
size = int(self._cell_size * style.get('size', 0.7))
|
|
270
|
+
|
|
271
|
+
shape = style.get('shape', 'circle')
|
|
272
|
+
|
|
273
|
+
if shape == 'circle':
|
|
274
|
+
radius = size // 2
|
|
275
|
+
fill_color = style.get('fill')
|
|
276
|
+
outline_color = style.get('outline')
|
|
277
|
+
|
|
278
|
+
# Draw main circle
|
|
279
|
+
draw.ellipse([
|
|
280
|
+
center_x - radius, center_y - radius,
|
|
281
|
+
center_x + radius, center_y + radius
|
|
282
|
+
], fill=fill_color, outline=outline_color)
|
|
283
|
+
|
|
284
|
+
# Add highlight and shadow for质感效果
|
|
285
|
+
if fill_color:
|
|
286
|
+
# Top-left highlight
|
|
287
|
+
highlight_radius = radius // 2
|
|
288
|
+
highlight_offset = radius // 3
|
|
289
|
+
highlight_color = tuple(min(255, c + 60) for c in fill_color)
|
|
290
|
+
draw.ellipse([
|
|
291
|
+
center_x - radius + highlight_offset, center_y - radius + highlight_offset,
|
|
292
|
+
center_x - radius + highlight_offset + highlight_radius,
|
|
293
|
+
center_y - radius + highlight_offset + highlight_radius
|
|
294
|
+
], fill=highlight_color)
|
|
295
|
+
|
|
296
|
+
# Bottom-right shadow
|
|
297
|
+
shadow_radius = radius // 2
|
|
298
|
+
shadow_offset = radius // 3
|
|
299
|
+
shadow_color = tuple(max(0, c - 40) for c in fill_color)
|
|
300
|
+
draw.ellipse([
|
|
301
|
+
center_x + radius - shadow_offset - shadow_radius,
|
|
302
|
+
center_y + radius - shadow_offset - shadow_radius,
|
|
303
|
+
center_x + radius - shadow_offset,
|
|
304
|
+
center_y + radius - shadow_offset
|
|
305
|
+
], fill=shadow_color)
|
|
306
|
+
|
|
307
|
+
elif shape == 'ring':
|
|
308
|
+
radius = size // 2
|
|
309
|
+
draw.ellipse([
|
|
310
|
+
center_x - radius, center_y - radius,
|
|
311
|
+
center_x + radius, center_y + radius
|
|
312
|
+
], outline=style.get('outline'), width=style.get('outline_width', 2))
|
|
313
|
+
|
|
314
|
+
elif shape == 'square':
|
|
315
|
+
half_size = size // 2
|
|
316
|
+
left = center_x - half_size
|
|
317
|
+
top = center_y - half_size
|
|
318
|
+
right = center_x + half_size
|
|
319
|
+
bottom = center_y + half_size
|
|
320
|
+
|
|
321
|
+
fill_color = style.get('fill')
|
|
322
|
+
outline_color = style.get('outline')
|
|
323
|
+
outline_width = style.get('outline_width', 1)
|
|
324
|
+
|
|
325
|
+
# Create gradient fill for质感效果
|
|
326
|
+
if fill_color:
|
|
327
|
+
# Create a small gradient image
|
|
328
|
+
gradient_img = Image.new('RGBA', (size, size))
|
|
329
|
+
gradient_draw = ImageDraw.Draw(gradient_img)
|
|
330
|
+
|
|
331
|
+
# Linear gradient from top-left to bottom-right
|
|
332
|
+
for i in range(size):
|
|
333
|
+
for j in range(size):
|
|
334
|
+
# Calculate gradient factor (0.0 to 1.0)
|
|
335
|
+
factor = (i + j) / (2 * size)
|
|
336
|
+
# Interpolate color between highlight and shadow
|
|
337
|
+
highlight = tuple(min(255, c + 40) for c in fill_color)
|
|
338
|
+
shadow = tuple(max(0, c - 30) for c in fill_color)
|
|
339
|
+
# Calculate interpolated color
|
|
340
|
+
r = int(highlight[0] * (1 - factor) + shadow[0] * factor)
|
|
341
|
+
g = int(highlight[1] * (1 - factor) + shadow[1] * factor)
|
|
342
|
+
b = int(highlight[2] * (1 - factor) + shadow[2] * factor)
|
|
343
|
+
gradient_draw.point((i, j), fill=(r, g, b, 255))
|
|
344
|
+
|
|
345
|
+
# Paste gradient onto main image
|
|
346
|
+
main_img = draw._image
|
|
347
|
+
main_img.paste(gradient_img, (left, top), gradient_img)
|
|
348
|
+
|
|
349
|
+
# Draw outline if needed
|
|
350
|
+
if outline_color:
|
|
351
|
+
draw.rectangle([left, top, right, bottom],
|
|
352
|
+
outline=outline_color, width=outline_width)
|
|
353
|
+
else:
|
|
354
|
+
# Fallback to basic square if no fill color
|
|
355
|
+
draw.rectangle([left, top, right, bottom],
|
|
356
|
+
fill=fill_color,
|
|
357
|
+
outline=outline_color,
|
|
358
|
+
width=outline_width)
|
|
359
|
+
|
|
360
|
+
elif shape == 'rounded_square':
|
|
361
|
+
half_size = size // 2
|
|
362
|
+
left = center_x - half_size
|
|
363
|
+
top = center_y - half_size
|
|
364
|
+
right = center_x + half_size
|
|
365
|
+
bottom = center_y + half_size
|
|
366
|
+
radius = int(size * style.get('radius', 0.2))
|
|
367
|
+
|
|
368
|
+
fill_color = style.get('fill')
|
|
369
|
+
outline_color = style.get('outline')
|
|
370
|
+
outline_width = style.get('outline_width', 1)
|
|
371
|
+
|
|
372
|
+
# Create gradient fill for质感效果
|
|
373
|
+
if fill_color:
|
|
374
|
+
# Create a small gradient image
|
|
375
|
+
gradient_img = Image.new('RGBA', (size, size))
|
|
376
|
+
gradient_draw = ImageDraw.Draw(gradient_img)
|
|
377
|
+
|
|
378
|
+
# Linear gradient from top-left to bottom-right
|
|
379
|
+
for i in range(size):
|
|
380
|
+
for j in range(size):
|
|
381
|
+
# Calculate gradient factor (0.0 to 1.0)
|
|
382
|
+
factor = (i + j) / (2 * size)
|
|
383
|
+
# Interpolate color between highlight and shadow
|
|
384
|
+
highlight = tuple(min(255, c + 40) for c in fill_color)
|
|
385
|
+
shadow = tuple(max(0, c - 30) for c in fill_color)
|
|
386
|
+
# Calculate interpolated color
|
|
387
|
+
r = int(highlight[0] * (1 - factor) + shadow[0] * factor)
|
|
388
|
+
g = int(highlight[1] * (1 - factor) + shadow[1] * factor)
|
|
389
|
+
b = int(highlight[2] * (1 - factor) + shadow[2] * factor)
|
|
390
|
+
gradient_draw.point((i, j), fill=(r, g, b, 255))
|
|
391
|
+
|
|
392
|
+
# Paste gradient onto main image
|
|
393
|
+
main_img = draw._image
|
|
394
|
+
main_img.paste(gradient_img, (left, top), gradient_img)
|
|
395
|
+
|
|
396
|
+
# Draw rounded rectangle outline
|
|
397
|
+
if outline_color:
|
|
398
|
+
draw.rounded_rectangle([left, top, right, bottom], radius,
|
|
399
|
+
outline=outline_color,
|
|
400
|
+
width=outline_width)
|
|
401
|
+
else:
|
|
402
|
+
# Fallback to basic rounded square if no fill color
|
|
403
|
+
draw.rounded_rectangle([left, top, right, bottom], radius,
|
|
404
|
+
fill=fill_color,
|
|
405
|
+
outline=outline_color,
|
|
406
|
+
width=outline_width)
|
|
407
|
+
|
|
408
|
+
elif shape == 'diamond':
|
|
409
|
+
radius = size // 2
|
|
410
|
+
points = [
|
|
411
|
+
(center_x, center_y - radius), # top
|
|
412
|
+
(center_x + radius, center_y), # right
|
|
413
|
+
(center_x, center_y + radius), # bottom
|
|
414
|
+
(center_x - radius, center_y) # left
|
|
415
|
+
]
|
|
416
|
+
draw.polygon(points, fill=style.get('fill'), outline=style.get('outline'))
|
|
417
|
+
|
|
418
|
+
elif shape == 'tower':
|
|
419
|
+
# Circle background
|
|
420
|
+
radius = size // 2
|
|
421
|
+
circle_fill = style.get('circle_fill')
|
|
422
|
+
triangle_fill = style.get('triangle_fill')
|
|
423
|
+
|
|
424
|
+
# Draw main circle
|
|
425
|
+
draw.ellipse([
|
|
426
|
+
center_x - radius, center_y - radius,
|
|
427
|
+
center_x + radius, center_y + radius
|
|
428
|
+
], fill=circle_fill)
|
|
429
|
+
|
|
430
|
+
# Add metal reflection to circle
|
|
431
|
+
if circle_fill:
|
|
432
|
+
reflection_width = radius // 3
|
|
433
|
+
reflection_color = tuple(min(255, c + 80) for c in circle_fill)
|
|
434
|
+
draw.rectangle([
|
|
435
|
+
center_x - reflection_width,
|
|
436
|
+
center_y - radius,
|
|
437
|
+
center_x + reflection_width,
|
|
438
|
+
center_y + radius
|
|
439
|
+
], fill=reflection_color)
|
|
440
|
+
|
|
441
|
+
# Triangle foreground with metal texture
|
|
442
|
+
tri_size = size // 3
|
|
443
|
+
triangle_points = [
|
|
444
|
+
(center_x, center_y - tri_size), # top
|
|
445
|
+
(center_x - tri_size, center_y + tri_size), # bottom left
|
|
446
|
+
(center_x + tri_size, center_y + tri_size) # bottom right
|
|
447
|
+
]
|
|
448
|
+
|
|
449
|
+
# Draw main triangle
|
|
450
|
+
draw.polygon(triangle_points, fill=triangle_fill)
|
|
451
|
+
|
|
452
|
+
# Add metal reflection to triangle
|
|
453
|
+
if triangle_fill:
|
|
454
|
+
# Vertical reflection line
|
|
455
|
+
reflection_color = tuple(min(255, c + 60) for c in triangle_fill)
|
|
456
|
+
draw.line([
|
|
457
|
+
center_x, center_y - tri_size,
|
|
458
|
+
center_x, center_y + tri_size
|
|
459
|
+
], fill=reflection_color, width=2)
|
|
460
|
+
|
|
461
|
+
# Horizontal reflection line
|
|
462
|
+
draw.line([
|
|
463
|
+
center_x - tri_size, center_y,
|
|
464
|
+
center_x + tri_size, center_y
|
|
465
|
+
], fill=reflection_color, width=2)
|
|
466
|
+
|
|
467
|
+
elif shape == 'flag':
|
|
468
|
+
# Pole
|
|
469
|
+
pole_x = center_x - size // 4
|
|
470
|
+
pole_top = center_y - size // 2
|
|
471
|
+
pole_bottom = center_y + size // 2
|
|
472
|
+
draw.line([pole_x, pole_top, pole_x, pole_bottom],
|
|
473
|
+
fill=style.get('pole_color'), width=2)
|
|
474
|
+
|
|
475
|
+
# Flag shape
|
|
476
|
+
flag_left = pole_x
|
|
477
|
+
flag_top = pole_top
|
|
478
|
+
flag_right = center_x + size // 2
|
|
479
|
+
flag_bottom = center_y - size // 4
|
|
480
|
+
flag_middle = center_x
|
|
481
|
+
|
|
482
|
+
flag_points = [
|
|
483
|
+
(flag_left, flag_top),
|
|
484
|
+
(flag_right, flag_top + (flag_bottom - flag_top) // 3),
|
|
485
|
+
(flag_middle, center_y),
|
|
486
|
+
(flag_right, flag_bottom - (flag_bottom - flag_top) // 3),
|
|
487
|
+
(flag_left, flag_bottom)
|
|
488
|
+
]
|
|
489
|
+
draw.polygon(flag_points, fill=style.get('flag_color'))
|
|
490
|
+
|
|
491
|
+
# Add flag texture
|
|
492
|
+
flag_color = style.get('flag_color')
|
|
493
|
+
if flag_color:
|
|
494
|
+
# Add diagonal stripes for texture
|
|
495
|
+
stripe_color = tuple(max(0, c - 30) for c in flag_color)
|
|
496
|
+
for i in range(5):
|
|
497
|
+
offset = i * 5
|
|
498
|
+
draw.line([
|
|
499
|
+
flag_left + offset, flag_top,
|
|
500
|
+
flag_right, flag_bottom - offset
|
|
501
|
+
], fill=stripe_color, width=1)
|
|
502
|
+
|
|
503
|
+
elif shape == 'line':
|
|
504
|
+
# For roads, we'll draw a simple line, but in a real implementation
|
|
505
|
+
# you might want to connect adjacent road pieces
|
|
506
|
+
left = center_x - size // 2
|
|
507
|
+
right = center_x + size // 2
|
|
508
|
+
top = center_y - style.get('width', 3) // 2
|
|
509
|
+
bottom = center_y + style.get('width', 3) // 2
|
|
510
|
+
draw.rectangle([left, top, right, bottom], fill=style.get('fill'))
|
|
511
|
+
|
|
512
|
+
# Apply overlay color if provided
|
|
513
|
+
if overlay_color:
|
|
514
|
+
# Calculate bounding box for overlay application
|
|
515
|
+
half_size = size // 2
|
|
516
|
+
bbox = [center_x - half_size, center_y - half_size, center_x + half_size, center_y + half_size]
|
|
517
|
+
|
|
518
|
+
# Get main image reference
|
|
519
|
+
main_img = draw._image
|
|
520
|
+
|
|
521
|
+
# Extract the region containing the shape
|
|
522
|
+
shape_region = main_img.crop(bbox)
|
|
523
|
+
|
|
524
|
+
# Convert to RGBA if not already
|
|
525
|
+
if shape_region.mode != 'RGBA':
|
|
526
|
+
shape_region = shape_region.convert('RGBA')
|
|
527
|
+
|
|
528
|
+
# Apply overlay using the provided blend method
|
|
529
|
+
r, g, b, a = shape_region.split()
|
|
530
|
+
rgb_img = Image.merge('RGB', (r, g, b))
|
|
531
|
+
color_overlay = Image.new('RGB', shape_region.size, overlay_color)
|
|
532
|
+
|
|
533
|
+
# Blend RGB parts (50% transparency)
|
|
534
|
+
blended_rgb = Image.blend(rgb_img, color_overlay, alpha=0.5)
|
|
535
|
+
|
|
536
|
+
# Recombine with original alpha channel
|
|
537
|
+
new_r, new_g, new_b = blended_rgb.split()
|
|
538
|
+
blended_region = Image.merge('RGBA', (new_r, new_g, new_b, a))
|
|
539
|
+
|
|
540
|
+
# Paste blended region back to main image
|
|
541
|
+
main_img.paste(blended_region, bbox, blended_region)
|
|
542
|
+
|
|
543
|
+
def _draw_objects(self, draw):
|
|
544
|
+
"""Draw game objects."""
|
|
545
|
+
objects = self._map_data.get('objects', {})
|
|
546
|
+
|
|
547
|
+
for obj_type, obj_list in objects.items():
|
|
548
|
+
if obj_type in self._object_styles and obj_list:
|
|
549
|
+
style = self._object_styles[obj_type]
|
|
550
|
+
|
|
551
|
+
for obj in obj_list:
|
|
552
|
+
x, y = obj['x'], obj['y']
|
|
553
|
+
|
|
554
|
+
# Check for 'my' attribute and determine overlay color
|
|
555
|
+
overlay_color = None
|
|
556
|
+
if 'my' in obj:
|
|
557
|
+
if obj['my']:
|
|
558
|
+
overlay_color = '#76EE00' # Green for friendly
|
|
559
|
+
else:
|
|
560
|
+
overlay_color = '#EE6363' # Red for enemy
|
|
561
|
+
|
|
562
|
+
# Try to use local resource image first
|
|
563
|
+
resource_name = obj_type
|
|
564
|
+
if resource_name in self._resources:
|
|
565
|
+
# Use local image
|
|
566
|
+
try:
|
|
567
|
+
self._draw_resource_image(draw, x, y, resource_name, style, overlay_color)
|
|
568
|
+
continue
|
|
569
|
+
except Exception as e:
|
|
570
|
+
print(f"[DEBUG] Failed to draw resource image for {obj_type}: {e}")
|
|
571
|
+
|
|
572
|
+
# Fall back to质感绘图
|
|
573
|
+
self._draw_shape(draw, x, y, style, overlay_color)
|
|
574
|
+
|
|
575
|
+
def _draw_resource_image(self, draw, x, y, resource_name, style, overlay_color=None):
|
|
576
|
+
"""Draw local resource image."""
|
|
577
|
+
# Get the image from resource
|
|
578
|
+
img = self._resources[resource_name]()
|
|
579
|
+
|
|
580
|
+
# Calculate size based on style
|
|
581
|
+
size = int(self._cell_size * style.get('size', 0.7))
|
|
582
|
+
|
|
583
|
+
# Resize the image to fit the cell
|
|
584
|
+
img = img.resize((size, size), Image.Resampling.LANCZOS)
|
|
585
|
+
|
|
586
|
+
# Apply overlay color if needed
|
|
587
|
+
if overlay_color:
|
|
588
|
+
# Convert to RGBA if not already
|
|
589
|
+
if img.mode != 'RGBA':
|
|
590
|
+
img = img.convert('RGBA')
|
|
591
|
+
|
|
592
|
+
# Apply color overlay as per the provided blend method
|
|
593
|
+
r, g, b, a = img.split()
|
|
594
|
+
rgb_img = Image.merge('RGB', (r, g, b))
|
|
595
|
+
color_overlay = Image.new('RGB', img.size, overlay_color)
|
|
596
|
+
blended_rgb = Image.blend(rgb_img, color_overlay, alpha=0.5)
|
|
597
|
+
new_r, new_g, new_b = blended_rgb.split()
|
|
598
|
+
img = Image.merge('RGBA', (new_r, new_g, new_b, a))
|
|
599
|
+
|
|
600
|
+
# Calculate position to center the image
|
|
601
|
+
cell_x = x * self._cell_size
|
|
602
|
+
cell_y = y * self._cell_size
|
|
603
|
+
offset_x = (self._cell_size - size) // 2
|
|
604
|
+
offset_y = (self._cell_size - size) // 2
|
|
605
|
+
|
|
606
|
+
# Paste the image onto the main image
|
|
607
|
+
# Note: draw is a ImageDraw object, but we need to access the underlying image
|
|
608
|
+
# So we'll use the img attribute from the draw object
|
|
609
|
+
main_img = draw._image
|
|
610
|
+
main_img.paste(img, (cell_x + offset_x, cell_y + offset_y), img)
|
|
611
|
+
|
|
612
|
+
def _draw_grid(self, draw, img_width, img_height):
|
|
613
|
+
"""Draw grid lines with transparency."""
|
|
614
|
+
# Create a semi-transparent overlay for grid
|
|
615
|
+
grid_img = Image.new('RGBA', (img_width, img_height), (255, 248, 220, 0)) # #FFF8DC with 0 alpha
|
|
616
|
+
grid_draw = ImageDraw.Draw(grid_img)
|
|
617
|
+
|
|
618
|
+
# Vertical lines
|
|
619
|
+
for x in range(0, img_width + 1, self._cell_size):
|
|
620
|
+
grid_draw.line([x, 0, x, img_height], fill=(255, 248, 220, 128), width=1) # 50% transparent
|
|
621
|
+
|
|
622
|
+
# Horizontal lines
|
|
623
|
+
for y in range(0, img_height + 1, self._cell_size):
|
|
624
|
+
grid_draw.line([0, y, img_width, y], fill=(255, 248, 220, 128), width=1) # 50% transparent
|
|
625
|
+
|
|
626
|
+
# Composite the grid onto the main image
|
|
627
|
+
return grid_img
|
|
628
|
+
|
|
629
|
+
def render(self, output_path=None, show_grid=True):
|
|
630
|
+
"""
|
|
631
|
+
Render map to image file.
|
|
632
|
+
|
|
633
|
+
:param output_path: Output image path (default: map_render.png in same directory as map)
|
|
634
|
+
:param show_grid: Whether to show grid lines
|
|
635
|
+
|
|
636
|
+
:raises: FileNotFoundError: If map file doesn't exist
|
|
637
|
+
:raises: ValueError: If map data is invalid
|
|
638
|
+
|
|
639
|
+
:usage:
|
|
640
|
+
# 示例用法
|
|
641
|
+
renderer = MapRender('map1.json')
|
|
642
|
+
renderer.render('output.png')
|
|
643
|
+
"""
|
|
644
|
+
self._load_map()
|
|
645
|
+
|
|
646
|
+
# Get map dimensions
|
|
647
|
+
width, height = self._get_map_dimensions()
|
|
648
|
+
img_width = width * self._cell_size
|
|
649
|
+
img_height = height * self._cell_size
|
|
650
|
+
|
|
651
|
+
print(f"[DEBUG] Creating image {img_width}x{img_height}") # 调试输出:查看图像尺寸
|
|
652
|
+
|
|
653
|
+
# Create image with alpha channel for transparency support
|
|
654
|
+
img = Image.new('RGBA', (img_width, img_height), color=(255, 255, 255, 255))
|
|
655
|
+
draw = ImageDraw.Draw(img)
|
|
656
|
+
|
|
657
|
+
# Draw terrain
|
|
658
|
+
self._draw_terrain(draw, img_width, img_height)
|
|
659
|
+
|
|
660
|
+
# Draw grid if requested (returns overlay image)
|
|
661
|
+
if show_grid:
|
|
662
|
+
grid_overlay = self._draw_grid(draw, img_width, img_height)
|
|
663
|
+
img = Image.alpha_composite(img, grid_overlay)
|
|
664
|
+
draw = ImageDraw.Draw(img)
|
|
665
|
+
|
|
666
|
+
# Draw objects
|
|
667
|
+
self._draw_objects(draw)
|
|
668
|
+
|
|
669
|
+
# Save image
|
|
670
|
+
if output_path is None:
|
|
671
|
+
map_dir = os.path.dirname(self._map_path)
|
|
672
|
+
output_path = os.path.join(map_dir, 'map_render.png')
|
|
673
|
+
|
|
674
|
+
# Convert to RGB if saving as JPEG, keep RGBA for PNG
|
|
675
|
+
if output_path.lower().endswith('.jpg') or output_path.lower().endswith('.jpeg'):
|
|
676
|
+
img = img.convert('RGB')
|
|
677
|
+
|
|
678
|
+
img.save(output_path)
|
|
679
|
+
print(f"[DEBUG] Map rendered to {output_path}") # 调试输出:查看输出路径
|
|
680
|
+
|
|
681
|
+
return output_path
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def main():
|
|
685
|
+
"""Main function for command line usage."""
|
|
686
|
+
import sys
|
|
687
|
+
|
|
688
|
+
if len(sys.argv) < 2:
|
|
689
|
+
print("Usage: python map_render.py <map1.json> [output.png]")
|
|
690
|
+
sys.exit(1)
|
|
691
|
+
|
|
692
|
+
map_path = sys.argv[1]
|
|
693
|
+
output_path = sys.argv[2] if len(sys.argv) > 2 else None
|
|
694
|
+
|
|
695
|
+
try:
|
|
696
|
+
renderer = MapRender(map_path)
|
|
697
|
+
result_path = renderer.render(output_path)
|
|
698
|
+
print(f"Map rendered successfully to {result_path}")
|
|
699
|
+
except Exception as e:
|
|
700
|
+
print(f"Error rendering map: {e}")
|
|
701
|
+
sys.exit(1)
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
if __name__ == "__main__":
|
|
705
|
+
main()
|