gui.py 27KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777
  1. # coding: utf-8
  2. import typing
  3. import weakref
  4. from math import floor
  5. import pyglet
  6. from pyglet.window import key
  7. from pyglet.window import mouse
  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_cocos2d.actions import MoveTo
  17. from synergine2_cocos2d.actor import Actor
  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.move import MoveEvent
  28. from synergine2_xyz.xyz import XYZSubjectMixin
  29. class GridManager(object):
  30. def __init__(
  31. self,
  32. cell_width: int,
  33. cell_height: int,
  34. world_width: int,
  35. world_height: int,
  36. ) -> None:
  37. self.cell_width = cell_width
  38. self.cell_height = cell_height
  39. self.world_width = world_width
  40. self.world_height = world_height
  41. def get_grid_position(self, pixel_position: typing.Tuple[int, int]) -> typing.Tuple[int, int]:
  42. pixel_x, pixel_y = pixel_position
  43. cell_x = int(floor(pixel_x / self.cell_width))
  44. cell_y = int(floor(pixel_y / self.cell_height))
  45. if cell_x > self.world_width or cell_y > self.world_height or cell_x < 0 or cell_y < 0:
  46. raise OuterWorldPosition('Position "{}" is outer world ({}x{})'.format(
  47. (cell_x, cell_y),
  48. self.world_width,
  49. self.world_height,
  50. ))
  51. return cell_x, cell_y
  52. def get_pixel_position_of_grid_position(self, grid_position: typing.Tuple[int, int]) -> typing.Tuple[int, int]:
  53. return grid_position[0] * self.cell_width + (self.cell_width // 2),\
  54. grid_position[1] * self.cell_height + (self.cell_height // 2)
  55. def get_rectangle_positions(
  56. self,
  57. grid_position: typing.Tuple[int, int],
  58. ) -> rectangle_positions_type:
  59. """
  60. A<---D
  61. | |
  62. B--->C
  63. :param grid_position:grid position to exploit
  64. :return: grid pixel corners positions
  65. """
  66. grid_x, grid_y = grid_position
  67. a = grid_x * self.cell_width, grid_y * self.cell_height + self.cell_height
  68. b = grid_x * self.cell_width, grid_y * self.cell_height
  69. c = grid_x * self.cell_width + self.cell_width, grid_y * self.cell_height
  70. d = grid_x * self.cell_width + self.cell_width, grid_y * self.cell_height + self.cell_height
  71. return a, d, c, b
  72. class MinMaxRect(cocos.cocosnode.CocosNode):
  73. def __init__(self, layer_manager: LayerManager):
  74. super(MinMaxRect, self).__init__()
  75. self.layer_manager = layer_manager
  76. self.color3 = (20, 20, 20)
  77. self.color3f = (0, 0, 0, 0.2)
  78. self.vertexes = [(0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)]
  79. self.visible = False
  80. def adjust_from_w_minmax(self, wminx, wmaxx, wminy, wmaxy):
  81. # asumes world to screen preserves order
  82. sminx, sminy = self.layer_manager.scrolling_manager.world_to_screen(wminx, wminy)
  83. smaxx, smaxy = self.layer_manager.scrolling_manager.world_to_screen(wmaxx, wmaxy)
  84. self.vertexes = [(sminx, sminy), (sminx, smaxy), (smaxx, smaxy), (smaxx, sminy)]
  85. def draw(self):
  86. if not self.visible:
  87. return
  88. draw_rectangle(
  89. self.vertexes,
  90. self.color3,
  91. self.color3f,
  92. )
  93. def set_vertexes_from_minmax(self, minx, maxx, miny, maxy):
  94. self.vertexes = [(minx, miny), (minx, maxy), (maxx, maxy), (maxx, miny)]
  95. class EditLayer(cocos.layer.Layer):
  96. is_event_handler = True
  97. def __init__(
  98. self,
  99. config: Config,
  100. logger: SynergineLogger,
  101. layer_manager: LayerManager,
  102. grid_manager: GridManager,
  103. worldview,
  104. bindings=None,
  105. fastness=None,
  106. autoscroll_border=10,
  107. autoscroll_fastness=None,
  108. wheel_multiplier=None,
  109. zoom_min=None,
  110. zoom_max=None,
  111. zoom_fastness=None,
  112. mod_modify_selection=None,
  113. mod_restricted_mov=None,
  114. ):
  115. # TODO: Clean init params
  116. super(EditLayer, self).__init__()
  117. self.config = config
  118. self.logger = logger
  119. self.layer_manager = layer_manager
  120. self.grid_manager = grid_manager
  121. self.bindings = bindings
  122. buttons = {}
  123. modifiers = {}
  124. for k in bindings:
  125. buttons[bindings[k]] = 0
  126. modifiers[bindings[k]] = 0
  127. self.buttons = buttons
  128. self.modifiers = modifiers
  129. self.fastness = fastness
  130. self.autoscroll_border = autoscroll_border
  131. self.autoscroll_fastness = autoscroll_fastness
  132. self.wheel_multiplier = wheel_multiplier
  133. self.zoom_min = zoom_min
  134. self.zoom_max = zoom_max
  135. self.zoom_fastness = zoom_fastness
  136. self.mod_modify_selection = mod_modify_selection
  137. self.mod_restricted_mov = mod_restricted_mov
  138. self.weak_scroller = weakref.ref(self.layer_manager.scrolling_manager)
  139. self.weak_worldview = weakref.ref(worldview)
  140. self.wwidth = worldview.width
  141. self.wheight = worldview.height
  142. self.autoscrolling = False
  143. self.drag_selecting = False
  144. self.drag_moving = False
  145. self.restricted_mov = False
  146. self.wheel = 0
  147. self.dragging = False
  148. self.keyscrolling = False
  149. self.keyscrolling_descriptor = (0, 0)
  150. self.wdrag_start_point = (0, 0)
  151. self.elastic_box = None # type: MinMaxRect
  152. self.elastic_box_wminmax = 0, 0, 0, 0
  153. self.selection = {}
  154. self.screen_mouse = (0, 0)
  155. self.world_mouse = (0, 0)
  156. self.sleft = None
  157. self.sright = None
  158. self.sbottom = None
  159. self.s_top = None
  160. self.user_action_pending = None # UserAction
  161. # opers that change cshape must ensure it goes to False,
  162. # selection opers must ensure it goes to True
  163. self.selection_in_collman = True
  164. # TODO: Hardcoded here, should be obtained from level properties or calc
  165. # from available actors or current actors in worldview
  166. gsize = 32 * 1.25
  167. self.collision_manager = collision_model.CollisionManagerGrid(
  168. -gsize,
  169. self.wwidth + gsize,
  170. -gsize,
  171. self.wheight + gsize,
  172. gsize,
  173. gsize,
  174. )
  175. self.schedule(self.update)
  176. self.selectable_actors = []
  177. def set_selectable(self, actor: Actor) -> None:
  178. self.selectable_actors.append(actor)
  179. self.collision_manager.add(actor)
  180. def unset_selectable(self, actor: Actor) -> None:
  181. self.selectable_actors.remove(actor)
  182. self.collision_manager.remove_tricky(actor)
  183. def draw(self, *args, **kwargs) -> None:
  184. self.draw_update_cshapes()
  185. self.draw_selection()
  186. self.draw_interactions()
  187. def draw_update_cshapes(self) -> None:
  188. for actor in self.selectable_actors:
  189. if actor.need_update_cshape:
  190. if self.collision_manager.knows(actor):
  191. self.collision_manager.remove_tricky(actor)
  192. actor.update_cshape()
  193. self.collision_manager.add(actor)
  194. def draw_selection(self) -> None:
  195. for actor, cshape in self.selection.items():
  196. grid_position = self.grid_manager.get_grid_position(actor.position)
  197. rect_positions = self.grid_manager.get_rectangle_positions(grid_position)
  198. draw_rectangle(
  199. self.layer_manager.scrolling_manager.world_to_screen_positions(rect_positions),
  200. (0, 81, 211),
  201. )
  202. def draw_interactions(self) -> None:
  203. if self.user_action_pending:
  204. try:
  205. interaction = self.layer_manager.interaction_manager.get_for_user_action(self.user_action_pending)
  206. interaction.draw_pending()
  207. except InteractionNotFound:
  208. pass
  209. def on_enter(self):
  210. super(EditLayer, self).on_enter()
  211. scene = self.get_ancestor(cocos.scene.Scene)
  212. if self.elastic_box is None:
  213. self.elastic_box = MinMaxRect(self.layer_manager)
  214. scene.add(self.elastic_box, z=10)
  215. def update(self, dt):
  216. mx = self.buttons['right'] - self.buttons['left']
  217. my = self.buttons['up'] - self.buttons['down']
  218. dz = self.buttons['zoomin'] - self.buttons['zoomout']
  219. # scroll
  220. if self.autoscrolling:
  221. self.update_autoscroll(dt)
  222. else:
  223. # care for keyscrolling
  224. new_keyscrolling = ((len(self.selection) == 0) and
  225. (mx != 0 or my != 0))
  226. new_keyscrolling_descriptor = (mx, my)
  227. if ((new_keyscrolling != self.keyscrolling) or
  228. (new_keyscrolling_descriptor != self.keyscrolling_descriptor)):
  229. self.keyscrolling = new_keyscrolling
  230. self.keyscrolling_descriptor = new_keyscrolling_descriptor
  231. fastness = 1.0
  232. if mx != 0 and my != 0:
  233. fastness *= 0.707106 # 1/sqrt(2)
  234. self.autoscrolling_sdelta = (0.5 * fastness * mx, 0.5 * fastness * my)
  235. if self.keyscrolling:
  236. self.update_autoscroll(dt)
  237. # selection move
  238. if self.drag_moving:
  239. # update positions
  240. wx, wy = self.world_mouse
  241. dx = wx - self.wdrag_start_point[0]
  242. dy = wy - self.wdrag_start_point[1]
  243. if self.restricted_mov:
  244. if abs(dy) > abs(dx):
  245. dx = 0
  246. else:
  247. dy = 0
  248. dpos = euclid.Vector2(dx, dy)
  249. for actor in self.selection:
  250. old_pos = self.selection[actor].center
  251. new_pos = old_pos + dpos
  252. try:
  253. grid_pos = self.grid_manager.get_grid_position(new_pos)
  254. grid_pixel_pos = self.grid_manager.get_pixel_position_of_grid_position(grid_pos)
  255. actor.update_position(grid_pixel_pos)
  256. except OuterWorldPosition:
  257. # don't update position
  258. pass
  259. scroller = self.weak_scroller()
  260. # zoom
  261. zoom_change = (dz != 0 or self.wheel != 0)
  262. if zoom_change:
  263. if self.mouse_into_world():
  264. wzoom_center = self.world_mouse
  265. szoom_center = self.screen_mouse
  266. else:
  267. # decay to scroller unadorned
  268. wzoom_center = None
  269. if self.wheel != 0:
  270. dt_dz = 0.01666666 * self.wheel
  271. self.wheel = 0
  272. else:
  273. dt_dz = dt * dz
  274. zoom = scroller.scale + dt_dz * self.zoom_fastness
  275. if zoom < self.zoom_min:
  276. zoom = self.zoom_min
  277. elif zoom > self.zoom_max:
  278. zoom = self.zoom_max
  279. scroller.scale = zoom
  280. if wzoom_center is not None:
  281. # postprocess toward 'world point under mouse the same before
  282. # and after zoom' ; other restrictions may prevent fully comply
  283. wx1, wy1 = self.layer_manager.scrolling_manager.screen_to_world(*szoom_center)
  284. fx = scroller.restricted_fx + (wzoom_center[0] - wx1)
  285. fy = scroller.restricted_fy + (wzoom_center[1] - wy1)
  286. scroller.set_focus(fx, fy)
  287. def update_mouse_position(self, sx, sy):
  288. self.screen_mouse = sx, sy
  289. self.world_mouse = self.layer_manager.scrolling_manager.screen_to_world(sx, sy)
  290. # handle autoscroll
  291. border = self.autoscroll_border
  292. if border is not None:
  293. # sleft and companions includes the border
  294. scroller = self.weak_scroller()
  295. self.update_view_bounds()
  296. sdx = 0.0
  297. if sx < self.sleft:
  298. sdx = sx - self.sleft
  299. elif sx > self.sright:
  300. sdx = sx - self.sright
  301. sdy = 0.0
  302. if sy < self.sbottom:
  303. sdy = sy - self.sbottom
  304. elif sy > self.s_top:
  305. sdy = sy - self.s_top
  306. self.autoscrolling = sdx != 0.0 or sdy != 0.0
  307. if self.autoscrolling:
  308. self.autoscrolling_sdelta = (sdx / border, sdy / border)
  309. def update_autoscroll(self, dt):
  310. fraction_sdx, fraction_sdy = self.autoscrolling_sdelta
  311. scroller = self.weak_scroller()
  312. worldview = self.weak_worldview()
  313. f = self.autoscroll_fastness
  314. wdx = (fraction_sdx * f * dt) / scroller.scale / worldview.scale
  315. wdy = (fraction_sdy * f * dt) / scroller.scale / worldview.scale
  316. # ask scroller to try scroll (wdx, wdy)
  317. fx = scroller.restricted_fx + wdx
  318. fy = scroller.restricted_fy + wdy
  319. scroller.set_focus(fx, fy)
  320. self.world_mouse = self.layer_manager.scrolling_manager.screen_to_world(*self.screen_mouse)
  321. self.adjust_elastic_box()
  322. # self.update_view_bounds()
  323. def update_view_bounds(self):
  324. scroller = self.weak_scroller()
  325. scx, scy = self.layer_manager.scrolling_manager.world_to_screen(
  326. scroller.restricted_fx,
  327. scroller.restricted_fy,
  328. )
  329. hw = scroller.view_w / 2.0
  330. hh = scroller.view_h / 2.0
  331. border = self.autoscroll_border
  332. self.sleft = scx - hw + border
  333. self.sright = scx + hw - border
  334. self.sbottom = scy - hh + border
  335. self.s_top = scy + hh - border
  336. def mouse_into_world(self):
  337. worldview = self.weak_worldview()
  338. # TODO: allow lower limits != 0 ?
  339. return ((0 <= self.world_mouse[0] <= worldview.width) and
  340. (0 <= self.world_mouse[1] <= worldview.height))
  341. def on_key_press(self, k, m):
  342. binds = self.bindings
  343. # TODO: Clarify code
  344. # Actions available if actor selected
  345. if self.selection:
  346. if k == key.M:
  347. self.user_action_pending = UserAction.ORDER_MOVE
  348. if k in binds:
  349. self.buttons[binds[k]] = 1
  350. self.modifiers[binds[k]] = 1
  351. return True
  352. return False
  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. def __init__(
  571. self,
  572. config: Config,
  573. logger: SynergineLogger,
  574. terminal: Terminal,
  575. read_queue_interval: float= 1/60.0,
  576. ):
  577. self.config = config
  578. self.logger = logger
  579. self._read_queue_interval = read_queue_interval
  580. self.terminal = terminal
  581. self.cycle_duration = self.config.core.cycle_duration
  582. cocos.director.director.init(
  583. width=640,
  584. height=480,
  585. vsync=True,
  586. resizable=True,
  587. )
  588. self.interaction_manager = InteractionManager(
  589. config=self.config,
  590. logger=self.logger,
  591. terminal=self.terminal,
  592. )
  593. self.layer_manager = LayerManager(
  594. self.config,
  595. self.logger,
  596. middleware=self.get_layer_middleware(),
  597. interaction_manager=self.interaction_manager,
  598. )
  599. self.layer_manager.init()
  600. self.layer_manager.center()
  601. # Enable blending
  602. pyglet.gl.glEnable(pyglet.gl.GL_BLEND)
  603. pyglet.gl.glBlendFunc(pyglet.gl.GL_SRC_ALPHA, pyglet.gl.GL_ONE_MINUS_SRC_ALPHA)
  604. # Enable transparency
  605. pyglet.gl.glEnable(pyglet.gl.GL_ALPHA_TEST)
  606. pyglet.gl.glAlphaFunc(pyglet.gl.GL_GREATER, .1)
  607. self.subject_mapper_factory = SubjectMapperFactory()
  608. def get_layer_middleware(self) -> MapMiddleware:
  609. raise NotImplementedError()
  610. def run(self):
  611. self.before_run()
  612. pyglet.clock.schedule_interval(
  613. lambda *_, **__: self.terminal.read(),
  614. self._read_queue_interval,
  615. )
  616. cocos.director.director.run(self.get_main_scene())
  617. def before_run(self) -> None:
  618. pass
  619. def get_main_scene(self) -> cocos.cocosnode.CocosNode:
  620. raise NotImplementedError()
  621. def before_received(self, package: TerminalPackage):
  622. pass
  623. def after_received(self, package: TerminalPackage):
  624. pass
  625. class TMXGui(Gui):
  626. def __init__(
  627. self,
  628. config: Config,
  629. logger: SynergineLogger,
  630. terminal: Terminal,
  631. read_queue_interval: float = 1 / 60.0,
  632. map_dir_path: str=None,
  633. ):
  634. assert map_dir_path
  635. self.map_dir_path = map_dir_path
  636. super(TMXGui, self).__init__(
  637. config,
  638. logger,
  639. terminal,
  640. read_queue_interval,
  641. )
  642. self.terminal.register_event_handler(
  643. MoveEvent,
  644. self.move_subject,
  645. )
  646. def get_layer_middleware(self) -> MapMiddleware:
  647. return TMXMiddleware(
  648. self.config,
  649. self.logger,
  650. self.map_dir_path,
  651. )
  652. def get_main_scene(self) -> cocos.cocosnode.CocosNode:
  653. return self.layer_manager.main_scene
  654. def before_received(self, package: TerminalPackage):
  655. super().before_received(package)
  656. if package.subjects: # They are new subjects in the simulation
  657. for subject in package.subjects:
  658. self.append_subject(subject)
  659. def append_subject(self, subject: XYZSubjectMixin) -> None:
  660. subject_mapper = self.subject_mapper_factory.get_subject_mapper(subject)
  661. subject_mapper.append(subject, self.layer_manager)
  662. def move_subject(self, event: MoveEvent):
  663. actor = self.layer_manager.subject_layer.subjects_index[event.subject_id]
  664. new_world_position = self.layer_manager.grid_manager.get_pixel_position_of_grid_position(event.position)
  665. new_window_position = self.layer_manager.scrolling_manager.world_to_screen(*new_world_position)
  666. move_action = MoveTo(new_window_position, 0.5)
  667. actor.do(move_action)