ファイル
nizika_broadcast/test.py
T
2024-12-17 07:19:33 +09:00

565 行
14 KiB
Python

from __future__ import annotations
import math
import sys
from datetime import datetime, timedelta
from enum import Enum, auto
from typing import Callable, TypedDict
import cv2
import pygame
from cv2 import VideoCapture
from ephem import Moon, Observer, Sun # type: ignore
from pygame import Rect, Surface
from pygame.font import Font
from pygame.mixer import Sound
from pygame.time import Clock
from common_module import CommonModule
pygame.init ()
FPS = 30
SYSTEM_FONT = pygame.font.SysFont ('notosanscjkjp', 24, bold = True)
USER_FONT = pygame.font.SysFont ('notosanscjkjp', 32, italic = True)
DEERJIKA_FONT = pygame.font.SysFont ('07nikumarufont', 50)
def main (
) -> None:
game = Game ()
Bg (game)
Deerjika (game, DeerjikaPattern.RELAXED, x = CWindow.WIDTH * 3 / 4, y = CWindow.HEIGHT - 120)
balloon = Balloon (game)
CurrentTime (game, SYSTEM_FONT)
try:
Sound ('assets/bgm.mp3').play (loops = -1)
except Exception:
pass
while True:
for event in pygame.event.get ():
if event.type == pygame.QUIT:
pygame.quit ()
sys.exit ()
game.redraw ()
class Bg:
"""
背景オブゼクト管理用クラス
Attributes:
base (BgBase): 最背面
grass (BgGrass): 草原部分
jojoko (Jojoko): 大月ヨヨコ
kita (KitaSun): き太く陽
"""
base: BgBase
grass: BgGrass
jojoko: Jojoko
kita: KitaSun
def __init__ (
self,
game: Game,
):
self.base = BgBase (game)
self.jojoko = Jojoko (game)
self.kita = KitaSun (game)
self.grass = BgGrass (game)
class DeerjikaPattern (Enum):
"""
ニジカの状態
Members:
NORMAL: 通常
RELAXED: 足パタパタ
SLEEPING: 寝ニジカ
DANCING: ダンシング・ニジカ
"""
NORMAL = auto ()
RELAXED = auto ()
SLEEPING = auto ()
DANCING = auto ()
class Direction (Enum):
"""
クリーチャの向き
Members:
LEFT: 左向き
RIGHT: 右向き
"""
LEFT = auto ()
RIGHT = auto ()
class Game:
"""
ゲーム・クラス
Attributes:
clock (Clock): Clock オブゼクト
frame (int): フレーム・カウンタ
redrawers (list[Redrawer]): 再描画するクラスのリスト
screen (Surface): 基底スクリーン
sky (Sky): 天体情報
"""
clock: Clock
frame: int
redrawers: list[Redrawer]
screen: Surface
sky: Sky
def __init__ (
self,
):
self.screen = pygame.display.set_mode ((CWindow.WIDTH, CWindow.HEIGHT))
self.clock = Clock ()
self.frame = 0
self.redrawers = []
self._create_sky ()
def redraw (
self,
) -> None:
for redrawer in sorted (self.redrawers, key = lambda x: x['layer']):
if redrawer['obj'].enabled:
redrawer['obj'].redraw ()
pygame.display.update ()
self.clock.tick (FPS)
def _create_sky (
self,
) -> None:
self.sky = Sky ()
self.sky.observer = Observer ()
self.sky.observer.lat = '35'
self.sky.observer.lon = '139'
class GameObject:
"""
各ゲーム・オブゼクトの基底クラス
Attributes:
arg (float): 回転角度 (rad)
ax (float): X 軸に対する加速度 (px/frame^2)
ay (float): y 軸に対する加速度 (px/frame^2)
enabled (bool): オブゼクトの表示可否
frame (int): フレーム・カウンタ
game (Game): ゲーム基盤
height (int): 高さ (px)
vx (float): x 軸に対する速度 (px/frame)
vy (float): y 軸に対する速度 (px/frame)
width (int): 幅 (px)
x (float): X 座標 (px)
y (float): Y 座標 (px)
"""
arg: float = 0
ax: float = 0
ay: float = 0
enabled: bool = True
frame: int
game: Game
height: int
vx: float = 0
vy: float = 0
width: int
x: float
y: float
def __init__ (
self,
game: Game,
layer: int | None = None,
enabled: bool = True,
x: float = 0,
y: float = 0,
):
self.game = game
self.enabled = enabled
self.frame = 0
if layer is None:
if self.game.redrawers:
layer = max (r['layer'] for r in self.game.redrawers) + 10
else:
layer = 0
self.game.redrawers.append ({ 'layer': layer, 'obj': self })
self.x = x
self.y = y
def redraw (
self,
) -> None:
self.x += self.vx
self.y += self.vy
self.vx += self.ax
self.vy += self.ay
self.frame += 1
class BgBase (GameObject):
"""
背景
Attributes:
surface (Surface): 背景 Surface
"""
surface: Surface
def __init__ (
self,
game: Game,
):
super ().__init__ (game)
self.surface = pygame.image.load ('assets/bg.jpg')
self.surface = pygame.transform.scale (self.surface, (CWindow.WIDTH, CWindow.HEIGHT))
def redraw (
self,
) -> None:
self.game.screen.blit (self.surface, (self.x, self.y))
super ().redraw ()
class BgGrass (GameObject):
"""
背景の草原部分
Attributes:
surface (Surface): 草原 Surface
"""
surface: Surface
def __init__ (
self,
game: Game,
):
super ().__init__ (game)
self.game = game
self.surface = pygame.image.load ('assets/bg-grass.png')
self.surface = pygame.transform.scale (self.surface, (CWindow.WIDTH, CWindow.HEIGHT))
def redraw (
self,
) -> None:
self.game.screen.blit (self.surface, (self.x, self.y))
super ().redraw ()
class Deerjika (GameObject):
"""
伊地知ニジカ
Attributes:
height (int): 高さ (px)
scale (float): 拡大率
surfaces (list[Surface]): ニジカの各フレームを Surface にしたリスト
width (int): 幅 (px)
"""
height: int
scale: float = .8
surfaces: list[Surface]
width: int
def __init__ (
self,
game: Game,
pattern: DeerjikaPattern = DeerjikaPattern.NORMAL,
direction: Direction = Direction.LEFT,
layer: int | None = None,
x: float = 0,
y: float = 0,
):
super ().__init__ (game, layer, x = x, y = y)
self.pattern = pattern
self.direction = direction
match pattern:
case DeerjikaPattern.NORMAL:
...
case DeerjikaPattern.RELAXED:
match direction:
case Direction.LEFT:
self.width = 1280
self.height = 720
surface = pygame.image.load ('assets/deerjika_relax_left.png')
self.surfaces = []
for x in range (0, surface.get_width (), self.width):
self.surfaces.append (
surface.subsurface (x, 0, self.width, self.height))
case Direction.RIGHT:
...
def redraw (
self,
) -> None:
surface = pygame.transform.scale (self.surfaces[self.frame % len (self.surfaces)],
(self.width * self.scale, self.height * self.scale))
self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
super ().redraw ()
class CurrentTime (GameObject):
"""
現在日時表示
Attributes:
font (Font): フォント
"""
font: Font
def __init__ (
self,
game: Game,
font: Font,
):
super ().__init__ (game)
self.font = font
def redraw (
self,
) -> None:
for i in range (4):
self.game.screen.blit (
self.font.render (str (datetime.now ()), True, (0, 0, 0)),
(i % 2, i // 2 * 2))
super ().redraw ()
class Balloon (GameObject):
"""
吹出し
Attributes:
answer (str): 回答テキスト
image_url (str, None): 画像 URL
length (int): 表示する時間 (frame)
query (str): 質問テキスト
surface (Surface): 吹出し Surface
x_flip (bool): 左右反転フラグ
y_flip (bool): 上下反転フラグ
"""
answer: str = ''
image_url: str | None = None
length: int = 300
query: str = ''
surface: Surface
x_flip: bool = False
y_flip: bool = False
def __init__ (
self,
game: Game,
x_flip: bool = False,
y_flip: bool = False,
):
super ().__init__ (game, enabled = False)
self.x_flip = x_flip
self.y_flip = y_flip
self.surface = pygame.transform.scale (pygame.image.load ('assets/balloon.png'),
(CWindow.WIDTH, CWindow.HEIGHT / 2))
self.surface = pygame.transform.flip (self.surface, self.x_flip, self.y_flip)
def redraw (
self,
) -> None:
if self.frame >= self.length:
self.enabled = False
return
query = self.query
if CommonModule.len_by_full (query) > 21:
query = CommonModule.mid_by_full (query, 0, 19.5) + '...'
answer = Surface ((800, ((CommonModule.len_by_full (self.answer) - 1) // 16 + 1) * 50),
pygame.SRCALPHA)
for i in range (int (CommonModule.len_by_full (self.answer) - 1) // 16 + 1):
answer.blit (DEERJIKA_FONT.render (
CommonModule.mid_by_full (self.answer, 16 * i, 16), True, (192, 0, 0)),
(0, 50 * i))
surface = self.surface.copy ()
surface.blit (USER_FONT.render ('>' + query, True, (0, 0, 0)), (120, 70))
y: int
if self.frame < 30:
y = 0
elif self.frame >= self.length - 90:
y = answer.get_height () - 100
else:
y = int ((answer.get_height () - 100) * (self.frame - 30) / (self.length - 120))
surface.blit (answer, (100, 150), Rect (0, y, 800, 100))
self.game.screen.blit (surface, (0, 0))
super ().redraw ()
def talk (
self,
query: str,
answer: str,
image_url: str | None = None,
length: int = 300,
) -> None:
self.query = query
self.answer = answer
self.image_url = image_url
self.length = length
self.frame = 0
self.enabled = True
class KitaSun (GameObject):
"""
き太く陽
Attributes:
sun (Sun): ephem の太陽オブゼクト
surface (Surface): き太く陽 Surface
"""
alt: float
az: float
sun: Sun
surface: Surface
def __init__ (
self,
game: Game,
):
super ().__init__ (game)
self.surface = pygame.transform.scale (pygame.image.load ('assets/sun.png'), (200, 200))
self.sun = Sun ()
def redraw (
self,
) -> None:
surface = pygame.transform.rotate (self.surface, -(90 + math.degrees (self.arg)))
self.game.screen.blit (surface, surface.get_rect (center = (self.x, self.y)))
super ().redraw ()
self.game.sky.observer.date = datetime.now () - timedelta (hours = 9)
self.sun.compute (self.game.sky.observer)
self.alt = self.sun.alt
self.az = self.sun.az
if abs (self.new_arg - self.arg) > math.radians (15):
self.arg = self.new_arg
self.x = self.new_x
self.y = self.new_y
@property
def new_x (
self,
) -> float:
return CWindow.WIDTH * (math.degrees (self.az) - 80) / 120
@property
def new_y (
self,
) -> float:
return ((CWindow.HEIGHT / 2)
- ((CWindow.HEIGHT / 2 + 100) * math.sin (self.alt)
/ math.sin (math.radians (60))))
@property
def new_arg (
self,
) -> float:
return math.atan2 (self.new_y - self.y, self.new_x - self.x)
class Jojoko (GameObject):
"""
大月ヨヨコ
Attributes:
phase (float): 月齢
"""
phase: float
def __init__ (
self,
game: Game,
):
...
def redraw (
self,
) -> None:
...
class Sky:
"""
天体に関する情報を保持するクラス
Attributes:
observer (Observer): 観測値
"""
observer: Observer
class CWindow:
"""
ウィンドゥに関する定数クラス
Attributes:
WIDTH (int): ウィンドゥ幅
HEIGHT (int): ウィンドゥ高さ
"""
WIDTH = 1024
HEIGHT = 768
class Redrawer (TypedDict):
"""
再描画処理を行ふゲーム・オブゼクトとその優先順位のペア
Attributes:
layer (int): レイア
obj (GameObject): ゲーム・オブゼクト
"""
layer: int
obj: GameObject
def get_surfaces_from_video (
video_path: str,
) -> list[Surface]:
cap = VideoCapture (video_path)
if not cap.isOpened ():
return []
fps = cap.get (cv2.CAP_PROP_FPS)
surfaces: list[Surface] = []
while cap.isOpened ():
(ret, frame) = cap.read ()
if not ret:
break
frame = cv2.cvtColor (frame, cv2.COLOR_BGR2RGB)
frame_surface = pygame.surfarray.make_surface (frame)
frame_surface = pygame.transform.rotate (frame_surface, -90)
surfaces.append (pygame.transform.flip (frame_surface, True, False))
cap.release ()
return surfaces
if __name__ == '__main__':
main ()