gui.py 26KB

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