ohmyzsh/plugins/taskman/dino_animation.py
2025-06-16 16:28:47 +08:00

471 lines
No EOL
18 KiB
Python

#!/usr/bin/env python3
"""
Dino Animation Module for Taskman
Chrome dino-style game with enhanced physics and visual effects
"""
import random
import time
import math
class DinoAnimation:
"""Enhanced Chrome dino-style animation with physics-based jumping"""
def __init__(self, width: int):
self.width = width - 10 # Leave margin for UI
self.enabled = False
self.last_update = 0
# Player state
self.player_pos = 8 # Fixed horizontal position
self.is_jumping = False
self.jump_velocity = 0 # Vertical velocity
self.jump_height = 0 # Current height
self.gravity = 0.6 # Reduced gravity for more natural feel
self.ground_level = 0 # Ground level
self.initial_jump_velocity = 2.8 # Reduced initial jump velocity
# Animation state
self.frame_counter = 0
self.player_sprites = ["", "", "", ""] # Running animation
self.jump_sprite = ""
# Game elements
self.obstacles = []
self.clouds = []
self.ground_dots = []
self.obstacle_spawn_timer = 0
# Game state
self.game_over = False
self.game_over_timer = 0
self.score = 0
# State tracking for flicker reduction
self._last_display_state = None
self._display_cache = None
self._state_changed = True
# Initialize ground pattern
self._init_ground()
self._init_obstacle_types()
self._init_cloud_patterns()
def _init_ground(self):
"""Initialize scrolling ground pattern"""
ground_chars = [".", "·", "˙", ""]
for i in range(self.width + 20):
self.ground_dots.append({
'x': i * 2,
'char': random.choice(ground_chars),
'speed': 1.0
})
def _init_obstacle_types(self):
"""Initialize obstacle types with ASCII art for tall obstacles"""
self.obstacle_types = [
# Low ground obstacles (height 1) - single element
{'type': 'ground_low', 'height': 1, 'chars': ['|', '', '', '', '', '']},
# Medium ground obstacles (height 1) - single emoji elements
{'type': 'ground_medium', 'height': 1, 'chars': ['🌵', '🪨', '🗿']},
# Tall ground obstacles (height 2) - ASCII art stacked
{'type': 'ground_tall', 'height': 2, 'chars': [
{'base': '/|\\', 'top': '/^\\'}, # Mountain peak
{'base': '|||', 'top': '═══'}, # Building with roof
{'base': '▓▓▓', 'top': '^^^'}, # Rock formation
{'base': '│█│', 'top': '┌─┐'}, # Tower
{'base': '/█\\', 'top': ''}, # Tree with crown
{'base': '▀▀▀', 'top': '┬┬┬'}, # Fence/barrier
]},
# Flying obstacles (height 2-3) - appear in air
{'type': 'flying', 'height': 0, 'chars': ['🦅', '✈️', '🚁', '🛸', '', '', '']}
]
def _init_cloud_patterns(self):
"""Initialize cloud patterns for background"""
self.cloud_patterns = ['', '☁️', ''] # Removed sun and moving elements
def _start_jump(self):
"""Start a jump with realistic physics"""
if not self.is_jumping:
self.is_jumping = True
self.jump_velocity = self.initial_jump_velocity
self.jump_height = 0
def _update_jump_physics(self):
"""Update jump physics with realistic gravity"""
if self.is_jumping:
# Update position based on velocity
self.jump_height += self.jump_velocity
# Apply gravity (decelerate upward velocity)
self.jump_velocity -= self.gravity
# Land when hitting ground
if self.jump_height <= self.ground_level:
self.jump_height = self.ground_level
self.is_jumping = False
self.jump_velocity = 0
def _spawn_obstacle(self):
"""Spawn a new obstacle at the right edge"""
if len(self.obstacles) < 3: # Reduced obstacle limit for less density
# Reduce flying obstacles frequency
ground_types = [t for t in self.obstacle_types if t['type'] != 'flying']
flying_types = [t for t in self.obstacle_types if t['type'] == 'flying']
# 80% chance for ground obstacles, 20% for flying
if random.random() < 0.8:
obstacle_type = random.choice(ground_types)
else:
obstacle_type = random.choice(flying_types)
# Handle different obstacle char structures
if obstacle_type['type'] == 'ground_tall':
# For tall obstacles, pick a stacked pair
obstacle_char = random.choice(obstacle_type['chars'])
else:
# For simple obstacles, pick a single char
obstacle_char = random.choice(obstacle_type['chars'])
# Spawn at right edge with more spacing
spawn_x = self.width + random.randint(15, 40) # Increased spacing
# Determine height based on obstacle type
if obstacle_type['type'] == 'flying':
height = random.choice([2, 3]) # Flying obstacles at height 2-3
else:
height = obstacle_type['height']
self.obstacles.append({
'x': spawn_x,
'char': obstacle_char,
'height': height,
'type': obstacle_type['type'],
'speed': random.uniform(1.0, 2.0) # Slower speed range
})
def _spawn_cloud(self):
"""Spawn background clouds for atmosphere"""
if len(self.clouds) < 2: # Reduced cloud limit
cloud_char = random.choice(self.cloud_patterns)
spawn_x = self.width + random.randint(20, 60) # More spacing between clouds
self.clouds.append({
'x': spawn_x,
'char': cloud_char,
'speed': random.uniform(0.1, 0.3) # Much slower cloud movement for background effect
})
def _check_collision(self):
"""Check for collisions between player and obstacles"""
if self.is_jumping:
player_height = int(self.jump_height) + 1
else:
player_height = 0
for obstacle in self.obstacles:
# Only check collision with ground-based obstacles
if obstacle['type'] in ['ground_low', 'ground_medium', 'ground_tall']:
# Calculate obstacle width for ASCII art
obstacle_width = 1
if obstacle['type'] == 'ground_tall' and isinstance(obstacle['char'], dict):
if 'base' in obstacle['char']:
obstacle_width = len(obstacle['char']['base'])
# Check horizontal collision with obstacle width
for i in range(obstacle_width):
if abs((obstacle['x'] + i) - self.player_pos) <= 1:
# Check vertical collision
obstacle_top = obstacle['height']
if player_height < obstacle_top:
self.game_over = True
self.game_over_timer = 45 # 3.6 seconds at 80ms intervals
return True
# Flying obstacles don't cause collisions - player passes underneath
return False
def _reset_game(self):
"""Reset game state after game over"""
self.game_over = False
self.obstacles.clear()
self.clouds.clear()
self.score = 0
self.is_jumping = False
self.jump_velocity = 0
self.jump_height = 0
self.obstacle_spawn_timer = 0
def toggle_animation(self):
"""Toggle animation on/off"""
self.enabled = not self.enabled
if not self.enabled:
self._reset_game()
return self.enabled
def is_enabled(self):
"""Check if animation is enabled"""
return self.enabled
def _get_display_state_hash(self):
"""Generate a hash of the current display state to detect changes"""
state_data = []
# Player state
state_data.append(f"player:{self.player_pos}:{self.is_jumping}:{int(self.jump_height)}:{self.game_over}")
# Obstacles
for obs in self.obstacles:
state_data.append(f"obs:{int(obs['x'])}:{obs['char']}:{obs['height']}")
# Clouds (only position matters for display)
for cloud in self.clouds:
state_data.append(f"cloud:{int(cloud['x'])}:{cloud['char']}")
# Frame counter for animation
sprite_index = (self.frame_counter // 3) % len(self.player_sprites)
state_data.append(f"frame:{sprite_index}")
return hash(tuple(state_data))
def update(self):
"""Update animation state with improved physics and state tracking"""
if not self.enabled:
return
current_time = time.time()
# Slower animation: update every 80ms for more relaxed pace
if current_time - self.last_update < 0.08:
return
self.last_update = current_time
old_state_hash = self._get_display_state_hash()
# Handle game over state
if self.game_over:
if self.game_over_timer > 0:
self.game_over_timer -= 1
else:
self._reset_game()
# Check if state changed
new_state_hash = self._get_display_state_hash()
self._state_changed = (old_state_hash != new_state_hash)
return
# Update frame counter for animations
self.frame_counter += 1
# Update jump physics
self._update_jump_physics()
# Smart auto-jump logic - only jump for ground obstacles
if not self.is_jumping:
for obstacle in self.obstacles:
# Only jump for obstacles that are on the ground or blocking the path
if obstacle['type'] in ['ground_low', 'ground_medium', 'ground_tall']:
distance = obstacle['x'] - self.player_pos
if 4 <= distance <= 7: # Reasonable jump timing window
self._start_jump()
break
# Flying obstacles don't require jumping - player passes underneath
# Move obstacles
obstacles_changed = False
for obstacle in self.obstacles[:]:
old_x = obstacle['x']
obstacle['x'] -= obstacle['speed']
if obstacle['x'] < -5:
self.obstacles.remove(obstacle)
self.score += 1
obstacles_changed = True
elif int(old_x) != int(obstacle['x']): # Only flag change if integer position changed
obstacles_changed = True
# Move clouds
clouds_changed = False
for cloud in self.clouds[:]:
old_x = cloud['x']
cloud['x'] -= cloud['speed']
if cloud['x'] < -10:
self.clouds.remove(cloud)
clouds_changed = True
elif int(old_x) != int(cloud['x']): # Only flag change if integer position changed
clouds_changed = True
# Move ground dots (these don't affect display state hash, so no tracking needed)
for dot in self.ground_dots:
dot['x'] -= dot['speed']
if dot['x'] < -5:
dot['x'] = self.width + 5
# Spawn new obstacles and clouds with better timing
self.obstacle_spawn_timer += 1
if self.obstacle_spawn_timer >= random.randint(15, 25): # Much less frequent spawning
self._spawn_obstacle()
self.obstacle_spawn_timer = 0
obstacles_changed = True
# Spawn clouds less frequently
if random.random() < 0.05: # 5% chance each update (reduced from 10%)
self._spawn_cloud()
clouds_changed = True
# Check collisions
collision_occurred = self._check_collision()
# Determine if display state changed
new_state_hash = self._get_display_state_hash()
self._state_changed = (
old_state_hash != new_state_hash or
obstacles_changed or
clouds_changed or
collision_occurred
)
def has_display_changed(self):
"""Check if the display state has changed since last check"""
return self._state_changed
def get_display_lines(self) -> tuple:
"""Generate all 6 display lines for the animation with caching"""
if not self.enabled:
return ("", "", "", "", "", "")
# Return cached display if state hasn't changed
if not self._state_changed and self._display_cache is not None:
return self._display_cache
# Generate new display
display_lines = self._generate_display_lines()
# Cache the result
self._display_cache = display_lines
self._state_changed = False
return display_lines
def _generate_display_lines(self) -> tuple:
"""Generate all 6 display lines for the animation"""
if not self.enabled:
return ("", "", "", "", "", "")
# Initialize all lines
sky_line = [' '] * self.width
high_line = [' '] * self.width
mid_line = [' '] * self.width
jump_line = [' '] * self.width
ground_line = [' '] * self.width
base_line = [' '] * self.width
# Draw clouds fixed in sky layer only
for cloud in self.clouds:
x = int(cloud['x'])
if 0 <= x < self.width and x < len(sky_line):
sky_line[x] = cloud['char']
# Draw obstacles at appropriate heights
for obstacle in self.obstacles:
x = int(obstacle['x'])
if 0 <= x < self.width:
height = obstacle['height']
char = obstacle['char']
if obstacle['type'] == 'flying':
# Flying obstacles appear in high layers only
if height == 3 and x < len(sky_line):
sky_line[x] = char
elif height == 2 and x < len(high_line):
high_line[x] = char
elif obstacle['type'] == 'ground_tall':
# Tall obstacles use ASCII art stacked display
if isinstance(char, dict) and 'base' in char and 'top' in char:
base_art = char['base']
top_art = char['top']
# Draw multi-character ASCII art
for i, c in enumerate(base_art):
if x + i < len(ground_line):
ground_line[x + i] = c
for i, c in enumerate(top_art):
if x + i < len(jump_line):
jump_line[x + i] = c
else:
# Fallback for simple char
if x < len(ground_line):
ground_line[x] = char
else:
# Simple ground obstacles (height 1)
if x < len(ground_line):
ground_line[x] = char
# Draw player
if self.player_pos < self.width:
if self.game_over:
# Game over state
if self.player_pos < len(ground_line):
ground_line[self.player_pos] = ''
elif self.is_jumping:
# Calculate which line to draw player on based on jump height
# Use more reasonable height mapping
if self.jump_height >= 3:
# Very high jump - sky line
if self.player_pos < len(high_line):
high_line[self.player_pos] = self.jump_sprite
# Add trail at previous height
if self.player_pos > 0 and mid_line[self.player_pos - 1] == ' ':
mid_line[self.player_pos - 1] = '·'
elif self.jump_height >= 2:
# Medium jump - mid line
if self.player_pos < len(mid_line):
mid_line[self.player_pos] = self.jump_sprite
# Add trail at previous height
if self.player_pos > 0 and jump_line[self.player_pos - 1] == ' ':
jump_line[self.player_pos - 1] = '·'
elif self.jump_height >= 1:
# Low jump - jump line
if self.player_pos < len(jump_line):
jump_line[self.player_pos] = self.jump_sprite
# Add trail at ground level
if self.player_pos > 0 and ground_line[self.player_pos - 1] == ' ':
ground_line[self.player_pos - 1] = '·'
else:
# Just off ground - still on ground line
if self.player_pos < len(ground_line):
ground_line[self.player_pos] = self.jump_sprite
else:
# Running animation
sprite_index = (self.frame_counter // 3) % len(self.player_sprites)
if self.player_pos < len(ground_line):
ground_line[self.player_pos] = self.player_sprites[sprite_index]
# Draw ground pattern
for dot in self.ground_dots:
x = int(dot['x'])
if 0 <= x < self.width and x < len(base_line):
if base_line[x] == ' ': # Don't overwrite other elements
base_line[x] = dot['char']
# Convert to strings
return (
''.join(sky_line),
''.join(high_line),
''.join(mid_line),
''.join(jump_line),
''.join(ground_line),
''.join(base_line)
)
def get_status(self) -> str:
"""Get current game status"""
if not self.enabled:
return "Animation: OFF (press 'x' to enable)"
if self.game_over:
return f"Game Over! Score: {self.score} (restarting...)"
player_state = 'Jumping' if self.is_jumping else 'Running'
return f"Dino Game: {player_state} | Score: {self.score} | Press 'x' to toggle"