伊地知ニジカ放送局だぬ゛ん゛. https://www.youtube.com/@deerjika
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

595 lines
16 KiB

  1. from __future__ import annotations
  2. import math
  3. import sys
  4. from datetime import datetime, timedelta
  5. from enum import Enum, auto
  6. from typing import Callable, TypedDict
  7. import cv2
  8. import ephem # type: ignore
  9. import pygame
  10. import pygame.gfxdraw
  11. from cv2 import VideoCapture
  12. from ephem import Moon, Observer, Sun # type: ignore
  13. from pygame import Rect, Surface
  14. from pygame.font import Font
  15. from pygame.mixer import Sound
  16. from pygame.time import Clock
  17. from common_module import CommonModule
  18. pygame.init ()
  19. FPS = 30
  20. SYSTEM_FONT = pygame.font.SysFont ('notosanscjkjp', 24, bold = True)
  21. USER_FONT = pygame.font.SysFont ('notosanscjkjp', 32, italic = True)
  22. DEERJIKA_FONT = pygame.font.SysFont ('07nikumarufont', 50)
  23. def main (
  24. ) -> None:
  25. game = Game ()
  26. Bg (game)
  27. Deerjika (game, DeerjikaPattern.RELAXED, x = CWindow.WIDTH * 3 / 4, y = CWindow.HEIGHT - 120)
  28. balloon = Balloon (game)
  29. CurrentTime (game, SYSTEM_FONT)
  30. try:
  31. Sound ('assets/bgm.mp3').play (loops = -1)
  32. except Exception:
  33. pass
  34. while True:
  35. for event in pygame.event.get ():
  36. if event.type == pygame.QUIT:
  37. pygame.quit ()
  38. sys.exit ()
  39. game.redraw ()
  40. class Bg:
  41. """
  42. 背景オブゼクト管理用クラス
  43. Attributes:
  44. base (BgBase): 最背面
  45. grass (BgGrass): 草原部分
  46. jojoko (Jojoko): 大月ヨヨコ
  47. kita (KitaSun): き太く陽
  48. """
  49. base: BgBase
  50. grass: BgGrass
  51. jojoko: Jojoko
  52. kita: KitaSun
  53. def __init__ (
  54. self,
  55. game: Game,
  56. ):
  57. self.base = BgBase (game)
  58. self.jojoko = Jojoko (game)
  59. self.kita = KitaSun (game)
  60. self.grass = BgGrass (game)
  61. class DeerjikaPattern (Enum):
  62. """
  63. ニジカの状態
  64. Members:
  65. NORMAL: 通常
  66. RELAXED: 足パタパタ
  67. SLEEPING: 寝ニジカ
  68. DANCING: ダンシング・ニジカ
  69. """
  70. NORMAL = auto ()
  71. RELAXED = auto ()
  72. SLEEPING = auto ()
  73. DANCING = auto ()
  74. class Direction (Enum):
  75. """
  76. クリーチャの向き
  77. Members:
  78. LEFT: 左向き
  79. RIGHT: 右向き
  80. """
  81. LEFT = auto ()
  82. RIGHT = auto ()
  83. class Game:
  84. """
  85. ゲーム・クラス
  86. Attributes:
  87. clock (Clock): Clock オブゼクト
  88. frame (int): フレーム・カウンタ
  89. redrawers (list[Redrawer]): 再描画するクラスのリスト
  90. screen (Surface): 基底スクリーン
  91. sky (Sky): 天体情報
  92. """
  93. clock: Clock
  94. frame: int
  95. redrawers: list[Redrawer]
  96. screen: Surface
  97. sky: Sky
  98. def __init__ (
  99. self,
  100. ):
  101. self.screen = pygame.display.set_mode ((CWindow.WIDTH, CWindow.HEIGHT))
  102. self.clock = Clock ()
  103. self.frame = 0
  104. self.redrawers = []
  105. self._create_sky ()
  106. def redraw (
  107. self,
  108. ) -> None:
  109. for redrawer in sorted (self.redrawers, key = lambda x: x['layer']):
  110. if redrawer['obj'].enabled:
  111. redrawer['obj'].redraw ()
  112. pygame.display.update ()
  113. self.clock.tick (FPS)
  114. def _create_sky (
  115. self,
  116. ) -> None:
  117. self.sky = Sky ()
  118. self.sky.observer = Observer ()
  119. self.sky.observer.lat = '35'
  120. self.sky.observer.lon = '139'
  121. class GameObject:
  122. """
  123. 各ゲーム・オブゼクトの基底クラス
  124. Attributes:
  125. arg (float): 回転角度 (rad)
  126. ax (float): X 軸に対する加速度 (px/frame^2)
  127. ay (float): y 軸に対する加速度 (px/frame^2)
  128. enabled (bool): オブゼクトの表示可否
  129. frame (int): フレーム・カウンタ
  130. game (Game): ゲーム基盤
  131. height (int): 高さ (px)
  132. vx (float): x 軸に対する速度 (px/frame)
  133. vy (float): y 軸に対する速度 (px/frame)
  134. width (int): 幅 (px)
  135. x (float): X 座標 (px)
  136. y (float): Y 座標 (px)
  137. """
  138. arg: float = 0
  139. ax: float = 0
  140. ay: float = 0
  141. enabled: bool = True
  142. frame: int
  143. game: Game
  144. height: int
  145. vx: float = 0
  146. vy: float = 0
  147. width: int
  148. x: float
  149. y: float
  150. def __init__ (
  151. self,
  152. game: Game,
  153. layer: int | None = None,
  154. enabled: bool = True,
  155. x: float = 0,
  156. y: float = 0,
  157. ):
  158. self.game = game
  159. self.enabled = enabled
  160. self.frame = 0
  161. if layer is None:
  162. if self.game.redrawers:
  163. layer = max (r['layer'] for r in self.game.redrawers) + 10
  164. else:
  165. layer = 0
  166. self.game.redrawers.append ({ 'layer': layer, 'obj': self })
  167. self.x = x
  168. self.y = y
  169. def redraw (
  170. self,
  171. ) -> None:
  172. self.x += self.vx
  173. self.y += self.vy
  174. self.vx += self.ax
  175. self.vy += self.ay
  176. self.frame += 1
  177. class BgBase (GameObject):
  178. """
  179. 背景
  180. Attributes:
  181. surface (Surface): 背景 Surface
  182. """
  183. surface: Surface
  184. def __init__ (
  185. self,
  186. game: Game,
  187. ):
  188. super ().__init__ (game)
  189. self.surface = pygame.image.load ('assets/bg.jpg')
  190. self.surface = pygame.transform.scale (self.surface, (CWindow.WIDTH, CWindow.HEIGHT))
  191. def redraw (
  192. self,
  193. ) -> None:
  194. self.game.screen.blit (self.surface, (self.x, self.y))
  195. super ().redraw ()
  196. class BgGrass (GameObject):
  197. """
  198. 背景の草原部分
  199. Attributes:
  200. surface (Surface): 草原 Surface
  201. """
  202. surface: Surface
  203. def __init__ (
  204. self,
  205. game: Game,
  206. ):
  207. super ().__init__ (game)
  208. self.game = game
  209. self.surface = pygame.image.load ('assets/bg-grass.png')
  210. self.surface = pygame.transform.scale (self.surface, (CWindow.WIDTH, CWindow.HEIGHT))
  211. def redraw (
  212. self,
  213. ) -> None:
  214. self.game.screen.blit (self.surface, (self.x, self.y))
  215. super ().redraw ()
  216. class Deerjika (GameObject):
  217. """
  218. 伊地知ニジカ
  219. Attributes:
  220. height (int): 高さ (px)
  221. scale (float): 拡大率
  222. surfaces (list[Surface]): ニジカの各フレームを Surface にしたリスト
  223. width (int): 幅 (px)
  224. """
  225. height: int
  226. scale: float = .8
  227. surfaces: list[Surface]
  228. width: int
  229. def __init__ (
  230. self,
  231. game: Game,
  232. pattern: DeerjikaPattern = DeerjikaPattern.NORMAL,
  233. direction: Direction = Direction.LEFT,
  234. layer: int | None = None,
  235. x: float = 0,
  236. y: float = 0,
  237. ):
  238. super ().__init__ (game, layer, x = x, y = y)
  239. self.pattern = pattern
  240. self.direction = direction
  241. match pattern:
  242. case DeerjikaPattern.NORMAL:
  243. ...
  244. case DeerjikaPattern.RELAXED:
  245. match direction:
  246. case Direction.LEFT:
  247. self.width = 1280
  248. self.height = 720
  249. surface = pygame.image.load ('assets/deerjika_relax_left.png')
  250. self.surfaces = []
  251. for x in range (0, surface.get_width (), self.width):
  252. self.surfaces.append (
  253. surface.subsurface (x, 0, self.width, self.height))
  254. case Direction.RIGHT:
  255. ...
  256. def redraw (
  257. self,
  258. ) -> None:
  259. surface = pygame.transform.scale (self.surfaces[self.frame % len (self.surfaces)],
  260. (self.width * self.scale, self.height * self.scale))
  261. self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
  262. super ().redraw ()
  263. class CurrentTime (GameObject):
  264. """
  265. 現在日時表示
  266. Attributes:
  267. font (Font): フォント
  268. """
  269. font: Font
  270. def __init__ (
  271. self,
  272. game: Game,
  273. font: Font,
  274. ):
  275. super ().__init__ (game)
  276. self.font = font
  277. def redraw (
  278. self,
  279. ) -> None:
  280. for i in range (4):
  281. self.game.screen.blit (
  282. self.font.render (str (datetime.now ()), True, (0, 0, 0)),
  283. (i % 2, i // 2 * 2))
  284. super ().redraw ()
  285. class Balloon (GameObject):
  286. """
  287. 吹出し
  288. Attributes:
  289. answer (str): 回答テキスト
  290. image_url (str, None): 画像 URL
  291. length (int): 表示する時間 (frame)
  292. query (str): 質問テキスト
  293. surface (Surface): 吹出し Surface
  294. x_flip (bool): 左右反転フラグ
  295. y_flip (bool): 上下反転フラグ
  296. """
  297. answer: str = ''
  298. image_url: str | None = None
  299. length: int = 300
  300. query: str = ''
  301. surface: Surface
  302. x_flip: bool = False
  303. y_flip: bool = False
  304. def __init__ (
  305. self,
  306. game: Game,
  307. x_flip: bool = False,
  308. y_flip: bool = False,
  309. ):
  310. super ().__init__ (game, enabled = False)
  311. self.x_flip = x_flip
  312. self.y_flip = y_flip
  313. self.surface = pygame.transform.scale (pygame.image.load ('assets/balloon.png'),
  314. (CWindow.WIDTH, CWindow.HEIGHT / 2))
  315. self.surface = pygame.transform.flip (self.surface, self.x_flip, self.y_flip)
  316. def redraw (
  317. self,
  318. ) -> None:
  319. if self.frame >= self.length:
  320. self.enabled = False
  321. return
  322. query = self.query
  323. if CommonModule.len_by_full (query) > 21:
  324. query = CommonModule.mid_by_full (query, 0, 19.5) + '...'
  325. answer = Surface ((800, ((CommonModule.len_by_full (self.answer) - 1) // 16 + 1) * 50),
  326. pygame.SRCALPHA)
  327. for i in range (int (CommonModule.len_by_full (self.answer) - 1) // 16 + 1):
  328. answer.blit (DEERJIKA_FONT.render (
  329. CommonModule.mid_by_full (self.answer, 16 * i, 16), True, (192, 0, 0)),
  330. (0, 50 * i))
  331. surface = self.surface.copy ()
  332. surface.blit (USER_FONT.render ('>' + query, True, (0, 0, 0)), (120, 70))
  333. y: int
  334. if self.frame < 30:
  335. y = 0
  336. elif self.frame >= self.length - 90:
  337. y = answer.get_height () - 100
  338. else:
  339. y = int ((answer.get_height () - 100) * (self.frame - 30) / (self.length - 120))
  340. surface.blit (answer, (100, 150), Rect (0, y, 800, 100))
  341. self.game.screen.blit (surface, (0, 0))
  342. super ().redraw ()
  343. def talk (
  344. self,
  345. query: str,
  346. answer: str,
  347. image_url: str | None = None,
  348. length: int = 300,
  349. ) -> None:
  350. self.query = query
  351. self.answer = answer
  352. self.image_url = image_url
  353. self.length = length
  354. self.frame = 0
  355. self.enabled = True
  356. class KitaSun (GameObject):
  357. """
  358. き太く陽
  359. Attributes:
  360. sun (Sun): ephem の太陽オブゼクト
  361. surface (Surface): き太く陽 Surface
  362. """
  363. alt: float
  364. az: float
  365. sun: Sun
  366. surface: Surface
  367. def __init__ (
  368. self,
  369. game: Game,
  370. ):
  371. super ().__init__ (game)
  372. self.surface = pygame.transform.scale (pygame.image.load ('assets/sun.png'), (200, 200))
  373. self.sun = Sun ()
  374. def redraw (
  375. self,
  376. ) -> None:
  377. surface = pygame.transform.rotate (self.surface, -(90 + math.degrees (self.arg)))
  378. self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
  379. super ().redraw ()
  380. self.game.sky.observer.date = datetime.now () - timedelta (hours = 9)
  381. self.sun.compute (self.game.sky.observer)
  382. self.alt = self.sun.alt
  383. self.az = self.sun.az
  384. if abs (self.new_arg - self.arg) > math.radians (15):
  385. self.arg = self.new_arg
  386. self.x = self.new_x
  387. self.y = self.new_y
  388. @property
  389. def new_x (
  390. self,
  391. ) -> float:
  392. return CWindow.WIDTH * (math.degrees (self.az) - 80) / 120
  393. @property
  394. def new_y (
  395. self,
  396. ) -> float:
  397. return ((CWindow.HEIGHT / 2)
  398. - ((CWindow.HEIGHT / 2 + 100) * math.sin (self.alt)
  399. / math.sin (math.radians (60))))
  400. @property
  401. def new_arg (
  402. self,
  403. ) -> float:
  404. return math.atan2 (self.new_y - self.y, self.new_x - self.x)
  405. class Jojoko (GameObject):
  406. """
  407. 大月ヨヨコ
  408. Attributes:
  409. base (Surface): 満月ヨヨコ Surface
  410. moon (Moon): ephem の月オブゼクト
  411. surface (Surface): 缺けたヨヨコ Surface
  412. """
  413. base: Surface
  414. moon: Moon
  415. def __init__ (
  416. self,
  417. game: Game,
  418. ):
  419. super ().__init__ (game)
  420. self.base = pygame.transform.scale (pygame.image.load ('assets/moon.png'), (200, 200))
  421. def redraw (
  422. self,
  423. ) -> None:
  424. ...
  425. @property
  426. def phase (
  427. self,
  428. ) -> float:
  429. dt: datetime = ephem.localtime (ephem.previous_new_moon (self.game.sky.observer.date))
  430. return (datetime.now () - dt).total_seconds () / 60 / 60 / 24
  431. @property
  432. def surface (
  433. self,
  434. ) -> Surface:
  435. jojoko = self.base.copy ()
  436. jojoko.set_colorkey ((0, 255, 0))
  437. for i in range (200):
  438. if 1 <= self.phase < 15:
  439. pygame.gfxdraw.bezier (jojoko, ((0, 100 + i), (100, 180 * self.phase / 7 - 80 + i), (200, 100 + i)), 3, (0, 255, 0))
  440. elif self.phase < 16:
  441. pass
  442. elif self.phase < 30:
  443. pygame.gfxdraw.bezier (jojoko, ((0, 100 - i), (100, 180 * (self.phase - 15) / 7 - 80 - i), (200, 100 - i)), 3, (0, 255, 0))
  444. else:
  445. jojoko.fill ((0, 255, 0))
  446. return jojoko
  447. class Sky:
  448. """
  449. 天体に関する情報を保持するクラス
  450. Attributes:
  451. observer (Observer): 観測値
  452. """
  453. observer: Observer
  454. class CWindow:
  455. """
  456. ウィンドゥに関する定数クラス
  457. Attributes:
  458. WIDTH (int): ウィンドゥ幅
  459. HEIGHT (int): ウィンドゥ高さ
  460. """
  461. WIDTH = 1024
  462. HEIGHT = 768
  463. class Redrawer (TypedDict):
  464. """
  465. 再描画処理を行ふゲーム・オブゼクトとその優先順位のペア
  466. Attributes:
  467. layer (int): レイア
  468. obj (GameObject): ゲーム・オブゼクト
  469. """
  470. layer: int
  471. obj: GameObject
  472. def get_surfaces_from_video (
  473. video_path: str,
  474. ) -> list[Surface]:
  475. cap = VideoCapture (video_path)
  476. if not cap.isOpened ():
  477. return []
  478. fps = cap.get (cv2.CAP_PROP_FPS)
  479. surfaces: list[Surface] = []
  480. while cap.isOpened ():
  481. (ret, frame) = cap.read ()
  482. if not ret:
  483. break
  484. frame = cv2.cvtColor (frame, cv2.COLOR_BGR2RGB)
  485. frame_surface = pygame.surfarray.make_surface (frame)
  486. frame_surface = pygame.transform.rotate (frame_surface, -90)
  487. surfaces.append (pygame.transform.flip (frame_surface, True, False))
  488. cap.release ()
  489. return surfaces
  490. if __name__ == '__main__':
  491. main ()