gui.py 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. # coding: utf-8
  2. import weakref
  3. import cocos
  4. import pyglet
  5. from pyglet.window import mouse
  6. from cocos import collision_model
  7. from cocos import euclid
  8. from cocos.director import director
  9. from cocos.layer import ScrollableLayer
  10. from cocos.layer import Layer
  11. from cocos.sprite import Sprite
  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.layer import LayerManager
  17. from synergine2_cocos2d.middleware import TMXMiddleware
  18. class Actor(cocos.sprite.Sprite):
  19. def __init__(
  20. self,
  21. image,
  22. position=(0, 0),
  23. rotation=0,
  24. scale=1,
  25. opacity=255,
  26. color=(255, 255, 255),
  27. anchor=None,
  28. **kwargs
  29. ):
  30. super().__init__(
  31. image,
  32. position,
  33. rotation,
  34. scale,
  35. opacity,
  36. color,
  37. anchor,
  38. **kwargs
  39. )
  40. self.cshape = collision_model.AARectShape(
  41. euclid.Vector2(0.0, 0.0),
  42. self.width,
  43. self.height,
  44. )
  45. def update_position(self, new_position: euclid.Vector2) -> None:
  46. self.position = new_position
  47. self.cshape.center = new_position
  48. class GridManager(object):
  49. def __init__(
  50. self,
  51. layer: Layer,
  52. square_width: int,
  53. border: int=0,
  54. ):
  55. self.layer = layer
  56. self.square_width = square_width
  57. self.border = border
  58. @property
  59. def final_width(self):
  60. return self.square_width + self.border
  61. def scale_sprite(self, sprite: Sprite):
  62. sprite.scale_x = self.final_width / sprite.image.width
  63. sprite.scale_y = self.final_width / sprite.image.height
  64. def position_sprite(self, sprite: Sprite, grid_position):
  65. grid_x = grid_position[0]
  66. grid_y = grid_position[1]
  67. sprite.position = grid_x * self.final_width, grid_y * self.final_width
  68. def get_window_position(self, grid_position_x, grid_position_y):
  69. grid_x = grid_position_x
  70. grid_y = grid_position_y
  71. return grid_x * self.final_width, grid_y * self.final_width
  72. def get_grid_position(self, window_x, window_y, z=0) -> tuple:
  73. window_size = director.get_window_size()
  74. window_center_x = window_size[0] // 2
  75. window_center_y = window_size[1] // 2
  76. window_relative_x = window_x - window_center_x
  77. window_relative_y = window_y - window_center_y
  78. real_width = self.final_width * self.layer.scale
  79. return int(window_relative_x // real_width),\
  80. int(window_relative_y // real_width),\
  81. z
  82. class GridLayerMixin(object):
  83. def __init__(self, *args, **kwargs):
  84. square_width = kwargs.pop('square_width', 32)
  85. square_border = kwargs.pop('square_border', 2)
  86. self.grid_manager = GridManager(
  87. self,
  88. square_width=square_width,
  89. border=square_border,
  90. )
  91. super().__init__(*args, **kwargs)
  92. class MinMaxRect(cocos.cocosnode.CocosNode):
  93. def __init__(self, layer_manager: LayerManager):
  94. super(MinMaxRect, self).__init__()
  95. self.layer_manager = layer_manager
  96. self.color3 = (20, 20, 20)
  97. self.vertexes = [(0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)]
  98. self.visible = False
  99. def adjust_from_w_minmax(self, wminx, wmaxx, wminy, wmaxy):
  100. # asumes world to screen preserves order
  101. sminx, sminy = self.layer_manager.scrolling_manager.world_to_screen(wminx, wminy)
  102. smaxx, smaxy = self.layer_manager.scrolling_manager.world_to_screen(wmaxx, wmaxy)
  103. self.vertexes = [(sminx, sminy), (sminx, smaxy), (smaxx, smaxy), (smaxx, sminy)]
  104. def draw(self):
  105. if not self.visible:
  106. return
  107. pyglet.gl.glLineWidth(1) # deprecated
  108. pyglet.gl.glColor3ub(*self.color3)
  109. pyglet.gl.glBegin(pyglet.gl.GL_LINE_STRIP)
  110. for v in self.vertexes:
  111. pyglet.gl.glVertex2f(*v)
  112. pyglet.gl.glVertex2f(*self.vertexes[0])
  113. pyglet.gl.glEnd()
  114. # rectangle
  115. pyglet.gl.glColor4f(0, 0, 0, 0.5)
  116. pyglet.gl.glBegin(pyglet.gl.GL_QUADS)
  117. pyglet.gl.glVertex3f(self.vertexes[0][0], self.vertexes[0][1], 0)
  118. pyglet.gl.glVertex3f(self.vertexes[1][0], self.vertexes[1][1], 0)
  119. pyglet.gl.glVertex3f(self.vertexes[2][0], self.vertexes[2][1], 0)
  120. pyglet.gl.glVertex3f(self.vertexes[3][0], self.vertexes[3][1], 0)
  121. pyglet.gl.glEnd()
  122. def set_vertexes_from_minmax(self, minx, maxx, miny, maxy):
  123. self.vertexes = [(minx, miny), (minx, maxy), (maxx, maxy), (maxx, miny)]
  124. class EditLayer(cocos.layer.Layer):
  125. is_event_handler = True
  126. def __init__(
  127. self,
  128. layer_manager: LayerManager,
  129. worldview,
  130. bindings=None,
  131. fastness=None,
  132. autoscroll_border=10,
  133. autoscroll_fastness=None,
  134. wheel_multiplier=None,
  135. zoom_min=None,
  136. zoom_max=None,
  137. zoom_fastness=None,
  138. mod_modify_selection=None,
  139. mod_restricted_mov=None,
  140. ):
  141. super(EditLayer, self).__init__()
  142. self.layer_manager = layer_manager
  143. self.bindings = bindings
  144. buttons = {}
  145. modifiers = {}
  146. for k in bindings:
  147. buttons[bindings[k]] = 0
  148. modifiers[bindings[k]] = 0
  149. self.buttons = buttons
  150. self.modifiers = modifiers
  151. self.fastness = fastness
  152. self.autoscroll_border = autoscroll_border
  153. self.autoscroll_fastness = autoscroll_fastness
  154. self.wheel_multiplier = wheel_multiplier
  155. self.zoom_min = zoom_min
  156. self.zoom_max = zoom_max
  157. self.zoom_fastness = zoom_fastness
  158. self.mod_modify_selection = mod_modify_selection
  159. self.mod_restricted_mov = mod_restricted_mov
  160. self.weak_scroller = weakref.ref(self.layer_manager.scrolling_manager)
  161. self.weak_worldview = weakref.ref(worldview)
  162. self.wwidth = worldview.width
  163. self.wheight = worldview.height
  164. self.autoscrolling = False
  165. self.drag_selecting = False
  166. self.drag_moving = False
  167. self.restricted_mov = False
  168. self.wheel = 0
  169. self.dragging = False
  170. self.keyscrolling = False
  171. self.keyscrolling_descriptor = (0, 0)
  172. self.wdrag_start_point = (0, 0)
  173. self.elastic_box = None # type: MinMaxRect
  174. self.elastic_box_wminmax = 0, 0, 0, 0
  175. self.selection = {}
  176. self.screen_mouse = (0, 0)
  177. self.world_mouse = (0, 0)
  178. self.sleft = None
  179. self.sright = None
  180. self.sbottom = None
  181. self.s_top = None
  182. # opers that change cshape must ensure it goes to False,
  183. # selection opers must ensure it goes to True
  184. self.selection_in_collman = True
  185. # TODO: Hardcoded here, should be obtained from level properties or calc
  186. # from available actors or current actors in worldview
  187. gsize = 32 * 1.25
  188. self.collision_manager = collision_model.CollisionManagerGrid(
  189. -gsize,
  190. self.wwidth + gsize,
  191. -gsize,
  192. self.wheight + gsize,
  193. gsize,
  194. gsize,
  195. )
  196. self.schedule(self.update)
  197. def on_enter(self):
  198. super(EditLayer, self).on_enter()
  199. scene = self.get_ancestor(cocos.scene.Scene)
  200. if self.elastic_box is None:
  201. self.elastic_box = MinMaxRect(self.layer_manager)
  202. scene.add(self.elastic_box, z=10)
  203. def update(self, dt):
  204. mx = self.buttons['right'] - self.buttons['left']
  205. my = self.buttons['up'] - self.buttons['down']
  206. dz = self.buttons['zoomin'] - self.buttons['zoomout']
  207. # scroll
  208. if self.autoscrolling:
  209. self.update_autoscroll(dt)
  210. else:
  211. # care for keyscrolling
  212. new_keyscrolling = ((len(self.selection) == 0) and
  213. (mx != 0 or my != 0))
  214. new_keyscrolling_descriptor = (mx, my)
  215. if ((new_keyscrolling != self.keyscrolling) or
  216. (new_keyscrolling_descriptor != self.keyscrolling_descriptor)):
  217. self.keyscrolling = new_keyscrolling
  218. self.keyscrolling_descriptor = new_keyscrolling_descriptor
  219. fastness = 1.0
  220. if mx != 0 and my != 0:
  221. fastness *= 0.707106 # 1/sqrt(2)
  222. self.autoscrolling_sdelta = (0.5 * fastness * mx, 0.5 * fastness * my)
  223. if self.keyscrolling:
  224. self.update_autoscroll(dt)
  225. # selection move
  226. if self.drag_moving:
  227. # update positions
  228. wx, wy = self.world_mouse
  229. dx = wx - self.wdrag_start_point[0]
  230. dy = wy - self.wdrag_start_point[1]
  231. if self.restricted_mov:
  232. if abs(dy) > abs(dx):
  233. dx = 0
  234. else:
  235. dy = 0
  236. dpos = euclid.Vector2(dx, dy)
  237. for actor in self.selection:
  238. old_pos = self.selection[actor].center
  239. new_pos = old_pos + dpos
  240. # TODO: clamp new_pos so actor into world boundaries ?
  241. actor.update_position(new_pos)
  242. scroller = self.weak_scroller()
  243. # zoom
  244. zoom_change = (dz != 0 or self.wheel != 0)
  245. if zoom_change:
  246. if self.mouse_into_world():
  247. wzoom_center = self.world_mouse
  248. szoom_center = self.screen_mouse
  249. else:
  250. # decay to scroller unadorned
  251. wzoom_center = None
  252. if self.wheel != 0:
  253. dt_dz = 0.01666666 * self.wheel
  254. self.wheel = 0
  255. else:
  256. dt_dz = dt * dz
  257. zoom = scroller.scale + dt_dz * self.zoom_fastness
  258. if zoom < self.zoom_min:
  259. zoom = self.zoom_min
  260. elif zoom > self.zoom_max:
  261. zoom = self.zoom_max
  262. scroller.scale = zoom
  263. if wzoom_center is not None:
  264. # postprocess toward 'world point under mouse the same before
  265. # and after zoom' ; other restrictions may prevent fully comply
  266. wx1, wy1 = self.layer_manager.scrolling_manager.screen_to_world(*szoom_center)
  267. fx = scroller.restricted_fx + (wzoom_center[0] - wx1)
  268. fy = scroller.restricted_fy + (wzoom_center[1] - wy1)
  269. scroller.set_focus(fx, fy)
  270. def update_mouse_position(self, sx, sy):
  271. self.screen_mouse = sx, sy
  272. self.world_mouse = self.layer_manager.scrolling_manager.screen_to_world(sx, sy)
  273. # handle autoscroll
  274. border = self.autoscroll_border
  275. if border is not None:
  276. # sleft and companions includes the border
  277. scroller = self.weak_scroller()
  278. self.update_view_bounds()
  279. sdx = 0.0
  280. if sx < self.sleft:
  281. sdx = sx - self.sleft
  282. elif sx > self.sright:
  283. sdx = sx - self.sright
  284. sdy = 0.0
  285. if sy < self.sbottom:
  286. sdy = sy - self.sbottom
  287. elif sy > self.s_top:
  288. sdy = sy - self.s_top
  289. self.autoscrolling = sdx != 0.0 or sdy != 0.0
  290. if self.autoscrolling:
  291. self.autoscrolling_sdelta = (sdx / border, sdy / border)
  292. def update_autoscroll(self, dt):
  293. fraction_sdx, fraction_sdy = self.autoscrolling_sdelta
  294. scroller = self.weak_scroller()
  295. worldview = self.weak_worldview()
  296. f = self.autoscroll_fastness
  297. wdx = (fraction_sdx * f * dt) / scroller.scale / worldview.scale
  298. wdy = (fraction_sdy * f * dt) / scroller.scale / worldview.scale
  299. # ask scroller to try scroll (wdx, wdy)
  300. fx = scroller.restricted_fx + wdx
  301. fy = scroller.restricted_fy + wdy
  302. scroller.set_focus(fx, fy)
  303. self.world_mouse = self.layer_manager.scrolling_manager.screen_to_world(*self.screen_mouse)
  304. self.adjust_elastic_box()
  305. # self.update_view_bounds()
  306. def update_view_bounds(self):
  307. scroller = self.weak_scroller()
  308. scx, scy = self.layer_manager.scrolling_manager.world_to_screen(
  309. scroller.restricted_fx,
  310. scroller.restricted_fy,
  311. )
  312. hw = scroller.view_w / 2.0
  313. hh = scroller.view_h / 2.0
  314. border = self.autoscroll_border
  315. self.sleft = scx - hw + border
  316. self.sright = scx + hw - border
  317. self.sbottom = scy - hh + border
  318. self.s_top = scy + hh - border
  319. def mouse_into_world(self):
  320. worldview = self.weak_worldview()
  321. # TODO: allow lower limits != 0 ?
  322. return ((0 <= self.world_mouse[0] <= worldview.width) and
  323. (0 <= self.world_mouse[1] <= worldview.height))
  324. def on_key_press(self, k, m):
  325. binds = self.bindings
  326. if k in binds:
  327. self.buttons[binds[k]] = 1
  328. self.modifiers[binds[k]] = 1
  329. return True
  330. return False
  331. def on_key_release(self, k, m):
  332. binds = self.bindings
  333. if k in binds:
  334. self.buttons[binds[k]] = 0
  335. self.modifiers[binds[k]] = 0
  336. return True
  337. return False
  338. def on_mouse_motion(self, sx, sy, dx, dy):
  339. self.update_mouse_position(sx, sy)
  340. def on_mouse_leave(self, sx, sy):
  341. self.autoscrolling = False
  342. def on_mouse_press(self, x, y, buttons, modifiers):
  343. pass
  344. def on_mouse_release(self, sx, sy, button, modifiers):
  345. # should we handle here mod_restricted_mov ?
  346. wx, wy = self.layer_manager.scrolling_manager.screen_to_world(sx, sy)
  347. modify_selection = modifiers & self.mod_modify_selection
  348. if self.dragging:
  349. # ignore all buttons except left button
  350. if button != mouse.LEFT:
  351. return
  352. if self.drag_selecting:
  353. self.end_drag_selection(wx, wy, modify_selection)
  354. elif self.drag_moving:
  355. self.end_drag_move(wx, wy)
  356. self.dragging = False
  357. else:
  358. if button == mouse.LEFT:
  359. self.end_click_selection(wx, wy, modify_selection)
  360. def end_click_selection(self, wx, wy, modify_selection):
  361. under_mouse_unique = self.single_actor_from_mouse()
  362. if modify_selection:
  363. # toggle selected status for unique
  364. if under_mouse_unique in self.selection:
  365. self.selection_remove(under_mouse_unique)
  366. elif under_mouse_unique is not None:
  367. self.selection_add(under_mouse_unique)
  368. else:
  369. # new_selected becomes the current selected
  370. self.selection.clear()
  371. if under_mouse_unique is not None:
  372. self.selection_add(under_mouse_unique)
  373. def selection_add(self, actor):
  374. self.selection[actor] = actor.cshape.copy()
  375. def selection_remove(self, actor):
  376. del self.selection[actor]
  377. def end_drag_selection(self, wx, wy, modify_selection):
  378. new_selection = self.collision_manager.objs_into_box(*self.elastic_box_wminmax)
  379. if not modify_selection:
  380. # new_selected becomes the current selected
  381. self.selection.clear()
  382. for actor in new_selection:
  383. self.selection_add(actor)
  384. self.elastic_box.visible = False
  385. self.drag_selecting = False
  386. def on_mouse_drag(self, sx, sy, dx, dy, buttons, modifiers):
  387. # TODO: inhibir esta llamada si estamos fuera de la client area / viewport
  388. self.update_mouse_position(sx, sy)
  389. if not buttons & mouse.LEFT:
  390. # ignore except for left-btn-drag
  391. return
  392. if not self.dragging:
  393. print("begin drag")
  394. self.begin_drag()
  395. return
  396. if self.drag_selecting:
  397. # update elastic box
  398. self.adjust_elastic_box()
  399. elif self.drag_moving:
  400. self.restricted_mov = (modifiers & self.mod_restricted_mov)
  401. def adjust_elastic_box(self):
  402. # when elastic_box visible this method needs to be called any time
  403. # world_mouse changes or screen_to_world results changes (scroll, etc)
  404. wx0, wy0 = self.wdrag_start_point
  405. wx1, wy1 = self.world_mouse
  406. wminx = min(wx0, wx1)
  407. wmaxx = max(wx0, wx1)
  408. wminy = min(wy0, wy1)
  409. wmaxy = max(wy0, wy1)
  410. self.elastic_box_wminmax = wminx, wmaxx, wminy, wmaxy
  411. self.elastic_box.adjust_from_w_minmax(*self.elastic_box_wminmax)
  412. def begin_drag(self):
  413. self.dragging = True
  414. self.wdrag_start_point = self.world_mouse
  415. under_mouse_unique = self.single_actor_from_mouse()
  416. if under_mouse_unique is None:
  417. # begin drag selection
  418. self.drag_selecting = True
  419. self.adjust_elastic_box()
  420. self.elastic_box.visible = True
  421. print("begin drag selection: drag_selecting, drag_moving",
  422. self.drag_selecting, self.drag_moving)
  423. else:
  424. # want drag move
  425. if under_mouse_unique in self.selection:
  426. # want to move current selection
  427. pass
  428. else:
  429. # change selection before moving
  430. self.selection.clear()
  431. self.selection_add(under_mouse_unique)
  432. self.begin_drag_move()
  433. def begin_drag_move(self):
  434. # begin drag move
  435. self.drag_moving = True
  436. # how-to update collman: remove/add vs clear/add all
  437. # when total number of actors is low anyone will be fine,
  438. # with high numbers, most probably we move only a small fraction
  439. # For simplicity I choose remove/add, albeit a hybrid aproach
  440. # can be implemented later
  441. self.set_selection_in_collman(False)
  442. # print "begin drag: drag_selecting, drag_moving", self.drag_selecting, self.drag_moving
  443. def end_drag_move(self, wx, wy):
  444. self.set_selection_in_collman(True)
  445. for actor in self.selection:
  446. self.selection[actor] = actor.cshape.copy()
  447. self.drag_moving = False
  448. def single_actor_from_mouse(self):
  449. under_mouse = self.collision_manager.objs_touching_point(*self.world_mouse)
  450. if len(under_mouse) == 0:
  451. return None
  452. # return the one with the center most near to mouse, if tie then
  453. # an arbitrary in the tie
  454. nearest = None
  455. near_d = None
  456. p = euclid.Vector2(*self.world_mouse)
  457. for actor in under_mouse:
  458. d = (actor.cshape.center - p).magnitude_squared()
  459. if nearest is None or (d < near_d):
  460. nearest = actor
  461. near_d = d
  462. return nearest
  463. def set_selection_in_collman(self, bool_value):
  464. if self.selection_in_collman == bool_value:
  465. return
  466. self.selection_in_collman = bool_value
  467. if bool_value:
  468. for actor in self.selection:
  469. self.collision_manager.add(actor)
  470. else:
  471. for actor in self.selection:
  472. self.collision_manager.remove_tricky(actor)
  473. def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
  474. # TODO: check if mouse over scroller viewport?
  475. self.wheel += scroll_y * self.wheel_multiplier
  476. class MainLayer(ScrollableLayer):
  477. is_event_handler = True
  478. def __init__(
  479. self,
  480. layer_manager: LayerManager,
  481. width: int,
  482. height: int,
  483. scroll_step: int=100,
  484. ) -> None:
  485. super().__init__()
  486. self.layer_manager = layer_manager
  487. self.scroll_step = scroll_step
  488. self.grid_manager = GridManager(self, 32, border=2)
  489. self.width = width
  490. self.height = height
  491. self.px_width = width
  492. self.px_height = height
  493. class Gui(object):
  494. def __init__(
  495. self,
  496. config: Config,
  497. logger: SynergineLogger,
  498. terminal: Terminal,
  499. read_queue_interval: float= 1/60.0,
  500. ):
  501. self.config = config
  502. self.logger = logger
  503. self._read_queue_interval = read_queue_interval
  504. self.terminal = terminal
  505. self.cycle_duration = self.config.core.cycle_duration
  506. cocos.director.director.init(
  507. width=640,
  508. height=480,
  509. vsync=True,
  510. resizable=True,
  511. )
  512. # Enable blending
  513. pyglet.gl.glEnable(pyglet.gl.GL_BLEND)
  514. pyglet.gl.glBlendFunc(pyglet.gl.GL_SRC_ALPHA, pyglet.gl.GL_ONE_MINUS_SRC_ALPHA)
  515. # Enable transparency
  516. pyglet.gl.glEnable(pyglet.gl.GL_ALPHA_TEST)
  517. pyglet.gl.glAlphaFunc(pyglet.gl.GL_GREATER, .1)
  518. def run(self):
  519. self.before_run()
  520. pyglet.clock.schedule_interval(
  521. lambda *_, **__: self.terminal.read(),
  522. self._read_queue_interval,
  523. )
  524. cocos.director.director.run(self.get_main_scene())
  525. def before_run(self) -> None:
  526. pass
  527. def get_main_scene(self) -> cocos.cocosnode.CocosNode:
  528. raise NotImplementedError()
  529. def before_received(self, package: TerminalPackage):
  530. pass
  531. def after_received(self, package: TerminalPackage):
  532. pass
  533. class TMXGui(Gui):
  534. def __init__(
  535. self,
  536. config: Config,
  537. logger: SynergineLogger,
  538. terminal: Terminal,
  539. read_queue_interval: float = 1 / 60.0,
  540. map_dir_path: str=None,
  541. ):
  542. assert map_dir_path
  543. super(TMXGui, self).__init__(
  544. config,
  545. logger,
  546. terminal,
  547. read_queue_interval,
  548. )
  549. self.map_dir_path = map_dir_path
  550. self.layer_manager = LayerManager(
  551. self.config,
  552. self.logger,
  553. middleware=TMXMiddleware(
  554. self.config,
  555. self.logger,
  556. self.map_dir_path,
  557. ),
  558. )
  559. self.layer_manager.init()
  560. self.layer_manager.center()
  561. def get_main_scene(self) -> cocos.cocosnode.CocosNode:
  562. return self.layer_manager.main_scene