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

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