gui.py 27KB

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