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

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