gui.py 28KB

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