gui.py 26KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. # coding: utf-8
  2. import typing
  3. import weakref
  4. from math import floor
  5. import pyglet
  6. from pyglet.window import mouse
  7. import cocos
  8. from cocos import collision_model
  9. from cocos import euclid
  10. from cocos.director import director
  11. from cocos.layer import Layer
  12. from cocos.layer import ScrollableLayer
  13. from cocos.sprite import Sprite
  14. from synergine2.config import Config
  15. from synergine2.log import SynergineLogger
  16. from synergine2.simulation import Subject
  17. from synergine2.terminals import Terminal
  18. from synergine2.terminals import TerminalPackage
  19. from synergine2.xyz import XYZSubjectMixin
  20. from synergine2_cocos2d.actor import Actor
  21. from synergine2_cocos2d.exception import OuterWorldPosition
  22. from synergine2_cocos2d.layer import LayerManager
  23. from synergine2_cocos2d.middleware import TMXMiddleware
  24. # class GridManager(object):
  25. # def __init__(
  26. # self,
  27. # layer: Layer,
  28. # square_width: int,
  29. # border: int=0,
  30. # ):
  31. # self.layer = layer
  32. # self.square_width = square_width
  33. # self.border = border
  34. #
  35. # @property
  36. # def final_width(self):
  37. # return self.square_width + self.border
  38. #
  39. # def scale_sprite(self, sprite: Sprite):
  40. # sprite.scale_x = self.final_width / sprite.image.width
  41. # sprite.scale_y = self.final_width / sprite.image.height
  42. #
  43. # def position_sprite(self, sprite: Sprite, grid_position):
  44. # grid_x = grid_position[0]
  45. # grid_y = grid_position[1]
  46. # sprite.position = grid_x * self.final_width, grid_y * self.final_width
  47. #
  48. # def get_window_position(self, grid_position_x, grid_position_y):
  49. # grid_x = grid_position_x
  50. # grid_y = grid_position_y
  51. # return grid_x * self.final_width, grid_y * self.final_width
  52. #
  53. # def get_grid_position(self, window_x, window_y, z=0) -> tuple:
  54. # window_size = director.get_window_size()
  55. #
  56. # window_center_x = window_size[0] // 2
  57. # window_center_y = window_size[1] // 2
  58. #
  59. # window_relative_x = window_x - window_center_x
  60. # window_relative_y = window_y - window_center_y
  61. #
  62. # real_width = self.final_width * self.layer.scale
  63. #
  64. # return int(window_relative_x // real_width),\
  65. # int(window_relative_y // real_width),\
  66. # z
  67. #
  68. #
  69. # class GridLayerMixin(object):
  70. # def __init__(self, *args, **kwargs):
  71. # square_width = kwargs.pop('square_width', 32)
  72. # square_border = kwargs.pop('square_border', 2)
  73. # self.grid_manager = GridManager(
  74. # self,
  75. # square_width=square_width,
  76. # border=square_border,
  77. # )
  78. # super().__init__(*args, **kwargs)
  79. class GridManager(object):
  80. def __init__(
  81. self,
  82. cell_width: int,
  83. cell_height: int,
  84. world_width: int,
  85. world_height: int,
  86. ) -> None:
  87. self.cell_width = cell_width
  88. self.cell_height = cell_height
  89. self.world_width = world_width
  90. self.world_height = world_height
  91. def get_grid_position(self, pixel_position: typing.Tuple[int, int]) -> typing.Tuple[int, int]:
  92. pixel_x, pixel_y = pixel_position
  93. cell_x = int(floor(pixel_x / self.cell_width))
  94. cell_y = int(floor(pixel_y / self.cell_height))
  95. if cell_x > self.world_width or cell_y > self.world_height or cell_x < 0 or cell_y < 0:
  96. raise OuterWorldPosition('Position "{}" is outer world ({}x{})'.format(
  97. (cell_x, cell_y),
  98. self.world_width,
  99. self.world_height,
  100. ))
  101. return cell_x, cell_y
  102. def get_pixel_position_of_grid_position(self, grid_position: typing.Tuple[int, int]) -> typing.Tuple[int, int]:
  103. return grid_position[0] * self.cell_width + self.cell_width,\
  104. grid_position[1] * self.cell_height + self.cell_height
  105. class MinMaxRect(cocos.cocosnode.CocosNode):
  106. def __init__(self, layer_manager: LayerManager):
  107. super(MinMaxRect, self).__init__()
  108. self.layer_manager = layer_manager
  109. self.color3 = (20, 20, 20)
  110. self.vertexes = [(0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)]
  111. self.visible = False
  112. def adjust_from_w_minmax(self, wminx, wmaxx, wminy, wmaxy):
  113. # asumes world to screen preserves order
  114. sminx, sminy = self.layer_manager.scrolling_manager.world_to_screen(wminx, wminy)
  115. smaxx, smaxy = self.layer_manager.scrolling_manager.world_to_screen(wmaxx, wmaxy)
  116. self.vertexes = [(sminx, sminy), (sminx, smaxy), (smaxx, smaxy), (smaxx, sminy)]
  117. def draw(self):
  118. if not self.visible:
  119. return
  120. pyglet.gl.glLineWidth(1) # deprecated
  121. pyglet.gl.glColor3ub(*self.color3)
  122. pyglet.gl.glBegin(pyglet.gl.GL_LINE_STRIP)
  123. for v in self.vertexes:
  124. pyglet.gl.glVertex2f(*v)
  125. pyglet.gl.glVertex2f(*self.vertexes[0])
  126. pyglet.gl.glEnd()
  127. # rectangle
  128. pyglet.gl.glColor4f(0, 0, 0, 0.5)
  129. pyglet.gl.glBegin(pyglet.gl.GL_QUADS)
  130. pyglet.gl.glVertex3f(self.vertexes[0][0], self.vertexes[0][1], 0)
  131. pyglet.gl.glVertex3f(self.vertexes[1][0], self.vertexes[1][1], 0)
  132. pyglet.gl.glVertex3f(self.vertexes[2][0], self.vertexes[2][1], 0)
  133. pyglet.gl.glVertex3f(self.vertexes[3][0], self.vertexes[3][1], 0)
  134. pyglet.gl.glEnd()
  135. def set_vertexes_from_minmax(self, minx, maxx, miny, maxy):
  136. self.vertexes = [(minx, miny), (minx, maxy), (maxx, maxy), (maxx, miny)]
  137. class EditLayer(cocos.layer.Layer):
  138. is_event_handler = True
  139. def __init__(
  140. self,
  141. config: Config,
  142. logger: SynergineLogger,
  143. layer_manager: LayerManager,
  144. grid_manager: GridManager,
  145. worldview,
  146. bindings=None,
  147. fastness=None,
  148. autoscroll_border=10,
  149. autoscroll_fastness=None,
  150. wheel_multiplier=None,
  151. zoom_min=None,
  152. zoom_max=None,
  153. zoom_fastness=None,
  154. mod_modify_selection=None,
  155. mod_restricted_mov=None,
  156. ):
  157. # TODO: Clean init params
  158. super(EditLayer, self).__init__()
  159. self.config = config
  160. self.logger = logger
  161. self.layer_manager = layer_manager
  162. self.grid_manager = grid_manager
  163. self.bindings = bindings
  164. buttons = {}
  165. modifiers = {}
  166. for k in bindings:
  167. buttons[bindings[k]] = 0
  168. modifiers[bindings[k]] = 0
  169. self.buttons = buttons
  170. self.modifiers = modifiers
  171. self.fastness = fastness
  172. self.autoscroll_border = autoscroll_border
  173. self.autoscroll_fastness = autoscroll_fastness
  174. self.wheel_multiplier = wheel_multiplier
  175. self.zoom_min = zoom_min
  176. self.zoom_max = zoom_max
  177. self.zoom_fastness = zoom_fastness
  178. self.mod_modify_selection = mod_modify_selection
  179. self.mod_restricted_mov = mod_restricted_mov
  180. self.weak_scroller = weakref.ref(self.layer_manager.scrolling_manager)
  181. self.weak_worldview = weakref.ref(worldview)
  182. self.wwidth = worldview.width
  183. self.wheight = worldview.height
  184. self.autoscrolling = False
  185. self.drag_selecting = False
  186. self.drag_moving = False
  187. self.restricted_mov = False
  188. self.wheel = 0
  189. self.dragging = False
  190. self.keyscrolling = False
  191. self.keyscrolling_descriptor = (0, 0)
  192. self.wdrag_start_point = (0, 0)
  193. self.elastic_box = None # type: MinMaxRect
  194. self.elastic_box_wminmax = 0, 0, 0, 0
  195. self.selection = {}
  196. self.screen_mouse = (0, 0)
  197. self.world_mouse = (0, 0)
  198. self.sleft = None
  199. self.sright = None
  200. self.sbottom = None
  201. self.s_top = None
  202. # opers that change cshape must ensure it goes to False,
  203. # selection opers must ensure it goes to True
  204. self.selection_in_collman = True
  205. # TODO: Hardcoded here, should be obtained from level properties or calc
  206. # from available actors or current actors in worldview
  207. gsize = 32 * 1.25
  208. self.collision_manager = collision_model.CollisionManagerGrid(
  209. -gsize,
  210. self.wwidth + gsize,
  211. -gsize,
  212. self.wheight + gsize,
  213. gsize,
  214. gsize,
  215. )
  216. self.schedule(self.update)
  217. self.selectable_actors = []
  218. def set_selectable(self, actor: Actor) -> None:
  219. self.selectable_actors.append(actor)
  220. self.collision_manager.add(actor)
  221. def unset_selectable(self, actor: Actor) -> None:
  222. self.selectable_actors.remove(actor)
  223. self.collision_manager.remove_tricky(actor)
  224. def draw(self, *args, **kwargs) -> None:
  225. for actor in self.selectable_actors:
  226. if actor.need_update_cshape:
  227. if self.collision_manager.knows(actor):
  228. self.collision_manager.remove_tricky(actor)
  229. actor.update_cshape()
  230. self.collision_manager.add(actor)
  231. def on_enter(self):
  232. super(EditLayer, self).on_enter()
  233. scene = self.get_ancestor(cocos.scene.Scene)
  234. if self.elastic_box is None:
  235. self.elastic_box = MinMaxRect(self.layer_manager)
  236. scene.add(self.elastic_box, z=10)
  237. def update(self, dt):
  238. mx = self.buttons['right'] - self.buttons['left']
  239. my = self.buttons['up'] - self.buttons['down']
  240. dz = self.buttons['zoomin'] - self.buttons['zoomout']
  241. # scroll
  242. if self.autoscrolling:
  243. self.update_autoscroll(dt)
  244. else:
  245. # care for keyscrolling
  246. new_keyscrolling = ((len(self.selection) == 0) and
  247. (mx != 0 or my != 0))
  248. new_keyscrolling_descriptor = (mx, my)
  249. if ((new_keyscrolling != self.keyscrolling) or
  250. (new_keyscrolling_descriptor != self.keyscrolling_descriptor)):
  251. self.keyscrolling = new_keyscrolling
  252. self.keyscrolling_descriptor = new_keyscrolling_descriptor
  253. fastness = 1.0
  254. if mx != 0 and my != 0:
  255. fastness *= 0.707106 # 1/sqrt(2)
  256. self.autoscrolling_sdelta = (0.5 * fastness * mx, 0.5 * fastness * my)
  257. if self.keyscrolling:
  258. self.update_autoscroll(dt)
  259. # selection move
  260. if self.drag_moving:
  261. # update positions
  262. wx, wy = self.world_mouse
  263. dx = wx - self.wdrag_start_point[0]
  264. dy = wy - self.wdrag_start_point[1]
  265. if self.restricted_mov:
  266. if abs(dy) > abs(dx):
  267. dx = 0
  268. else:
  269. dy = 0
  270. dpos = euclid.Vector2(dx, dy)
  271. for actor in self.selection:
  272. old_pos = self.selection[actor].center
  273. new_pos = old_pos + dpos
  274. try:
  275. grid_pos = self.grid_manager.get_grid_position(new_pos)
  276. grid_pixel_pos = self.grid_manager.get_pixel_position_of_grid_position(grid_pos)
  277. actor.update_position(grid_pixel_pos)
  278. except OuterWorldPosition:
  279. # don't update position
  280. pass
  281. scroller = self.weak_scroller()
  282. # zoom
  283. zoom_change = (dz != 0 or self.wheel != 0)
  284. if zoom_change:
  285. if self.mouse_into_world():
  286. wzoom_center = self.world_mouse
  287. szoom_center = self.screen_mouse
  288. else:
  289. # decay to scroller unadorned
  290. wzoom_center = None
  291. if self.wheel != 0:
  292. dt_dz = 0.01666666 * self.wheel
  293. self.wheel = 0
  294. else:
  295. dt_dz = dt * dz
  296. zoom = scroller.scale + dt_dz * self.zoom_fastness
  297. if zoom < self.zoom_min:
  298. zoom = self.zoom_min
  299. elif zoom > self.zoom_max:
  300. zoom = self.zoom_max
  301. scroller.scale = zoom
  302. if wzoom_center is not None:
  303. # postprocess toward 'world point under mouse the same before
  304. # and after zoom' ; other restrictions may prevent fully comply
  305. wx1, wy1 = self.layer_manager.scrolling_manager.screen_to_world(*szoom_center)
  306. fx = scroller.restricted_fx + (wzoom_center[0] - wx1)
  307. fy = scroller.restricted_fy + (wzoom_center[1] - wy1)
  308. scroller.set_focus(fx, fy)
  309. def update_mouse_position(self, sx, sy):
  310. self.screen_mouse = sx, sy
  311. self.world_mouse = self.layer_manager.scrolling_manager.screen_to_world(sx, sy)
  312. # handle autoscroll
  313. border = self.autoscroll_border
  314. if border is not None:
  315. # sleft and companions includes the border
  316. scroller = self.weak_scroller()
  317. self.update_view_bounds()
  318. sdx = 0.0
  319. if sx < self.sleft:
  320. sdx = sx - self.sleft
  321. elif sx > self.sright:
  322. sdx = sx - self.sright
  323. sdy = 0.0
  324. if sy < self.sbottom:
  325. sdy = sy - self.sbottom
  326. elif sy > self.s_top:
  327. sdy = sy - self.s_top
  328. self.autoscrolling = sdx != 0.0 or sdy != 0.0
  329. if self.autoscrolling:
  330. self.autoscrolling_sdelta = (sdx / border, sdy / border)
  331. def update_autoscroll(self, dt):
  332. fraction_sdx, fraction_sdy = self.autoscrolling_sdelta
  333. scroller = self.weak_scroller()
  334. worldview = self.weak_worldview()
  335. f = self.autoscroll_fastness
  336. wdx = (fraction_sdx * f * dt) / scroller.scale / worldview.scale
  337. wdy = (fraction_sdy * f * dt) / scroller.scale / worldview.scale
  338. # ask scroller to try scroll (wdx, wdy)
  339. fx = scroller.restricted_fx + wdx
  340. fy = scroller.restricted_fy + wdy
  341. scroller.set_focus(fx, fy)
  342. self.world_mouse = self.layer_manager.scrolling_manager.screen_to_world(*self.screen_mouse)
  343. self.adjust_elastic_box()
  344. # self.update_view_bounds()
  345. def update_view_bounds(self):
  346. scroller = self.weak_scroller()
  347. scx, scy = self.layer_manager.scrolling_manager.world_to_screen(
  348. scroller.restricted_fx,
  349. scroller.restricted_fy,
  350. )
  351. hw = scroller.view_w / 2.0
  352. hh = scroller.view_h / 2.0
  353. border = self.autoscroll_border
  354. self.sleft = scx - hw + border
  355. self.sright = scx + hw - border
  356. self.sbottom = scy - hh + border
  357. self.s_top = scy + hh - border
  358. def mouse_into_world(self):
  359. worldview = self.weak_worldview()
  360. # TODO: allow lower limits != 0 ?
  361. return ((0 <= self.world_mouse[0] <= worldview.width) and
  362. (0 <= self.world_mouse[1] <= worldview.height))
  363. def on_key_press(self, k, m):
  364. binds = self.bindings
  365. if k in binds:
  366. self.buttons[binds[k]] = 1
  367. self.modifiers[binds[k]] = 1
  368. return True
  369. return False
  370. def on_key_release(self, k, m):
  371. binds = self.bindings
  372. if k in binds:
  373. self.buttons[binds[k]] = 0
  374. self.modifiers[binds[k]] = 0
  375. return True
  376. return False
  377. def on_mouse_motion(self, sx, sy, dx, dy):
  378. self.update_mouse_position(sx, sy)
  379. def on_mouse_leave(self, sx, sy):
  380. self.autoscrolling = False
  381. def on_mouse_press(self, x, y, buttons, modifiers):
  382. if self.logger.is_debug:
  383. rx, ry = self.layer_manager.scrolling_manager.screen_to_world(x, y)
  384. self.logger.debug('GUI click: x: {}, y: {}, rx: {}, ry: {}'.format(x, y, rx, ry))
  385. def on_mouse_release(self, sx, sy, button, modifiers):
  386. # should we handle here mod_restricted_mov ?
  387. wx, wy = self.layer_manager.scrolling_manager.screen_to_world(sx, sy)
  388. modify_selection = modifiers & self.mod_modify_selection
  389. if self.dragging:
  390. # ignore all buttons except left button
  391. if button != mouse.LEFT:
  392. return
  393. if self.drag_selecting:
  394. self.end_drag_selection(wx, wy, modify_selection)
  395. elif self.drag_moving:
  396. self.end_drag_move(wx, wy)
  397. self.dragging = False
  398. else:
  399. if button == mouse.LEFT:
  400. self.end_click_selection(wx, wy, modify_selection)
  401. def end_click_selection(self, wx, wy, modify_selection):
  402. under_mouse_unique = self.single_actor_from_mouse()
  403. if modify_selection:
  404. # toggle selected status for unique
  405. if under_mouse_unique in self.selection:
  406. self.selection_remove(under_mouse_unique)
  407. elif under_mouse_unique is not None:
  408. self.selection_add(under_mouse_unique)
  409. else:
  410. # new_selected becomes the current selected
  411. self.selection.clear()
  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()
  547. pixel_position = layer_manager.grid_manager.get_pixel_position_of_grid_position((
  548. subject.position[0],
  549. subject.position[1],
  550. ))
  551. actor.update_position(euclid.Vector2(*pixel_position))
  552. # TODO: Selectable nature must be configurable
  553. layer_manager.add_subject(actor)
  554. layer_manager.set_selectable(actor)
  555. class SubjectMapperFactory(object):
  556. def __init__(self) -> None:
  557. self.mapping = {} # type: typing.Dict[typing.Type[XYZSubjectMixin], SubjectMapper]
  558. def register_mapper(self, subject_class: typing.Type[XYZSubjectMixin], mapper: SubjectMapper) -> None:
  559. if subject_class not in self.mapping:
  560. self.mapping[subject_class] = mapper
  561. else:
  562. raise ValueError('subject_class already register with "{}"'.format(str(self.mapping[subject_class])))
  563. def get_subject_mapper(self, subject: XYZSubjectMixin) -> SubjectMapper:
  564. for subject_class, mapper in self.mapping.items():
  565. if isinstance(subject, subject_class):
  566. return mapper
  567. raise KeyError('No mapper for subject "{}"'.format(str(subject)))
  568. class Gui(object):
  569. def __init__(
  570. self,
  571. config: Config,
  572. logger: SynergineLogger,
  573. terminal: Terminal,
  574. read_queue_interval: float= 1/60.0,
  575. ):
  576. self.config = config
  577. self.logger = logger
  578. self._read_queue_interval = read_queue_interval
  579. self.terminal = terminal
  580. self.cycle_duration = self.config.core.cycle_duration
  581. cocos.director.director.init(
  582. width=640,
  583. height=480,
  584. vsync=True,
  585. resizable=True,
  586. )
  587. # Enable blending
  588. pyglet.gl.glEnable(pyglet.gl.GL_BLEND)
  589. pyglet.gl.glBlendFunc(pyglet.gl.GL_SRC_ALPHA, pyglet.gl.GL_ONE_MINUS_SRC_ALPHA)
  590. # Enable transparency
  591. pyglet.gl.glEnable(pyglet.gl.GL_ALPHA_TEST)
  592. pyglet.gl.glAlphaFunc(pyglet.gl.GL_GREATER, .1)
  593. self.subject_mapper_factory = SubjectMapperFactory()
  594. def run(self):
  595. self.before_run()
  596. pyglet.clock.schedule_interval(
  597. lambda *_, **__: self.terminal.read(),
  598. self._read_queue_interval,
  599. )
  600. cocos.director.director.run(self.get_main_scene())
  601. def before_run(self) -> None:
  602. pass
  603. def get_main_scene(self) -> cocos.cocosnode.CocosNode:
  604. raise NotImplementedError()
  605. def before_received(self, package: TerminalPackage):
  606. pass
  607. def after_received(self, package: TerminalPackage):
  608. pass
  609. class TMXGui(Gui):
  610. def __init__(
  611. self,
  612. config: Config,
  613. logger: SynergineLogger,
  614. terminal: Terminal,
  615. read_queue_interval: float = 1 / 60.0,
  616. map_dir_path: str=None,
  617. ):
  618. assert map_dir_path
  619. super(TMXGui, self).__init__(
  620. config,
  621. logger,
  622. terminal,
  623. read_queue_interval,
  624. )
  625. self.map_dir_path = map_dir_path
  626. self.layer_manager = LayerManager(
  627. self.config,
  628. self.logger,
  629. middleware=TMXMiddleware(
  630. self.config,
  631. self.logger,
  632. self.map_dir_path,
  633. ),
  634. )
  635. self.layer_manager.init()
  636. self.layer_manager.center()
  637. def get_main_scene(self) -> cocos.cocosnode.CocosNode:
  638. return self.layer_manager.main_scene
  639. def before_received(self, package: TerminalPackage):
  640. super().before_received(package)
  641. if package.subjects: # They are new subjects in the simulation
  642. for subject in package.subjects:
  643. self.append_subject(subject)
  644. def append_subject(self, subject: XYZSubjectMixin) -> None:
  645. subject_mapper = self.subject_mapper_factory.get_subject_mapper(subject)
  646. subject_mapper.append(subject, self.layer_manager)