base.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. # coding: utf-8
  2. import io
  3. import os
  4. import random
  5. import typing
  6. import cocos
  7. import pyglet
  8. import time
  9. from PIL import Image
  10. from pyglet.window import key
  11. from cocos.actions import MoveTo as BaseMoveTo
  12. from cocos.actions import RotateTo
  13. from synergine2_cocos2d.audio import AudioLibrary as BaseAudioLibrary
  14. from synergine2_cocos2d.interaction import InteractionManager
  15. from synergine2_cocos2d.middleware import MapMiddleware
  16. from synergine2_cocos2d.util import PathManager
  17. from opencombat.game.animation import ANIMATION_WALK
  18. from opencombat.game.animation import ANIMATION_CRAWL
  19. from opencombat.game.fire import GuiFiringEvent
  20. from opencombat.game.placement import SetSubjectPositionsInteraction
  21. from opencombat.game.state import SaveStateInteraction
  22. from opencombat.simulation.interior import InteriorManager
  23. from opencombat.simulation.tmx import TileMap
  24. from opencombat.user_action import UserAction
  25. from synergine2.config import Config
  26. from synergine2.terminals import Terminal
  27. from synergine2_cocos2d.actions import MoveTo
  28. from synergine2_cocos2d.animation import Animate
  29. from synergine2_cocos2d.gl import draw_line
  30. from synergine2_cocos2d.gui import EditLayer as BaseEditLayer
  31. from synergine2_cocos2d.gui import SubjectMapper
  32. from synergine2_cocos2d.gui import Gui
  33. from synergine2_cocos2d.gui import TMXGui
  34. from synergine2_cocos2d.layer import LayerManager
  35. from opencombat.simulation import move
  36. from synergine2_xyz.physics import Physics
  37. from synergine2_xyz.utils import get_angle
  38. from opencombat.simulation.event import NewVisibleOpponent
  39. from opencombat.simulation.event import NoLongerVisibleOpponent
  40. from opencombat.simulation.event import FireEvent
  41. from opencombat.simulation.event import DieEvent
  42. from opencombat.simulation.subject import ManSubject
  43. from opencombat.simulation.subject import TankSubject
  44. from opencombat.game.actor import Man as ManActor
  45. from opencombat.game.actor import HeavyVehicle as HeavyVehicleActor
  46. class EditLayer(BaseEditLayer):
  47. def _on_key_press(self, k, m):
  48. if self.selection:
  49. if k == key.M:
  50. self.user_action_pending = UserAction.ORDER_MOVE
  51. if k == key.R:
  52. self.user_action_pending = UserAction.ORDER_MOVE_FAST
  53. if k == key.C:
  54. self.user_action_pending = UserAction.ORDER_MOVE_CRAWL
  55. if k == key.F:
  56. self.user_action_pending = UserAction.ORDER_FIRE
  57. if k == key.S:
  58. self.run_save_state()
  59. def draw(self) -> None:
  60. super().draw()
  61. def run_save_state(self) -> None:
  62. interaction = self.layer_manager\
  63. .interaction_manager\
  64. .get_for_user_action(
  65. UserAction.SAVE_STATE,
  66. )
  67. interaction.execute()
  68. def can_move(self, selected) -> bool:
  69. return self.config.resolve('_runtime.placement_mode', False)
  70. def end_drag_move(self, wx, wy):
  71. # set position
  72. super().end_drag_move(wx, wy)
  73. interaction = self.layer_manager \
  74. .interaction_manager \
  75. .get_for_user_action(
  76. UserAction.SET_SUBJECTS_POSITION,
  77. )
  78. interaction.execute()
  79. class BackgroundLayer(cocos.layer.Layer):
  80. def __init__(
  81. self,
  82. config: Config,
  83. layer_manager: LayerManager,
  84. background_sprite: cocos.sprite.Sprite,
  85. ) -> None:
  86. super().__init__()
  87. self.config = config
  88. self.layer_manager = layer_manager
  89. self.background_sprite = background_sprite
  90. self.last_interior_draw_timestamp = 0
  91. self.draw_interiors_gap = self.config.resolve(
  92. 'game.building.draw_interior_gap',
  93. 2,
  94. )
  95. self.background_image = Image.open(os.path.join(
  96. self.layer_manager.middleware.map_dir_path,
  97. 'background.png',
  98. ))
  99. self.interior_manager = InteriorManager(
  100. TileMap(layer_manager.middleware.get_map_file_path()),
  101. original_image=self.background_image,
  102. )
  103. self.map_tile_width = self.layer_manager.middleware.get_cell_width()
  104. self.map_tile_height = self.layer_manager.middleware.get_cell_height()
  105. def draw(self, *args, **kwargs):
  106. super().draw(*args, **kwargs)
  107. self.draw_interiors()
  108. def draw_interiors(self):
  109. now = time.time()
  110. if now - self.last_interior_draw_timestamp > self.draw_interiors_gap:
  111. self.last_interior_draw_timestamp = now
  112. subject_grid_positions = [
  113. a.subject.position for a
  114. in self.layer_manager.subject_layer.subjects_index.values()
  115. ]
  116. interiors = self.interior_manager.get_interiors(
  117. where_positions=subject_grid_positions)
  118. # FIXME BS 2018-01-25: if not, put original background image
  119. if interiors:
  120. image_fake_file = io.BytesIO()
  121. new_background_image = self.interior_manager.update_image_for_interiors(
  122. interiors,
  123. self.map_tile_width,
  124. self.map_tile_height,
  125. )
  126. new_background_image.save(image_fake_file, format='PNG')
  127. self.background_sprite.image = pyglet.image.load(
  128. 'new_background.png',
  129. file=image_fake_file,
  130. )
  131. class TileLayerManager(LayerManager):
  132. edit_layer_class = EditLayer
  133. def __init__(
  134. self,
  135. config: Config,
  136. middleware: MapMiddleware,
  137. interaction_manager: 'InteractionManager',
  138. gui: 'Gui',
  139. ) -> None:
  140. super().__init__(
  141. config,
  142. middleware,
  143. interaction_manager,
  144. gui,
  145. )
  146. self.background_layer = None # type: BackgroundLayer
  147. self.interior_sprite = None # type: cocos.sprite.Sprite
  148. self.ground_layer = None # type: cocos.tiles.RectMapLayer
  149. self.top_layer = None # type: cocos.tiles.RectMapLayer
  150. def init(self) -> None:
  151. super().init()
  152. self.interior_sprite = self.middleware.get_interior_sprite()
  153. background_sprite = self.middleware.get_background_sprite()
  154. self.background_layer = BackgroundLayer(self.config, self, background_sprite)
  155. self.background_layer.add(background_sprite)
  156. self.ground_layer = self.middleware.get_ground_layer()
  157. self.top_layer = self.middleware.get_top_layer()
  158. def connect_layers(self) -> None:
  159. self.main_layer.add(self.interior_sprite)
  160. self.main_layer.add(self.background_layer)
  161. self.main_layer.add(self.ground_layer)
  162. super().connect_layers()
  163. self.main_layer.add(self.top_layer)
  164. def center(self) -> None:
  165. super().center()
  166. self.interior_sprite.position = \
  167. 0 + (self.interior_sprite.width / 2), 0 + (self.interior_sprite.height / 2)
  168. self.background_layer.background_sprite.position = \
  169. 0 + (self.background_layer.background_sprite.width / 2), 0 +\
  170. (self.background_layer.background_sprite.height/2)
  171. self.ground_layer.set_view(
  172. 0, 0, self.ground_layer.px_width, self.ground_layer.px_height,
  173. )
  174. # TODO BS 2018-06-14: We have to move this layer to be correct.
  175. # but some trees disapears ... wtf ?
  176. self.top_layer.set_view(
  177. 28, 28, self.top_layer.px_width, self.top_layer.px_height,
  178. )
  179. class AudioLibrary(BaseAudioLibrary):
  180. sound_file_paths = {
  181. 'gunshot_default': '204010__duckduckpony__homemade-gunshot-2.ogg',
  182. }
  183. class Game(TMXGui):
  184. layer_manager_class = TileLayerManager
  185. def __init__(
  186. self,
  187. config: Config,
  188. terminal: Terminal,
  189. physics: Physics,
  190. read_queue_interval: float = 1 / 60.0,
  191. map_dir_path: str=None,
  192. ):
  193. super().__init__(
  194. config,
  195. terminal,
  196. physics=physics,
  197. read_queue_interval=read_queue_interval,
  198. map_dir_path=map_dir_path,
  199. )
  200. self.sound_lib = AudioLibrary(config.resolve('global.include_path.sounds'))
  201. self.graphic_path_manager = PathManager(self.config.resolve(
  202. 'global.include_path.graphics',
  203. ))
  204. self.debug_gui = self.config.resolve('global.debug_gui', False)
  205. self.terminal.register_event_handler(
  206. move.SubjectFinishTileMoveEvent,
  207. self.set_subject_position,
  208. )
  209. self.terminal.register_event_handler(
  210. move.SubjectFinishMoveEvent,
  211. self.set_subject_position,
  212. )
  213. self.terminal.register_event_handler(
  214. move.SubjectStartTileMoveEvent,
  215. self.start_move_subject,
  216. )
  217. self.terminal.register_event_handler(
  218. move.SubjectStartRotationEvent,
  219. self.start_rotate_subject,
  220. )
  221. self.terminal.register_event_handler(
  222. move.SubjectFinishRotationEvent,
  223. self.rotate_subject,
  224. )
  225. self.terminal.register_event_handler(
  226. NewVisibleOpponent,
  227. self.new_visible_opponent,
  228. )
  229. self.terminal.register_event_handler(
  230. NoLongerVisibleOpponent,
  231. self.no_longer_visible_opponent,
  232. )
  233. self.terminal.register_event_handler(
  234. FireEvent,
  235. self.fire_happen,
  236. )
  237. self.terminal.register_event_handler(
  238. DieEvent,
  239. self.subject_die,
  240. )
  241. self.dead_soldier_image = pyglet.resource.image(self.graphic_path_manager.path(
  242. 'actors/man_d1.png',
  243. ))
  244. # subject/actor mapping
  245. self.subject_mapper_factory.register_mapper(
  246. ManSubject,
  247. SubjectMapper(self.config, ManActor),
  248. )
  249. self.subject_mapper_factory.register_mapper(
  250. TankSubject,
  251. SubjectMapper(self.config, HeavyVehicleActor),
  252. )
  253. def before_run(self) -> None:
  254. from opencombat.game.move import MoveActorInteraction
  255. from opencombat.game.move import MoveFastActorInteraction
  256. from opencombat.game.move import MoveCrawlActorInteraction
  257. from opencombat.game.fire import FireActorInteraction
  258. self.layer_manager.interaction_manager.register(
  259. MoveActorInteraction,
  260. self.layer_manager,
  261. )
  262. self.layer_manager.interaction_manager.register(
  263. MoveFastActorInteraction,
  264. self.layer_manager,
  265. )
  266. self.layer_manager.interaction_manager.register(
  267. MoveCrawlActorInteraction,
  268. self.layer_manager,
  269. )
  270. self.layer_manager.interaction_manager.register(
  271. FireActorInteraction,
  272. self.layer_manager,
  273. )
  274. self.layer_manager.interaction_manager.register(
  275. SaveStateInteraction,
  276. self.layer_manager,
  277. )
  278. self.layer_manager.interaction_manager.register(
  279. SetSubjectPositionsInteraction,
  280. self.layer_manager,
  281. )
  282. def set_subject_position(
  283. self,
  284. event: typing.Union[move.SubjectFinishMoveEvent, move.SubjectFinishTileMoveEvent]
  285. ):
  286. actor = self.layer_manager.subject_layer.subjects_index[event.subject_id]
  287. new_world_position = self.layer_manager\
  288. .grid_manager\
  289. .get_world_position_of_grid_position(event.move_to)
  290. actor.stop_actions((BaseMoveTo,))
  291. actor.set_position(*new_world_position)
  292. def start_move_subject(
  293. self,
  294. event: typing.Union[move.SubjectFinishTileMoveEvent, move.SubjectContinueTileMoveEvent, move.SubjectFinishMoveEvent], # nopep8
  295. ):
  296. actor = self.layer_manager.subject_layer.subjects_index[event.subject_id]
  297. new_world_position = self.layer_manager\
  298. .grid_manager\
  299. .get_world_position_of_grid_position(event.move_to)
  300. # FIXME BS 20180319: compute/config for cycle duration? ?
  301. if event.gui_action == UserAction.ORDER_MOVE:
  302. animation = ANIMATION_WALK
  303. cycle_duration = 2
  304. elif event.gui_action == UserAction.ORDER_MOVE_FAST:
  305. animation = ANIMATION_WALK
  306. cycle_duration = 0.5
  307. elif event.gui_action == UserAction.ORDER_MOVE_CRAWL:
  308. animation = ANIMATION_CRAWL
  309. cycle_duration = 2
  310. else:
  311. raise NotImplementedError(
  312. 'Gui action {} unknown'.format(event.gui_action)
  313. )
  314. move_duration = event.duration
  315. move_action = MoveTo(new_world_position, move_duration)
  316. actor.do(move_action)
  317. actor.do(Animate(
  318. animation,
  319. duration=move_duration,
  320. cycle_duration=cycle_duration,
  321. ))
  322. if actor.can_rotate_instant():
  323. actor.rotation = get_angle(actor.subject.position, event.move_to)
  324. actor.mode = actor.get_mode_for_gui_action(animation)
  325. def start_rotate_subject(self, event: move.SubjectStartRotationEvent):
  326. actor = self.layer_manager.subject_layer.subjects_index[event.subject_id]
  327. rotate_action = RotateTo(event.rotate_absolute, event.duration)
  328. actor.stop_actions((RotateTo,))
  329. actor.do(rotate_action)
  330. def rotate_subject(self, event: move.SubjectFinishRotationEvent):
  331. actor = self.layer_manager.subject_layer.subjects_index[event.subject_id]
  332. actor.rotation = event.rotation_absolute
  333. def new_visible_opponent(self, event: NewVisibleOpponent):
  334. self.visible_or_no_longer_visible_opponent(event, (153, 0, 153))
  335. def no_longer_visible_opponent(self, event: NoLongerVisibleOpponent):
  336. self.visible_or_no_longer_visible_opponent(event, (255, 102, 0))
  337. def visible_or_no_longer_visible_opponent(
  338. self,
  339. event: typing.Union[NoLongerVisibleOpponent, NewVisibleOpponent],
  340. line_color,
  341. ) -> None:
  342. if not self.layer_manager.debug:
  343. return
  344. observer_actor = self.layer_manager.subject_layer.subjects_index[event.observer_subject_id]
  345. observed_actor = self.layer_manager.subject_layer.subjects_index[event.observed_subject_id]
  346. observer_pixel_position = self.layer_manager.scrolling_manager.world_to_screen(
  347. *self.layer_manager.grid_manager.get_world_position_of_grid_position(
  348. observer_actor.subject.position,
  349. )
  350. )
  351. observed_pixel_position = self.layer_manager.scrolling_manager.world_to_screen(
  352. *self.layer_manager.grid_manager.get_world_position_of_grid_position(
  353. observed_actor.subject.position,
  354. )
  355. )
  356. def draw_visible_opponent():
  357. draw_line(
  358. observer_pixel_position,
  359. observed_pixel_position,
  360. line_color,
  361. )
  362. self.layer_manager.edit_layer.append_callback(draw_visible_opponent, 1.0)
  363. def fire_happen(self, event: FireEvent) -> None:
  364. shooter_actor = self.layer_manager.subject_layer.subjects_index[event.shooter_subject_id]
  365. shooter_pixel_position = self.layer_manager.scrolling_manager.world_to_screen(
  366. *self.layer_manager.grid_manager.get_world_position_of_grid_position(
  367. shooter_actor.subject.position,
  368. )
  369. )
  370. fire_to_pixel_position = self.layer_manager.scrolling_manager.world_to_screen(
  371. *self.layer_manager.grid_manager.get_world_position_of_grid_position(
  372. event.target_position,
  373. )
  374. )
  375. def gunshot_trace():
  376. draw_line(
  377. shooter_pixel_position,
  378. fire_to_pixel_position,
  379. color=(255, 0, 0),
  380. )
  381. def gunshot_sound():
  382. self.sound_lib.get_sound('gunshot_default').play()
  383. firing_event = GuiFiringEvent(shooter_actor, event.weapon_type)
  384. def actor_rotate():
  385. shooter_actor.rotation = get_angle(
  386. shooter_actor.subject.position,
  387. event.target_position,
  388. )
  389. def actor_firing():
  390. shooter_actor.firing(firing_event)
  391. def actor_end_firing():
  392. shooter_actor.reset_default_texture()
  393. # To avoid all in same time
  394. # TODO BS 2018-01-24: This should be unecessary when core events sending will be
  395. # base on time base instead cycle base. Remove it to ensure.
  396. delay = random.uniform(0.0, 0.6)
  397. if self.debug_gui:
  398. self.layer_manager.edit_layer.append_callback(
  399. gunshot_trace,
  400. duration=0.1,
  401. delay=delay,
  402. )
  403. self.layer_manager.edit_layer.append_callback(
  404. gunshot_sound,
  405. duration=0.0,
  406. delay=delay,
  407. )
  408. self.layer_manager.edit_layer.append_callback(
  409. actor_firing,
  410. duration=0.5, # TODO BS 2018-01-25: Wil depend of weapon type
  411. delay=delay,
  412. end_callback=actor_end_firing,
  413. start_callback=actor_rotate,
  414. )
  415. def subject_die(self, event: DieEvent) -> None:
  416. killed_actor = self.layer_manager.subject_layer.subjects_index[event.shoot_subject_id]
  417. killed_actor.update_image(self.dead_soldier_image)
  418. killed_actor.freeze()