565 行
14 KiB
Python
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 ()
|