gui.py 27KB

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