chartflow 0.1__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.
Files changed (79) hide show
  1. chartflow/__init__.py +28 -0
  2. chartflow/_verify_importexport.py +169 -0
  3. chartflow/edit/__init__.py +119 -0
  4. chartflow/edit/code_editor.py +1007 -0
  5. chartflow/edit/code_folder.py +523 -0
  6. chartflow/edit/completer.py +2652 -0
  7. chartflow/edit/context_analyzer.py +521 -0
  8. chartflow/edit/demo.py +469 -0
  9. chartflow/edit/line_number_area.py +548 -0
  10. chartflow/edit/minimap.py +581 -0
  11. chartflow/edit/python_editor.py +509 -0
  12. chartflow/edit/syntax_highlighter.py +291 -0
  13. chartflow/edit/test_editor.py +190 -0
  14. chartflow/flow/__init__.py +31 -0
  15. chartflow/flow/_demo.py +98 -0
  16. chartflow/flow/chart.py +1101 -0
  17. chartflow/flow/editor.py +375 -0
  18. chartflow/flow/flow.py +6 -0
  19. chartflow/flow/qchart.py +1250 -0
  20. chartflow/flow/test/__init__.py +0 -0
  21. chartflow/flow/test/test_chart.py +537 -0
  22. chartflow/flow/test_decorator_sync.py +102 -0
  23. chartflow/fsm/__init__.py +84 -0
  24. chartflow/fsm/decorators.py +395 -0
  25. chartflow/fsm/demo.py +381 -0
  26. chartflow/fsm/demo_pyqt6.py +881 -0
  27. chartflow/fsm/demo_tree.py +46 -0
  28. chartflow/fsm/logic.py +447 -0
  29. chartflow/fsm/meta.py +569 -0
  30. chartflow/fsm/scheduler.py +427 -0
  31. chartflow/fsm/stage.py +2079 -0
  32. chartflow/fsm/stdio.py +66 -0
  33. chartflow/fsm/test/__init__.py +7 -0
  34. chartflow/fsm/test/test_decorators.py +210 -0
  35. chartflow/fsm/test/test_events.py +202 -0
  36. chartflow/fsm/test/test_integration.py +596 -0
  37. chartflow/fsm/test/test_logic.py +371 -0
  38. chartflow/fsm/test/test_meta.py +192 -0
  39. chartflow/fsm/test/test_runtime_param.py +336 -0
  40. chartflow/fsm/test/test_scheduler.py +280 -0
  41. chartflow/fsm/test/test_stage.py +314 -0
  42. chartflow/fsm/test_behavior_events.py +263 -0
  43. chartflow/fsm/test_nested_parallel.py +80 -0
  44. chartflow/fsm/test_stage_spec.py +448 -0
  45. chartflow/fsm/utils.py +34 -0
  46. chartflow/node/__init__.py +66 -0
  47. chartflow/node/_node_base.py +727 -0
  48. chartflow/node/_node_interaction.py +488 -0
  49. chartflow/node/_node_interface.py +426 -0
  50. chartflow/node/_node_markers.py +648 -0
  51. chartflow/node/_node_ports.py +536 -0
  52. chartflow/node/_port_interface.py +111 -0
  53. chartflow/node/arrow_item.py +287 -0
  54. chartflow/node/color_utils.py +113 -0
  55. chartflow/node/connection_item.py +939 -0
  56. chartflow/node/demo.py +258 -0
  57. chartflow/node/demo_arrow_drag.py +46 -0
  58. chartflow/node/demo_decorator.py +63 -0
  59. chartflow/node/demo_ports.py +69 -0
  60. chartflow/node/enums.py +35 -0
  61. chartflow/node/example.py +84 -0
  62. chartflow/node/layout_utils.py +307 -0
  63. chartflow/node/main_window.py +196 -0
  64. chartflow/node/menu_bar.py +240 -0
  65. chartflow/node/node_canvas.py +1730 -0
  66. chartflow/node/node_item.py +754 -0
  67. chartflow/node/popmenu.py +472 -0
  68. chartflow/node/port_item.py +591 -0
  69. chartflow/node/qnode_editor.py +73 -0
  70. chartflow/node/selection_rect.py +53 -0
  71. chartflow/node/style_panel.py +615 -0
  72. chartflow/node/styles.py +63 -0
  73. chartflow/node/test_qnode.py +2336 -0
  74. chartflow/showcase.py +528 -0
  75. chartflow/theme.py +508 -0
  76. chartflow-0.1.dist-info/METADATA +23 -0
  77. chartflow-0.1.dist-info/RECORD +79 -0
  78. chartflow-0.1.dist-info/WHEEL +5 -0
  79. chartflow-0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,881 @@
