gui.py 26KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760
  1. # coding: utf-8
  2. import typing
  3. import weakref
  4. from math import floor
  5. import pyglet
  6. from pyglet.window import mouse
  7. import cocos
  8. from cocos import collision_model
  9. from cocos import euclid
  10. from cocos.layer import ScrollableLayer
  11. from synergine2.config import Config
  12. from synergine2.log import SynergineLogger
  13. from synergine2.terminals import Terminal
  14. from synergine2.terminals import TerminalPackage
  15. from synergine2_cocos2d.actor import Actor
  16. from synergine2_cocos2d.exception import InteractionNotFound
  17. from synergine2_cocos2d.exception import OuterWorldPosition
  18. from synergine2_cocos2d.gl import draw_rectangle
  19. from synergine2_cocos2d.gl import rectangle_positions_type
  20. from synergine2_cocos2d.interaction import InteractionManager
  21. from synergine2_cocos2d.layer import LayerManager
  22. from synergine2_cocos2d.middleware import MapMiddleware
  23. from synergine2_cocos2d.middleware import TMXMiddleware
  24. from synergine2_cocos2d.user_action import UserAction
  25. from synergine2_xyz.xyz import XYZSubjectMixin
  26. class GridManager(object):
  27. def __init__(
  28. self,
  29. cell_width: int,
  30. cell_height: int,
  31. world_width: int,
  32. world_height: int,
  33. ) -> None:
  34. self.cell_width = cell_width
  35. self.cell_height = cell_height
  36. self.world_width = world_width
  37. self.world_height = world_height
  38. def get_grid_position(self, pixel_position: typing.Tuple[int, int]) -> typing.Tuple[int, int]:
  39. pixel_x, pixel_y = pixel_position
  40. cell_x = int(floor(pixel_x / self.cell_width))
  41. cell_y = int(floor(pixel_y / self.cell_height))
  42. if cell_x > self.world_width or cell_y > self.world_height or cell_x < 0 or cell_y < 0:
  43. raise OuterWorldPosition('Position "{}" is outer world ({}x{})'.format(
  44. (cell_x, cell_y),
  45. self.world_width,
  46. self.world_height,
  47. ))
  48. return cell_x, cell_y
  49. def get_pixel_position_of_grid_position(self, grid_position: typing.Tuple[int, int]) -> typing.Tuple[int, int]:
  50. return grid_position[0] * self.cell_width + (self.cell_width // 2),\
  51. grid_position[1] * self.cell_height + (self.cell_height // 2)
  52. def get_rectangle_positions(
  53. self,
  54. grid_position: typing.Tuple[int, int],
  55. ) -> rectangle_positions_type:
  56. """
  57. A<---D
  58. | |
  59. B--->C
  60. :param grid_position:grid position to exploit
  61. :return: grid pixel corners positions
  62. """
  63. grid_x, grid_y = grid_position
  64. a = grid_x * self.cell_width, grid_y * self.cell_height + self.cell_height
  65. b = grid_x * self.cell_width, grid_y * self.cell_height
  66. c = grid_x * self.cell_width + self.cell_width, grid_y * self.cell_height
  67. d = grid_x * self.cell_width + self.cell_width, grid_y * self.cell_height + self.cell_height
  68. return a, d, c, b
  69. class MinMaxRect(cocos.cocosnode.CocosNode):
  70. def __init__(self, layer_manager: LayerManager):
  71. super(MinMaxRect, self).__init__()
  72. self.layer_manager = layer_manager
  73. self.color3 = (20, 20, 20)
  74. self.color3f = (0, 0, 0, 0.2)
  75. self.vertexes = [(0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)]
  76. self.visible = False
  77. def adjust_from_w_minmax(self, wminx, wmaxx, wminy, wmaxy):
  78. # asumes world to screen preserves order
  79. sminx, sminy = self.layer_manager.scrolling_manager.world_to_screen(wminx, wminy)
  80. smaxx, smaxy = self.layer_manager.scrolling_manager.world_to_screen(wmaxx, wmaxy)
  81. self.vertexes = [(sminx, sminy), (sminx, smaxy), (smaxx, smaxy), (smaxx, sminy)]
  82. def draw(self):
  83. if not self.visible:
  84. return
  85. draw_rectangle(
  86. self.vertexes,
  87. self.color3,
  88. self.color3f,
  89. )
  90. def set_vertexes_from_minmax(self, minx, maxx, miny, maxy):
  91. self.vertexes = [(minx, miny), (minx, maxy), (maxx, maxy), (maxx, miny)]
  92. class EditLayer(cocos.layer.Layer):
  93. is_event_handler = True
  94. def __init__(
  95. self,
  96. config: Config,
  97. logger: SynergineLogger,
  98. layer_manager: LayerManager,
  99. grid_manager: GridManager,
  100. worldview,
  101. bindings=None,
  102. fastness=None,
  103. autoscroll_border=10,
  104. autoscroll_fastness=None,
  105. wheel_multiplier=None,
  106. zoom_min=None,
  107. zoom_max=None,
  108. zoom_fastness=None,
  109. mod_modify_selection=None,
  110. mod_restricted_mov=None,
  111. ):
  112. # TODO: Clean init params
  113. super().__init__()
  114. self.config = config
  115. self.logger = logger
  116. self.layer_manager = layer_manager
  117. self.grid_manager = grid_manager
  118. self.bindings = bindings
  119. buttons = {}
  120. modifiers = {}
  121. for k in bindings:
  122. buttons[bindings[k]] = 0
  123. modifiers[bindings[k]] = 0
  124. self.buttons = buttons
  125. self.modifiers = modifiers
  126. self.fastness = fastness
  127. self.autoscroll_border = autoscroll_border
  128. self.autoscroll_fastness = autoscroll_fastness
  129. self.wheel_multiplier = wheel_multiplier
  130. self.zoom_min = zoom_min
  131. self.zoom_max = zoom_max
  132. self.zoom_fastness = zoom_fastness
  133. self.mod_modify_selection = mod_modify_selection
  134. self.mod_restricted_mov = mod_restricted_mov
  135. self.weak_scroller = weakref.ref(self.layer_manager.scrolling_manager)
  136. self.weak_worldview = weakref.ref(worldview)
  137. self.wwidth = worldview.width
  138. self.wheight = worldview.height
  139. self.autoscrolling = False
  140. self.drag_selecting = False
  141. self.drag_moving = False
  142. self.restricted_mov = False
  143. self.wheel = 0
  144. self.dragging = False
  145. self.keyscrolling = False
  146. self.keyscrolling_descriptor = (0, 0)
  147. self.wdrag_start_point = (0, 0)
  148. self.elastic_box = None # type: MinMaxRect
  149. self.elastic_box_wminmax = 0, 0, 0, 0
  150. self.selection = {}
  151. self.screen_mouse = (0, 0)
  152. self.world_mouse = (0, 0)
  153. self.sleft = None
  154. self.sright = None
  155. self.sbottom = None
  156. self.s_top = None
  157. self.user_action_pending = None # type: UserAction
  158. # opers that change cshape must ensure it goes to False,
  159. # selection opers must ensure it goes to True
  160. self.selection_in_collman = True
  161. # TODO: Hardcoded here, should be obtained from level properties or calc
  162. # from available actors or current actors in worldview
  163. gsize = 32 * 1.25
  164. self.collision_manager = collision_model.CollisionManagerGrid(
  165. -gsize,
  166. self.wwidth + gsize,
  167. -gsize,
  168. self.wheight + gsize,
  169. gsize,
  170. gsize,
  171. )
  172. self.schedule(self.update)
  173. self.selectable_actors = []
  174. def set_selectable(self, actor: Actor) -> None:
  175. self.selectable_actors.append(actor)
  176. self.collision_manager.add(actor)
  177. def unset_selectable(self, actor: Actor) -> None:
  178. self.selectable_actors.remove(actor)
  179. self.collision_manager.remove_tricky(actor)
  180. def draw(self, *args, **kwargs) -> None:
  181. self.draw_update_cshapes()
  182. self.draw_selection()
  183. self.draw_interactions()
  184. def draw_update_cshapes(self) -> None:
  185. for actor in self.selectable_actors:
  186. if actor.need_update_cshape:
  187. if self.collision_manager.knows(actor):
  188. self.collision_manager.remove_tricky(actor)
  189. actor.update_cshape()
  190. self.collision_manager.add(actor)
  191. def draw_selection(self) -> None:
  192. for actor, cshape in self.selection.items():
  193. grid_position = self.grid_manager.get_grid_position(actor.position)
  194. rect_positions = self.grid_manager.get_rectangle_positions(grid_position)
  195. draw_rectangle(
  196. self.layer_manager.scrolling_manager.world_to_screen_positions(rect_positions),
  197. (0, 81, 211),
  198. )
  199. def draw_interactions(self) -> None:
  200. if self.user_action_pending:
  201. try:
  202. interaction = self.layer_manager.interaction_manager.get_for_user_action(self.user_action_pending)
  203. interaction.draw_pending()
  204. except InteractionNotFound:
  205. pass
  206. def on_enter(self):
  207. super().on_enter()
  208. scene = self.get_ancestor(cocos.scene.Scene)
  209. if self.elastic_box is None:
  210. self.elastic_box = MinMaxRect(self.layer_manager)
  211. scene.add(self.elastic_box, z=10)
  212. def update(self, dt):
  213. mx = self.buttons['right'] - self.buttons['left']
  214. my = self.buttons['up'] - self.buttons['down']
  215. dz = self.buttons['zoomin'] - self.buttons['zoomout']
  216. # scroll
  217. if self.autoscrolling:
  218. self.update_autoscroll(dt)
  219. else:
  220. # care for keyscrolling
  221. new_keyscrolling = ((len(self.selection) == 0) and
  222. (mx != 0 or my != 0))
  223. new_keyscrolling_descriptor = (mx, my)
  224. if ((new_keyscrolling != self.keyscrolling) or
  225. (new_keyscrolling_descriptor != self.keyscrolling_descriptor)):
  226. self.keyscrolling = new_keyscrolling
  227. self.keyscrolling_descriptor = new_keyscrolling_descriptor
  228. fastness = 1.0
  229. if mx != 0 and my != 0:
  230. fastness *= 0.707106 # 1/sqrt(2)
  231. self.autoscrolling_sdelta = (0.5 * fastness * mx, 0.5 * fastness * my)
  232. if self.keyscrolling:
  233. self.update_autoscroll(dt)
  234. # selection move
  235. if self.drag_moving:
  236. # update positions
  237. wx, wy = self.world_mouse
  238. dx = wx - self.wdrag_start_point[0]
  239. dy = wy - self.wdrag_start_point[1]
  240. if self.restricted_mov:
  241. if abs(dy) > abs(dx):
  242. dx = 0
  243. else:
  244. dy = 0
  245. dpos = euclid.Vector2(dx, dy)
  246. for actor in self.selection:
  247. old_pos = self.selection[actor].center
  248. new_pos = old_pos + dpos
  249. try:
  250. grid_pos = self.grid_manager.get_grid_position(new_pos)
  251. grid_pixel_pos = self.grid_manager.get_pixel_position_of_grid_position(grid_pos)
  252. actor.update_position(grid_pixel_pos)
  253. except OuterWorldPosition:
  254. # don't update position
  255. pass
  256. scroller = self.weak_scroller()
  257. # zoom
  258. zoom_change = (dz != 0 or self.wheel != 0)
  259. if zoom_change:
  260. if self.mouse_into_world():
  261. wzoom_center = self.world_mouse
  262. szoom_center = self.screen_mouse
  263. else:
  264. # decay to scroller unadorned
  265. wzoom_center = None
  266. if self.wheel != 0:
  267. dt_dz = 0.01666666 * self.wheel
  268. self.wheel = 0
  269. else:
  270. dt_dz = dt * dz
  271. zoom = scroller.scale + dt_dz * self.zoom_fastness
  272. if zoom < self.zoom_min:
  273. zoom = self.zoom_min
  274. elif zoom > self.zoom_max:
  275. zoom = self.zoom_max
  276. scroller.scale = zoom
  277. if wzoom_center is not None:
  278. # postprocess toward 'world point under mouse the same before
  279. # and after zoom' ; other restrictions may prevent fully comply
  280. wx1, wy1 = self.layer_manager.scrolling_manager.screen_to_world(*szoom_center)
  281. fx = scroller.restricted_fx + (wzoom_center[0] - wx1)
  282. fy = scroller.restricted_fy + (wzoom_center[1] - wy1)
  283. scroller.set_focus(fx, fy)
  284. def update_mouse_position(self, sx, sy):
  285. self.screen_mouse = sx, sy
  286. self.world_mouse = self.layer_manager.scrolling_manager.screen_to_world(sx, sy)
  287. # handle autoscroll
  288. border = self.autoscroll_border
  289. if border is not None:
  290. # sleft and companions includes the border
  291. scroller = self.weak_scroller()
  292. self.update_view_bounds()
  293. sdx = 0.0
  294. if sx < self.sleft:
  295. sdx = sx - self.sleft
  296. elif sx > self.sright:
  297. sdx = sx - self.sright
  298. sdy = 0.0
  299. if sy < self.sbottom:
  300. sdy = sy - self.sbottom
  301. elif sy > self.s_top:
  302. sdy = sy - self.s_top
  303. self.autoscrolling = sdx != 0.0 or sdy != 0.0
  304. if self.autoscrolling:
  305. self.autoscrolling_sdelta = (sdx / border, sdy / border)
  306. def update_autoscroll(self, dt):
  307. fraction_sdx, fraction_sdy = self.autoscrolling_sdelta
  308. scroller = self.weak_scroller()
  309. worldview = self.weak_worldview()
  310. f = self.autoscroll_fastness
  311. wdx = (fraction_sdx * f * dt) / scroller.scale / worldview.scale
  312. wdy = (fraction_sdy * f * dt) / scroller.scale / worldview.scale
  313. # ask scroller to try scroll (wdx, wdy)
  314. fx = scroller.restricted_fx + wdx
  315. fy = scroller.restricted_fy + wdy
  316. scroller.set_focus(fx, fy)
  317. self.world_mouse = self.layer_manager.scrolling_manager.screen_to_world(*self.screen_mouse)
  318. self.adjust_elastic_box()
  319. # self.update_view_bounds()
  320. def update_view_bounds(self):
  321. scroller = self.weak_scroller()
  322. scx, scy = self.layer_manager.scrolling_manager.world_to_screen(
  323. scroller.restricted_fx,
  324. scroller.restricted_fy,
  325. )
  326. hw = scroller.view_w / 2.0
  327. hh = scroller.view_h / 2.0
  328. border = self.autoscroll_border
  329. self.sleft = scx - hw + border
  330. self.sright = scx + hw - border
  331. self.sbottom = scy - hh + border
  332. self.s_top = scy + hh - border
  333. def mouse_into_world(self):
  334. worldview = self.weak_worldview()
  335. # TODO: allow lower limits != 0 ?
  336. return ((0 <= self.world_mouse[0] <= worldview.width) and
  337. (0 <= self.world_mouse[1] <= worldview.height))
  338. def on_key_press(self, k, m):
  339. binds = self.bindings
  340. self._on_key_press(k, m)
  341. if k in binds:
  342. self.buttons[binds[k]] = 1
  343. self.modifiers[binds[k]] = 1
  344. return True
  345. return False
  346. def _on_key_press(self, k, m):
  347. pass
  348. def on_key_release(self, k, m):
  349. binds = self.bindings
  350. if k in binds:
  351. self.buttons[binds[k]] = 0
  352. self.modifiers[binds[k]] = 0
  353. return True
  354. return False
  355. def on_mouse_motion(self, sx, sy, dx, dy):
  356. self.update_mouse_position(sx, sy)
  357. def on_mouse_leave(self, sx, sy):
  358. self.autoscrolling = False
  359. def on_mouse_press(self, x, y, buttons, modifiers):
  360. rx, ry = self.layer_manager.scrolling_manager.screen_to_world(x, y)
  361. self.logger.info(
  362. 'GUI click: x: {}, y: {}, rx: {}, ry: {} ({}|{})'.format(x, y, rx, ry, buttons, modifiers)
  363. )
  364. if mouse.LEFT:
  365. # Non action pending case
  366. if not self.user_action_pending:
  367. actor = self.single_actor_from_mouse()
  368. if actor:
  369. self.selection.clear()
  370. self.selection_add(actor)
  371. # Action pending case
  372. else:
  373. try:
  374. interaction = self.layer_manager.interaction_manager.get_for_user_action(self.user_action_pending)
  375. interaction.execute()
  376. except InteractionNotFound:
  377. pass
  378. if mouse.RIGHT:
  379. if self.user_action_pending:
  380. self.user_action_pending = None
  381. def on_mouse_release(self, sx, sy, button, modifiers):
  382. # should we handle here mod_restricted_mov ?
  383. wx, wy = self.layer_manager.scrolling_manager.screen_to_world(sx, sy)
  384. modify_selection = modifiers & self.mod_modify_selection
  385. if self.dragging:
  386. # ignore all buttons except left button
  387. if button != mouse.LEFT:
  388. return
  389. if self.drag_selecting:
  390. self.end_drag_selection(wx, wy, modify_selection)
  391. elif self.drag_moving:
  392. self.end_drag_move(wx, wy)
  393. self.dragging = False
  394. else:
  395. if button == mouse.LEFT:
  396. self.end_click_selection(wx, wy, modify_selection)
  397. def end_click_selection(self, wx, wy, modify_selection):
  398. under_mouse_unique = self.single_actor_from_mouse()
  399. if modify_selection:
  400. # toggle selected status for unique
  401. if under_mouse_unique in self.selection:
  402. self.selection_remove(under_mouse_unique)
  403. elif under_mouse_unique is not None:
  404. self.selection_add(under_mouse_unique)
  405. else:
  406. # new_selected becomes the current selected
  407. self.selection.clear()
  408. self.user_action_pending = None
  409. if under_mouse_unique is not None:
  410. self.selection_add(under_mouse_unique)
  411. def selection_add(self, actor):
  412. self.selection[actor] = actor.cshape.copy()
  413. def selection_remove(self, actor):
  414. del self.selection[actor]
  415. def end_drag_selection(self, wx, wy, modify_selection):
  416. new_selection = self.collision_manager.objs_into_box(*self.elastic_box_wminmax)
  417. if not modify_selection:
  418. # new_selected becomes the current selected
  419. self.selection.clear()
  420. for actor in new_selection:
  421. self.selection_add(actor)
  422. self.elastic_box.visible = False
  423. self.drag_selecting = False
  424. def on_mouse_drag(self, sx, sy, dx, dy, buttons, modifiers):
  425. # TODO: inhibir esta llamada si estamos fuera de la client area / viewport
  426. self.update_mouse_position(sx, sy)
  427. if not buttons & mouse.LEFT:
  428. # ignore except for left-btn-drag
  429. return
  430. if not self.dragging:
  431. print("begin drag")
  432. self.begin_drag()
  433. return
  434. if self.drag_selecting:
  435. # update elastic box
  436. self.adjust_elastic_box()
  437. elif self.drag_moving:
  438. self.restricted_mov = (modifiers & self.mod_restricted_mov)
  439. def adjust_elastic_box(self):
  440. # when elastic_box visible this method needs to be called any time
  441. # world_mouse changes or screen_to_world results changes (scroll, etc)
  442. wx0, wy0 = self.wdrag_start_point
  443. wx1, wy1 = self.world_mouse
  444. wminx = min(wx0, wx1)
  445. wmaxx = max(wx0, wx1)
  446. wminy = min(wy0, wy1)
  447. wmaxy = max(wy0, wy1)
  448. self.elastic_box_wminmax = wminx, wmaxx, wminy, wmaxy
  449. self.elastic_box.adjust_from_w_minmax(*self.elastic_box_wminmax)
  450. def begin_drag(self):
  451. self.dragging = True
  452. self.wdrag_start_point = self.world_mouse
  453. under_mouse_unique = self.single_actor_from_mouse()
  454. if under_mouse_unique is None:
  455. # begin drag selection
  456. self.drag_selecting = True
  457. self.adjust_elastic_box()
  458. self.elastic_box.visible = True
  459. print("begin drag selection: drag_selecting, drag_moving",
  460. self.drag_selecting, self.drag_moving)
  461. else:
  462. # want drag move
  463. if under_mouse_unique in self.selection:
  464. # want to move current selection
  465. pass
  466. else:
  467. # change selection before moving
  468. self.selection.clear()
  469. self.selection_add(under_mouse_unique)
  470. self.begin_drag_move()
  471. def begin_drag_move(self):
  472. # begin drag move
  473. self.drag_moving = True
  474. # how-to update collman: remove/add vs clear/add all
  475. # when total number of actors is low anyone will be fine,
  476. # with high numbers, most probably we move only a small fraction
  477. # For simplicity I choose remove/add, albeit a hybrid aproach
  478. # can be implemented later
  479. self.set_selection_in_collman(False)
  480. # print "begin drag: drag_selecting, drag_moving", self.drag_selecting, self.drag_moving
  481. def end_drag_move(self, wx, wy):
  482. self.set_selection_in_collman(True)
  483. for actor in self.selection:
  484. self.selection[actor] = actor.cshape.copy()
  485. self.drag_moving = False
  486. def single_actor_from_mouse(self):
  487. under_mouse = self.collision_manager.objs_touching_point(*self.world_mouse)
  488. if len(under_mouse) == 0:
  489. return None
  490. # return the one with the center most near to mouse, if tie then
  491. # an arbitrary in the tie
  492. nearest = None
  493. near_d = None
  494. p = euclid.Vector2(*self.world_mouse)
  495. for actor in under_mouse:
  496. d = (actor.cshape.center - p).magnitude_squared()
  497. if nearest is None or (d < near_d):
  498. nearest = actor
  499. near_d = d
  500. return nearest
  501. def set_selection_in_collman(self, bool_value):
  502. if self.selection_in_collman == bool_value:
  503. return
  504. self.selection_in_collman = bool_value
  505. if bool_value:
  506. for actor in self.selection:
  507. self.collision_manager.add(actor)
  508. else:
  509. for actor in self.selection:
  510. self.collision_manager.remove_tricky(actor)
  511. def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
  512. # TODO: check if mouse over scroller viewport?
  513. self.wheel += scroll_y * self.wheel_multiplier
  514. class MainLayer(ScrollableLayer):
  515. is_event_handler = True
  516. def __init__(
  517. self,
  518. layer_manager: LayerManager,
  519. grid_manager: GridManager,
  520. width: int,
  521. height: int,
  522. scroll_step: int=100,
  523. ) -> None:
  524. super().__init__()
  525. self.layer_manager = layer_manager
  526. self.scroll_step = scroll_step
  527. self.grid_manager = grid_manager
  528. self.width = width
  529. self.height = height
  530. self.px_width = width
  531. self.px_height = height
  532. class SubjectMapper(object):
  533. def __init__(
  534. self,
  535. actor_class: typing.Type[Actor],
  536. ) -> None:
  537. self.actor_class = actor_class
  538. def append(
  539. self,
  540. subject: XYZSubjectMixin,
  541. layer_manager: LayerManager,
  542. ) -> None:
  543. actor = self.actor_class(subject)
  544. pixel_position = layer_manager.grid_manager.get_pixel_position_of_grid_position(
  545. (subject.position[0], subject.position[1]),
  546. )
  547. actor.update_position(euclid.Vector2(*pixel_position))
  548. # TODO: Selectable nature must be configurable
  549. layer_manager.add_subject(actor)
  550. layer_manager.set_selectable(actor)
  551. class SubjectMapperFactory(object):
  552. def __init__(self) -> None:
  553. self.mapping = {} # type: typing.Dict[typing.Type[XYZSubjectMixin], SubjectMapper]
  554. def register_mapper(self, subject_class: typing.Type[XYZSubjectMixin], mapper: SubjectMapper) -> None:
  555. if subject_class not in self.mapping:
  556. self.mapping[subject_class] = mapper
  557. else:
  558. raise ValueError('subject_class already register with "{}"'.format(str(self.mapping[subject_class])))
  559. def get_subject_mapper(self, subject: XYZSubjectMixin) -> SubjectMapper:
  560. for subject_class, mapper in self.mapping.items():
  561. if isinstance(subject, subject_class):
  562. return mapper
  563. raise KeyError('No mapper for subject "{}"'.format(str(subject)))
  564. class Gui(object):
  565. layer_manager_class = LayerManager
  566. def __init__(
  567. self,
  568. config: Config,
  569. logger: SynergineLogger,
  570. terminal: Terminal,
  571. read_queue_interval: float= 1/60.0,
  572. ):
  573. self.config = config
  574. self.logger = logger
  575. self._read_queue_interval = read_queue_interval
  576. self.terminal = terminal
  577. self.cycle_duration = self.config.resolve('core.cycle_duration')
  578. cocos.director.director.init(
  579. width=640,
  580. height=480,
  581. vsync=True,
  582. resizable=True,
  583. )
  584. self.interaction_manager = InteractionManager(
  585. config=self.config,
  586. logger=self.logger,
  587. terminal=self.terminal,
  588. )
  589. self.layer_manager = self.layer_manager_class(
  590. self.config,
  591. self.logger,
  592. middleware=self.get_layer_middleware(),
  593. interaction_manager=self.interaction_manager,
  594. )
  595. self.layer_manager.init()
  596. self.layer_manager.center()
  597. # Enable blending
  598. pyglet.gl.glEnable(pyglet.gl.GL_BLEND)
  599. pyglet.gl.glBlendFunc(pyglet.gl.GL_SRC_ALPHA, pyglet.gl.GL_ONE_MINUS_SRC_ALPHA)
  600. # Enable transparency
  601. pyglet.gl.glEnable(pyglet.gl.GL_ALPHA_TEST)
  602. pyglet.gl.glAlphaFunc(pyglet.gl.GL_GREATER, .1)
  603. self.subject_mapper_factory = SubjectMapperFactory()
  604. def get_layer_middleware(self) -> MapMiddleware:
  605. raise NotImplementedError()
  606. def run(self):
  607. self.before_run()
  608. pyglet.clock.schedule_interval(
  609. lambda *_, **__: self.terminal.read(),
  610. self._read_queue_interval,
  611. )
  612. cocos.director.director.run(self.get_main_scene())
  613. def before_run(self) -> None:
  614. pass
  615. def get_main_scene(self) -> cocos.cocosnode.CocosNode:
  616. raise NotImplementedError()
  617. def before_received(self, package: TerminalPackage):
  618. pass
  619. def after_received(self, package: TerminalPackage):
  620. pass
  621. class TMXGui(Gui):
  622. def __init__(
  623. self,
  624. config: Config,
  625. logger: SynergineLogger,
  626. terminal: Terminal,
  627. read_queue_interval: float = 1 / 60.0,
  628. map_dir_path: str=None,
  629. ):
  630. assert map_dir_path
  631. self.map_dir_path = map_dir_path
  632. super(TMXGui, self).__init__(
  633. config,
  634. logger,
  635. terminal,
  636. read_queue_interval,
  637. )
  638. def get_layer_middleware(self) -> MapMiddleware:
  639. return TMXMiddleware(
  640. self.config,
  641. self.logger,
  642. self.map_dir_path,
  643. )
  644. def get_main_scene(self) -> cocos.cocosnode.CocosNode:
  645. return self.layer_manager.main_scene
  646. def before_received(self, package: TerminalPackage):
  647. super().before_received(package)
  648. if package.subjects: # They are new subjects in the simulation
  649. for subject in package.subjects:
  650. self.append_subject(subject)
  651. def append_subject(self, subject: XYZSubjectMixin) -> None:
  652. subject_mapper = self.subject_mapper_factory.get_subject_mapper(subject)
  653. subject_mapper.append(subject, self.layer_manager)