gui.py 29KB

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