1
+ from __future__ import annotations
2
+ """
3
+ QFSM PyQt6 Demo - 可视化对战界面
4
+
5
+ 展示:
6
+ 1. 实时游戏画面渲染
7
+ 2. 角色移动、攻击动画
8
+ 3. 状态机状态显示
9
+ 4. 战斗日志
10
+ 5. 游戏控制(开始/暂停/加速)
11
+ """
12
+
13
+ import time
14
+
15
+
16
+
17
+ import sys
18
+ import math
19
+ from typing import Any, Optional
20
+ from dataclasses import dataclass
21
+ from enum import Enum, auto
22
+
23
+ from PyQt6.QtWidgets import (
24
+ QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
25
+ QPushButton, QLabel, QTextEdit, QSlider, QGroupBox, QGridLayout,
26
+ QSplitter, QFrame
27
+ )
28
+ from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QRectF, QPointF
29
+ from PyQt6.QtGui import (
30
+ QPainter, QColor, QBrush, QPen, QFont, QFontMetrics,
31
+ QLinearGradient, QRadialGradient
32
+ )
33
+
34
+ from chartflow.fsm.logic import Logic
35
+ from chartflow.fsm.scheduler import Scheduler
36
+
37
+
38
+ # =============================================================================
39
+ # 游戏实体和数据类
40
+ # =============================================================================
41
+
42
+ class EntityType(Enum):
43
+ PLAYER = auto()
44
+ ENEMY = auto()
45
+ PROJECTILE = auto()
46
+
47
+
48
+ @dataclass
49
+ class GameEntity:
50
+ """游戏实体"""
51
+ name: str
52
+ x: float
53
+ y: float
54
+ health: int
55
+ max_health: int
56
+ entity_type: EntityType
57
+ radius: float = 20.0
58
+ is_active: bool = True
59
+
60
+ def distance_to(self, other: 'GameEntity') -> float:
61
+ return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
62
+
63
+
64
+ @dataclass
65
+ class Projectile:
66
+ """投射物(攻击效果)"""
67
+ x: float
68
+ y: float
69
+ target_x: float
70
+ target_y: float
71
+ damage: int
72
+ source: str
73
+ speed: float = 8.0
74
+ life: int = 20
75
+
76
+
77
+ # =============================================================================
78
+ # 状态机类
79
+ # =============================================================================
80
+
81
+ class GameManager(Logic):
82
+ """游戏管理器"""
83
+ NAME = "GameManager"
84
+ PRIORITY = 100
85
+
86
+ def __init__(self, game_widget: 'GameWidget') -> None:
87
+ super().__init__(None)
88
+ self.game = game_widget
89
+ self.tick_count = 0
90
+ self.game_over = False
91
+
92
+ def onStart(self, it) -> None:
93
+ self.listenFor("enemy_died", self._on_enemy_died)
94
+ self.listenFor("player_died", self._on_player_died)
95
+ self.game.log("游戏开始!")
96
+
97
+ def _on_enemy_died(self, data: Any) -> None:
98
+ name = data.get("name", "Unknown") if isinstance(data, dict) else data
99
+ self.game.log(f"敌人 {name} 被消灭!")
100
+ self.game.enemy_killed += 1
101
+
102
+ def _on_player_died(self, data: Any) -> None:
103
+ self.game.log("玩家死亡! 游戏结束!")
104
+ self.game_over = True
105
+ self.game.game_over(False)
106
+
107
+ def onStep(self, it) -> None:
108
+ self.tick_count += 1
109
+ self.game.update_display()
110
+
111
+ # 检查胜利条件
112
+ if self.game.enemy_killed >= len(self.game.enemy_entities):
113
+ self.game.log("所有敌人被消灭! 胜利!")
114
+ self.game_over = True
115
+ self.game.game_over(True)
116
+ self.kill()
117
+
118
+ def idle(self, it) -> str | None:
119
+ if self.game_over:
120
+ return "game_over"
121
+ return None
122
+
123
+ def game_over(self, it) -> str | None:
124
+ self.pushEvent("game_over", None)
125
+ self.kill()
126
+ return None
127
+
128
+
129
+ class Player(Logic):
130
+ """玩家控制器"""
131
+ NAME = "Player"
132
+ PRIORITY = 50
133
+
134
+ def __init__(self, entity: GameEntity, game: 'GameWidget') -> None:
135
+ super().__init__(entity)
136
+ self._inst: GameEntity = entity
137
+ self.game = game
138
+ self.target: Optional[GameEntity] = None
139
+ self.attack_cooldown = 0
140
+ self.current_state_name = "idle"
141
+
142
+ def onStart(self, it) -> None:
143
+ self.game.log(f"玩家 {self._inst.name} 准备就绪")
144
+
145
+ def onStep(self, it) -> None:
146
+ if self.attack_cooldown > 0:
147
+ self.attack_cooldown -= 1
148
+ self.game.player_state = self.current_state_name
149
+
150
+ def idle(self, it) -> str | None:
151
+ self.current_state_name = "idle"
152
+ # 寻找最近的敌人
153
+ closest = None
154
+ closest_dist = float('inf')
155
+
156
+ for enemy_entity in self.game.enemy_entities:
157
+ if enemy_entity.health > 0:
158
+ dist = self._inst.distance_to(enemy_entity)
159
+ if dist < closest_dist:
160
+ closest_dist = dist
161
+ closest = enemy_entity
162
+
163
+ if closest:
164
+ self.target = closest
165
+ if closest_dist > 60: # 攻击范围
166
+ return "move"
167
+ else:
168
+ return "attack"
169
+ return None
170
+
171
+ def move(self, it) -> str | None:
172
+ self.current_state_name = "move"
173
+ if not self.target or self.target.health <= 0:
174
+ return "idle"
175
+
176
+ dx = self.target.x - self._inst.x
177
+ dy = self.target.y - self._inst.y
178
+ dist = math.sqrt(dx**2 + dy**2)
179
+
180
+ if dist <= 60:
181
+ return "attack"
182
+
183
+ # 移动
184
+ speed = 3.0
185
+ if dist > 0:
186
+ self._inst.x += (dx / dist) * speed
187
+ self._inst.y += (dy / dist) * speed
188
+
189
+ # 边界检查
190
+ self._inst.x = max(30, min(self.game.width() - 30, self._inst.x))
191
+ self._inst.y = max(30, min(self.game.height() - 100, self._inst.y))
192
+
193
+ return "move"
194
+
195
+ def attack(self, it) -> str | None:
196
+ self.current_state_name = "attack"
197
+ if not self.target or self.target.health <= 0:
198
+ return "idle"
199
+
200
+ if self.attack_cooldown > 0:
201
+ return "attack"
202
+
203
+ # 发射投射物
204
+ damage = 25
205
+ self.game.add_projectile(
206
+ self._inst.x, self._inst.y,
207
+ self.target.x, self.target.y,
208
+ damage, "player"
209
+ )
210
+ self.attack_cooldown = 8
211
+
212
+ return "attack"
213
+
214
+
215
+ class Enemy(Logic):
216
+ """敌人AI"""
217
+ NAME = "Enemy"
218
+ PRIORITY = 40
219
+
220
+ def __init__(self, entity: GameEntity, patrol_points: list[tuple[float, float]],
221
+ game: 'GameWidget') -> None:
222
+ super().__init__(entity)
223
+ self._inst: GameEntity = entity
224
+ self.patrol_points = patrol_points
225
+ self.current_point = 0
226
+ self.game = game
227
+ self.player_entity = game.player_entity
228
+ self.attack_cooldown = 0
229
+ self.alert_range = 150.0
230
+ self.attack_range = 60.0
231
+ self.current_state_name = "patrol"
232
+
233
+ def onStart(self, it) -> None:
234
+ self.listenFor("game_over", self._on_game_over)
235
+
236
+ def _on_game_over(self, data: Any) -> None:
237
+ self.kill()
238
+
239
+ def onStep(self, it) -> None:
240
+ if self.attack_cooldown > 0:
241
+ self.attack_cooldown -= 1
242
+ if self._inst.health <= 0:
243
+ self.kill()
244
+
245
+ def patrol(self, it) -> str | None:
246
+ self.current_state_name = "patrol"
247
+ # 检查玩家
248
+ if self.player_entity.health > 0:
249
+ dist = self._inst.distance_to(self.player_entity)
250
+ if dist <= self.alert_range:
251
+ return "chase"
252
+
253
+ # 巡逻移动
254
+ target = self.patrol_points[self.current_point]
255
+ dx = target[0] - self._inst.x
256
+ dy = target[1] - self._inst.y
257
+ dist = math.sqrt(dx**2 + dy**2)
258
+
259
+ if dist < 5:
260
+ self.current_point = (self.current_point + 1) % len(self.patrol_points)
261
+ else:
262
+ speed = 1.5
263
+ self._inst.x += (dx / dist) * speed
264
+ self._inst.y += (dy / dist) * speed
265
+
266
+ return None
267
+
268
+ def chase(self, it) -> str | None:
269
+ self.current_state_name = "chase"
270
+ if self.player_entity.health <= 0:
271
+ return "patrol"
272
+
273
+ dist = self._inst.distance_to(self.player_entity)
274
+
275
+ if dist > self.alert_range * 1.3:
276
+ return "patrol"
277
+ if dist <= self.attack_range:
278
+ return "attack"
279
+
280
+ # 追击
281
+ dx = self.player_entity.x - self._inst.x
282
+ dy = self.player_entity.y - self._inst.y
283
+ speed = 2.0
284
+ if dist > 0:
285
+ self._inst.x += (dx / dist) * speed
286
+ self._inst.y += (dy / dist) * speed
287
+
288
+ return "chase"
289
+
290
+ def attack(self, it) -> str | None:
291
+ self.current_state_name = "attack"
292
+ if self.player_entity.health <= 0:
293
+ return "patrol"
294
+
295
+ dist = self._inst.distance_to(self.player_entity)
296
+ if dist > self.attack_range:
297
+ return "chase"
298
+
299
+ if self.attack_cooldown > 0:
300
+ return "attack"
301
+
302
+ # 发射投射物
303
+ damage = 12
304
+ self.game.add_projectile(
305
+ self._inst.x, self._inst.y,
306
+ self.player_entity.x, self.player_entity.y,
307
+ damage, self._inst.name
308
+ )
309
+ self.attack_cooldown = 10
310
+
311
+ return "attack"
312
+
313
+
314
+ # =============================================================================
315
+ # PyQt6 游戏组件
316
+ # =============================================================================
317
+
318
+ class GameWidget(QWidget):
319
+ """游戏画面组件"""
320
+
321
+ def __init__(self, parent: Optional[QWidget] = None) -> None:
322
+ super().__init__(parent)
323
+ self.setMinimumSize(800, 500)
324
+ self.setAutoFillBackground(True)
325
+
326
+ # 游戏状态
327
+ self.running = False
328
+ self.game_speed = 1
329
+ self.tick_count = 0
330
+ self.enemy_killed = 0
331
+ self.player_state = "idle"
332
+ self.game_result: Optional[str] = None
333
+
334
+ # 游戏对象
335
+ self.player_entity = GameEntity("Hero", 100, 250, 100, 100, EntityType.PLAYER, 25)
336
+ self.enemy_entities: list[GameEntity] = []
337
+ self.projectiles: list[Projectile] = []
338
+
339
+ # 状态机
340
+ self.scheduler: Optional[Scheduler] = None
341
+ self.game_manager: Optional[GameManager] = None
342
+ self.player_logic: Optional[Player] = None
343
+ self.enemy_logics: list[Enemy] = []
344
+
345
+ # 定时器
346
+ self.timer = QTimer(self)
347
+ self.timer.timeout.connect(self.game_tick)
348
+
349
+ # 颜色
350
+ self.bg_color = QColor(30, 30, 40)
351
+ self.player_color = QColor(100, 200, 100)
352
+ self.enemy_color = QColor(200, 100, 100)
353
+ self.projectile_player = QColor(100, 255, 100)
354
+ self.projectile_enemy = QColor(255, 100, 100)
355
+
356
+ def init_game(self) -> None:
357
+ """初始化游戏"""
358
+ # 重置实体
359
+ self.player_entity = GameEntity("Hero", 100, 250, 100, 100, EntityType.PLAYER, 25)
360
+ self.enemy_entities = [
361
+ GameEntity("Slime", 600, 150, 75, 75, EntityType.ENEMY, 20),
362
+ GameEntity("Goblin", 650, 350, 100, 100, EntityType.ENEMY, 22),
363
+ GameEntity("Orc", 700, 250, 125, 125, EntityType.ENEMY, 25),
364
+ ]
365
+ self.projectiles.clear()
366
+ self.tick_count = 0
367
+ self.enemy_killed = 0
368
+ self.player_state = "idle"
369
+ self.game_result = None
370
+
371
+ # 创建调度器
372
+ self.scheduler = Scheduler()
373
+ self.scheduler.login(GameManager)
374
+ self.scheduler.login(Player)
375
+ self.scheduler.login(Enemy)
376
+
377
+ # 创建状态机
378
+ self.game_manager = GameManager(self)
379
+ self.player_logic = Player(self.player_entity, self)
380
+
381
+ self.enemy_logics = []
382
+ patrol_patterns = [
383
+ [(550, 100), (650, 100), (650, 200), (550, 200)],
384
+ [(600, 300), (700, 300), (700, 400), (600, 400)],
385
+ [(650, 200), (750, 200), (750, 300), (650, 300)],
386
+ ]
387
+
388
+ for i, enemy_entity in enumerate(self.enemy_entities):
389
+ logic = Enemy(enemy_entity, patrol_patterns[i], self)
390
+ self.enemy_logics.append(logic)
391
+
392
+ def start_game(self) -> None:
393
+ """开始游戏"""
394
+ if not self.running:
395
+ self.init_game()
396
+ self.running = True
397
+ self.timer.start(50 // self.game_speed) # 20fps基础
398
+
399
+ def pause_game(self) -> None:
400
+ """暂停/继续"""
401
+ if self.running:
402
+ if self.timer.isActive():
403
+ self.timer.stop()
404
+ else:
405
+ self.timer.start(50 // self.game_speed)
406
+
407
+ def stop_game(self) -> None:
408
+ """停止游戏"""
409
+ self.running = False
410
+ self.timer.stop()
411
+
412
+ def set_speed(self, speed: int) -> None:
413
+ """设置游戏速度"""
414
+ self.game_speed = speed
415
+ if self.timer.isActive():
416
+ self.timer.start(50 // self.game_speed)
417
+
418
+ def game_tick(self) -> None:
419
+ """游戏tick"""
420
+ if self.scheduler and self.running:
421
+ self.scheduler.handle()
422
+ self.tick_count += 1
423
+
424
+ # 更新投射物
425
+ self.update_projectiles()
426
+
427
+ # 检查伤害
428
+ self.check_damage()
429
+
430
+ self.update()
431
+
432
+ def update_projectiles(self) -> None:
433
+ """更新投射物位置"""
434
+ for p in self.projectiles[:]:
435
+ dx = p.target_x - p.x
436
+ dy = p.target_y - p.y
437
+ dist = math.sqrt(dx**2 + dy**2)
438
+
439
+ if dist < p.speed or p.life <= 0:
440
+ self.projectiles.remove(p)
441
+ continue
442
+
443
+ p.x += (dx / dist) * p.speed
444
+ p.y += (dy / dist) * p.speed
445
+ p.life -= 1
446
+
447
+ def check_damage(self) -> None:
448
+ """检查投射物命中"""
449
+ for p in self.projectiles[:]:
450
+ hit = False
451
+
452
+ if p.source == "player":
453
+ # 玩家攻击,检查命中敌人
454
+ for enemy in self.enemy_entities:
455
+ if enemy.health > 0:
456
+ dist = math.sqrt((p.x - enemy.x)**2 + (p.y - enemy.y)**2)
457
+ if dist < enemy.radius:
458
+ enemy.health -= p.damage
459
+ hit = True
460
+ if enemy.health <= 0:
461
+ self.pushEvent("enemy_died", {"name": enemy.name})
462
+ break
463
+ else:
464
+ # 敌人攻击,检查命中玩家
465
+ if self.player_entity.health > 0:
466
+ dist = math.sqrt((p.x - self.player_entity.x)**2 + (p.y - self.player_entity.y)**2)
467
+ if dist < self.player_entity.radius:
468
+ self.player_entity.health -= p.damage
469
+ hit = True
470
+ if self.player_entity.health <= 0:
471
+ self.pushEvent("player_died", {"name": self.player_entity.name})
472
+
473
+ if hit and p in self.projectiles:
474
+ self.projectiles.remove(p)
475
+
476
+ def add_projectile(self, x: float, y: float, tx: float, ty: float,
477
+ damage: int, source: str) -> None:
478
+ """添加投射物"""
479
+ self.projectiles.append(Projectile(x, y, tx, ty, damage, source))
480
+
481
+ def pushEvent(self, event: str, data: Any) -> None:
482
+ """推送事件"""
483
+ if self.game_manager:
484
+ self.game_manager.pushEvent(event, data)
485
+
486
+ def log(self, message: str) -> None:
487
+ """添加日志"""
488
+ # 由主窗口处理
489
+ if self.parent() and hasattr(self.parent(), 'add_log'):
490
+ self.parent().add_log(message)
491
+
492
+ def update_display(self) -> None:
493
+ """更新显示信息"""
494
+ if self.parent() and hasattr(self.parent(), 'update_info'):
495
+ self.parent().update_info()
496
+
497
+ def game_over(self, victory: bool) -> None:
498
+ """游戏结束"""
499
+ self.game_result = "VICTORY" if victory else "DEFEAT"
500
+ self.running = False
501
+ self.timer.stop()
502
+
503
+ def paintEvent(self, event) -> None:
504
+ """绘制游戏画面"""
505
+ painter = QPainter(self)
506
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
507
+
508
+ # 绘制背景
509
+ painter.fillRect(self.rect(), self.bg_color)
510
+
511
+ # 绘制网格
512
+ pen = QPen(QColor(50, 50, 60))
513
+ pen.setWidth(1)
514
+ painter.setPen(pen)
515
+ for i in range(0, self.width(), 50):
516
+ painter.drawLine(i, 0, i, self.height())
517
+ for i in range(0, self.height(), 50):
518
+ painter.drawLine(0, i, self.width(), i)
519
+
520
+ # 绘制投射物
521
+ for p in self.projectiles:
522
+ color = self.projectile_player if p.source == "player" else self.projectile_enemy
523
+ painter.setBrush(QBrush(color))
524
+ painter.setPen(Qt.PenStyle.NoPen)
525
+ painter.drawEllipse(int(p.x - 4), int(p.y - 4), 8, 8)
526
+
527
+ # 绘制敌人
528
+ for i, enemy in enumerate(self.enemy_entities):
529
+ if enemy.health > 0:
530
+ self.draw_entity(painter, enemy, self.enemy_color, i)
531
+
532
+ # 绘制玩家
533
+ if self.player_entity.health > 0:
534
+ self.draw_entity(painter, self.player_entity, self.player_color, -1)
535
+
536
+ # 绘制游戏结果
537
+ if self.game_result:
538
+ self.draw_game_result(painter)
539
+
540
+ painter.end()
541
+
542
+ def draw_entity(self, painter: QPainter, entity: GameEntity,
543
+ color: QColor, index: int) -> None:
544
+ """绘制实体"""
545
+ x, y = int(entity.x), int(entity.y)
546
+ r = int(entity.radius)
547
+
548
+ # 绘制范围圈(巡逻/警戒范围)
549
+ if entity.entity_type == EntityType.ENEMY:
550
+ pen = QPen(QColor(200, 100, 100, 50))
551
+ pen.setWidth(2)
552
+ pen.setStyle(Qt.PenStyle.DotLine)
553
+ painter.setPen(pen)
554
+ painter.setBrush(Qt.BrushStyle.NoBrush)
555
+ painter.drawEllipse(x - 150, y - 150, 300, 300)
556
+
557
+ # 绘制实体阴影
558
+ painter.setBrush(QBrush(QColor(0, 0, 0, 100)))
559
+ painter.setPen(Qt.PenStyle.NoPen)
560
+ painter.drawEllipse(x - r + 3, y - r + 5, r * 2, r * 2)
561
+
562
+ # 绘制实体主体
563
+ gradient = QRadialGradient(x - r//3, y - r//3, r)
564
+ gradient.setColorAt(0, color.lighter(150))
565
+ gradient.setColorAt(1, color)
566
+ painter.setBrush(QBrush(gradient))
567
+ painter.setPen(QPen(color.darker(120), 2))
568
+ painter.drawEllipse(x - r, y - r, r * 2, r * 2)
569
+
570
+ # 绘制方向指示
571
+ painter.setPen(QPen(color.darker(150), 3))
572
+ if entity.entity_type == EntityType.PLAYER and self.player_logic:
573
+ target = self.player_logic.target
574
+ if target:
575
+ dx = target.x - entity.x
576
+ dy = target.y - entity.y
577
+ dist = math.sqrt(dx**2 + dy**2)
578
+ if dist > 0:
579
+ end_x = x + (dx / dist) * (r - 5)
580
+ end_y = y + (dy / dist) * (r - 5)
581
+ painter.drawLine(x, y, int(end_x), int(end_y))
582
+
583
+ # 绘制血条背景
584
+ bar_width = r * 2
585
+ bar_height = 6
586
+ bar_x = x - bar_width // 2
587
+ bar_y = y - r - 15
588
+ painter.fillRect(bar_x, bar_y, bar_width, bar_height, QColor(60, 60, 60))
589
+
590
+ # 绘制血条
591
+ health_pct = max(0, entity.health / entity.max_health)
592
+ health_width = int(bar_width * health_pct)
593
+ if health_pct > 0.5:
594
+ health_color = QColor(100, 200, 100)
595
+ elif health_pct > 0.25:
596
+ health_color = QColor(200, 200, 100)
597
+ else:
598
+ health_color = QColor(200, 100, 100)
599
+ painter.fillRect(bar_x, bar_y, health_width, bar_height, health_color)
600
+
601
+ # 绘制血条边框
602
+ painter.setPen(QPen(QColor(100, 100, 100), 1))
603
+ painter.setBrush(Qt.BrushStyle.NoBrush)
604
+ painter.drawRect(bar_x, bar_y, bar_width, bar_height)
605
+
606
+ # 绘制名称
607
+ painter.setPen(QPen(Qt.GlobalColor.white))
608
+ font = QFont("Microsoft YaHei", 9, QFont.Weight.Bold)
609
+ painter.setFont(font)
610
+ text_rect = QRectF(x - 40, y + r + 5, 80, 20)
611
+ painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter, entity.name)
612
+
613
+ # 绘制状态(如果是敌人)
614
+ if entity.entity_type == EntityType.ENEMY and index >= 0:
615
+ logic = self.enemy_logics[index] if index < len(self.enemy_logics) else None
616
+ if logic:
617
+ state_text = logic.current_state_name.upper()
618
+ painter.setPen(QPen(QColor(200, 200, 200)))
619
+ font = QFont("Microsoft YaHei", 8)
620
+ painter.setFont(font)
621
+ state_rect = QRectF(x - 40, y + r + 22, 80, 16)
622
+ painter.drawText(state_rect, Qt.AlignmentFlag.AlignCenter, state_text)
623
+
624
+ def draw_game_result(self, painter: QPainter) -> None:
625
+ """绘制游戏结果"""
626
+ # 半透明遮罩
627
+ overlay = QColor(0, 0, 0, 180)
628
+ painter.fillRect(self.rect(), overlay)
629
+
630
+ # 结果文字
631
+ if self.game_result == "VICTORY":
632
+ text = "VICTORY!"
633
+ color = QColor(100, 255, 100)
634
+ else:
635
+ text = "DEFEAT!"
636
+ color = QColor(255, 100, 100)
637
+
638
+ painter.setPen(QPen(color))
639
+ font = QFont("Microsoft YaHei", 48, QFont.Weight.Bold)
640
+ painter.setFont(font)
641
+
642
+ metrics = QFontMetrics(font)
643
+ text_width = metrics.horizontalAdvance(text)
644
+ x = (self.width() - text_width) // 2
645
+ y = self.height() // 2
646
+
647
+ painter.drawText(x, y, text)
648
+
649
+ # 提示文字
650
+ painter.setPen(QPen(QColor(200, 200, 200)))
651
+ font = QFont("Microsoft YaHei", 14)
652
+ painter.setFont(font)
653
+ hint = "Press START to play again"
654
+ hint_width = QFontMetrics(font).horizontalAdvance(hint)
655
+ painter.drawText((self.width() - hint_width) // 2, y + 50, hint)
656
+
657
+
658
+ class MainWindow(QMainWindow):
659
+ """主窗口"""
660
+
661
+ def __init__(self) -> None:
662
+ super().__init__()
663
+ self.setWindowTitle("QFSM Battle Demo - PyQt6")
664
+ self.setMinimumSize(1000, 600)
665
+
666
+ # 创建中央部件
667
+ central = QWidget()
668
+ self.setCentralWidget(central)
669
+ layout = QHBoxLayout(central)
670
+ layout.setSpacing(10)
671
+
672
+ # 分割器
673
+ splitter = QSplitter(Qt.Orientation.Horizontal)
674
+ layout.addWidget(splitter)
675
+
676
+ # 左侧:游戏画面
677
+ self.game_widget = GameWidget()
678
+ splitter.addWidget(self.game_widget)
679
+
680
+ # 右侧:控制面板
681
+ control_panel = self.create_control_panel()
682
+ splitter.addWidget(control_panel)
683
+
684
+ splitter.setSizes([800, 200])
685
+
686
+ # 设置样式
687
+ self.setStyleSheet("""
688
+ QMainWindow {
689
+ background-color: #2b2b2b;
690
+ }
691
+ QWidget {
692
+ background-color: #2b2b2b;
693
+ color: #cccccc;
694
+ }
695
+ QGroupBox {
696
+ border: 1px solid #555;
697
+ border-radius: 5px;
698
+ margin-top: 10px;
699
+ padding-top: 10px;
700
+ font-weight: bold;
701
+ }
702
+ QGroupBox::title {
703
+ subcontrol-origin: margin;
704
+ left: 10px;
705
+ padding: 0 5px;
706
+ }
707
+ QPushButton {
708
+ background-color: #3d3d3d;
709
+ border: 1px solid #555;
710
+ padding: 8px 16px;
711
+ border-radius: 4px;
712
+ }
713
+ QPushButton:hover {
714
+ background-color: #4d4d4d;
715
+ }
716
+ QPushButton:pressed {
717
+ background-color: #5d5d5d;
718
+ }
719
+ QTextEdit {
720
+ background-color: #1e1e1e;
721
+ border: 1px solid #444;
722
+ color: #aaa;
723
+ }
724
+ QSlider::groove:horizontal {
725
+ height: 8px;
726
+ background: #3d3d3d;
727
+ border-radius: 4px;
728
+ }
729
+ QSlider::handle:horizontal {
730
+ background: #6d6d6d;
731
+ width: 18px;
732
+ margin: -5px 0;
733
+ border-radius: 9px;
734
+ }
735
+ QLabel {
736
+ color: #aaa;
737
+ }
738
+ """)
739
+
740
+ def create_control_panel(self) -> QWidget:
741
+ """创建控制面板"""
742
+ panel = QWidget()
743
+ layout = QVBoxLayout(panel)
744
+ layout.setSpacing(15)
745
+
746
+ # 控制按钮组
747
+ control_group = QGroupBox("Game Control")
748
+ control_layout = QVBoxLayout(control_group)
749
+
750
+ self.start_btn = QPushButton("START")
751
+ self.start_btn.setStyleSheet("background-color: #2d5a2d;")
752
+ self.start_btn.clicked.connect(self.on_start)
753
+ control_layout.addWidget(self.start_btn)
754
+
755
+ self.pause_btn = QPushButton("PAUSE")
756
+ self.pause_btn.clicked.connect(self.on_pause)
757
+ control_layout.addWidget(self.pause_btn)
758
+
759
+ self.stop_btn = QPushButton("STOP")
760
+ self.stop_btn.setStyleSheet("background-color: #5a2d2d;")
761
+ self.stop_btn.clicked.connect(self.on_stop)
762
+ control_layout.addWidget(self.stop_btn)
763
+
764
+ layout.addWidget(control_group)
765
+
766
+ # 速度控制
767
+ speed_group = QGroupBox("Game Speed")
768
+ speed_layout = QVBoxLayout(speed_group)
769
+
770
+ self.speed_slider = QSlider(Qt.Orientation.Horizontal)
771
+ self.speed_slider.setMinimum(1)
772
+ self.speed_slider.setMaximum(5)
773
+ self.speed_slider.setValue(1)
774
+ self.speed_slider.valueChanged.connect(self.on_speed_changed)
775
+ speed_layout.addWidget(self.speed_slider)
776
+
777
+ self.speed_label = QLabel("1x")
778
+ self.speed_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
779
+ speed_layout.addWidget(self.speed_label)
780
+
781
+ layout.addWidget(speed_group)
782
+
783
+ # 状态信息
784
+ info_group = QGroupBox("Status")
785
+ info_layout = QGridLayout(info_group)
786
+
787
+ info_layout.addWidget(QLabel("Tick:"), 0, 0)
788
+ self.tick_label = QLabel("0")
789
+ info_layout.addWidget(self.tick_label, 0, 1)
790
+
791
+ info_layout.addWidget(QLabel("Player State:"), 1, 0)
792
+ self.player_state_label = QLabel("idle")
793
+ info_layout.addWidget(self.player_state_label, 1, 1)
794
+
795
+ info_layout.addWidget(QLabel("Player HP:"), 2, 0)
796
+ self.player_hp_label = QLabel("100/100")
797
+ info_layout.addWidget(self.player_hp_label, 2, 1)
798
+
799
+ info_layout.addWidget(QLabel("Enemies:"), 3, 0)
800
+ self.enemy_label = QLabel("3")
801
+ info_layout.addWidget(self.enemy_label, 3, 1)
802
+
803
+ info_layout.addWidget(QLabel("Killed:"), 4, 0)
804
+ self.killed_label = QLabel("0")
805
+ info_layout.addWidget(self.killed_label, 4, 1)
806
+
807
+ layout.addWidget(info_group)
808
+
809
+ # 战斗日志
810
+ log_group = QGroupBox("Battle Log")
811
+ log_layout = QVBoxLayout(log_group)
812
+
813
+ self.log_text = QTextEdit()
814
+ self.log_text.setReadOnly(True)
815
+ self.log_text.document().setMaximumBlockCount(100)
816
+ log_layout.addWidget(self.log_text)
817
+
818
+ layout.addWidget(log_group)
819
+
820
+ # 添加弹性空间
821
+ layout.addStretch()
822
+
823
+ return panel
824
+
825
+ def on_start(self) -> None:
826
+ """开始按钮"""
827
+ self.game_widget.start_game()
828
+ self.add_log("游戏开始!")
829
+
830
+ def on_pause(self) -> None:
831
+ """暂停按钮"""
832
+ self.game_widget.pause_game()
833
+ if self.game_widget.timer.isActive():
834
+ self.pause_btn.setText("PAUSE")
835
+ else:
836
+ self.pause_btn.setText("RESUME")
837
+
838
+ def on_stop(self) -> None:
839
+ """停止按钮"""
840
+ self.game_widget.stop_game()
841
+ self.pause_btn.setText("PAUSE")
842
+
843
+ def on_speed_changed(self, value: int) -> None:
844
+ """速度改变"""
845
+ self.speed_label.setText(f"{value}x")
846
+ self.game_widget.set_speed(value)
847
+
848
+ def add_log(self, message: str) -> None:
849
+ """添加日志"""
850
+ timestamp = time.strftime("%H:%M:%S")
851
+ self.log_text.append(f"[{timestamp}] {message}")
852
+
853
+ def update_info(self) -> None:
854
+ """更新信息"""
855
+ self.tick_label.setText(str(self.game_widget.tick_count))
856
+ self.player_state_label.setText(self.game_widget.player_state.upper())
857
+
858
+ hp = self.game_widget.player_entity.health
859
+ max_hp = self.game_widget.player_entity.max_health
860
+ self.player_hp_label.setText(f"{max(0, hp)}/{max_hp}")
861
+
862
+ alive_enemies = sum(1 for e in self.game_widget.enemy_entities if e.health > 0)
863
+ self.enemy_label.setText(str(alive_enemies))
864
+ self.killed_label.setText(str(self.game_widget.enemy_killed))
865
+
866
+
867
+ def main():
868
+ app = QApplication(sys.argv)
869
+
870
+ # 设置应用字体
871
+ font = QFont("Microsoft YaHei", 10)
872
+ app.setFont(font)
873
+
874
+ window = MainWindow()
875
+ window.show()
876
+
877
+ sys.exit(app.exec())
878
+
879
+
880
+ if __name__ == "__main__":
881
+ main()