伊地知ニジカ放送局だぬ゛ん゛. 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.

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