feat: add gameplay enhancements and visual improvements

Backend:
- Add weather system with 6 weather types and transition probabilities
- Add day/night cycle (dawn, day, dusk, night) with phase modifiers
- Add mood system for agents (happy, neutral, sad, anxious)
- Add new commands: heal, talk, encourage, revive
- Add agent social interaction system with relationships
- Add casual mode with auto-revive and reduced decay rates

Frontend (Web):
- Add world state display (weather, time of day)
- Add mood bar to agent cards
- Add new action buttons for heal, encourage, talk, revive
- Handle new event types from server

Unity Client:
- Add EnvironmentManager with dynamic sky gradient and island scene
- Add WeatherEffects with rain, sun rays, fog, and heat particles
- Add SceneBootstrap for automatic visual system initialization
- Improve AgentVisual with better character sprites and animations
- Add breathing and bobbing idle animations
- Add character shadows
- Improve UI panels with rounded corners and borders
- Improve SpeechBubble with rounded corners and proper tail
- Add support for all new server events and commands

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
empty
2026-01-01 15:25:15 +08:00
parent 8264fe2be3
commit 6c66764cce
18 changed files with 3418 additions and 313 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -220,7 +220,8 @@ class LLMService:
async def generate_idle_chat(
self,
agent: "Agent",
weather: str = "Sunny"
weather: str = "Sunny",
time_of_day: str = "day"
) -> str:
"""
Generate idle chatter for an agent based on current conditions.
@@ -228,6 +229,7 @@ class LLMService:
Args:
agent: The Agent model instance
weather: Current weather condition
time_of_day: Current time of day (dawn/day/dusk/night)
Returns:
A spontaneous thought or comment from the agent
@@ -249,7 +251,7 @@ class LLMService:
f"Personality: {agent.personality}. "
f"Current Status: HP={agent.hp}, Energy={agent.energy}. "
f"You are stranded on a survival island. "
f"The weather is {weather}. "
f"It is currently {time_of_day} and the weather is {weather}. "
f"Say something brief (under 15 words) about your situation or thoughts. "
f"Speak naturally, as if talking to yourself or nearby survivors."
)
@@ -280,6 +282,185 @@ class LLMService:
logger.error(f"LLM API error for idle chat: {e}")
return self._get_mock_response(event_type)
async def generate_conversation_response(
self,
agent_name: str,
agent_personality: str,
agent_mood: int,
username: str,
topic: str = "just chatting"
) -> str:
"""
Generate a conversation response when a user talks to an agent.
Args:
agent_name: Name of the agent
agent_personality: Agent's personality trait
agent_mood: Agent's current mood (0-100)
username: Name of the user talking to the agent
topic: Topic of conversation
Returns:
Agent's response to the user
"""
if self._mock_mode:
mood_state = "happy" if agent_mood >= 70 else "neutral" if agent_mood >= 40 else "sad"
responses = {
"happy": [
f"Hey {username}! Great to see a friendly face!",
f"Oh, you want to chat? I'm in a good mood today!",
f"Nice of you to talk to me, {username}!",
],
"neutral": [
f"Oh, hi {username}. What's on your mind?",
f"Sure, I can chat for a bit.",
f"What do you want to talk about?",
],
"sad": [
f"*sighs* Oh... hey {username}...",
f"I'm not really in the mood, but... okay.",
f"What is it, {username}?",
]
}
return random.choice(responses.get(mood_state, responses["neutral"]))
try:
mood_desc = "happy and energetic" if agent_mood >= 70 else \
"calm and neutral" if agent_mood >= 40 else \
"a bit down" if agent_mood >= 20 else "anxious and worried"
system_prompt = (
f"You are {agent_name}, a survivor on a deserted island. "
f"Personality: {agent_personality}. "
f"Current mood: {mood_desc} (mood level: {agent_mood}/100). "
f"A viewer named {username} wants to chat with you. "
f"Respond naturally in character (under 30 words). "
f"Be conversational and show your personality."
)
user_msg = f"{username} says: {topic}" if topic != "just chatting" else \
f"{username} wants to chat with you."
kwargs = {
"model": self._model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_msg}
],
"max_tokens": 80,
"temperature": 0.85,
}
if self._api_base:
kwargs["api_base"] = self._api_base
if self._api_key and not self._api_key_header:
kwargs["api_key"] = self._api_key
if self._extra_headers:
kwargs["extra_headers"] = self._extra_headers
response = await self._acompletion(**kwargs)
return response.choices[0].message.content.strip()
except Exception as e:
logger.error(f"LLM API error for conversation: {e}")
return f"*nods at {username}* Hey there."
async def generate_social_interaction(
self,
initiator_name: str,
target_name: str,
interaction_type: str,
relationship_type: str,
weather: str = "Sunny",
time_of_day: str = "day"
) -> str:
"""
Generate dialogue for social interaction between two agents.
Args:
initiator_name: Name of the agent initiating interaction
target_name: Name of the target agent
interaction_type: Type of interaction (chat, share_food, help, argue, comfort)
relationship_type: Current relationship (stranger, acquaintance, friend, etc.)
weather: Current weather
time_of_day: Current time of day
Returns:
A brief dialogue exchange between the two agents
"""
if self._mock_mode:
dialogues = {
"chat": [
f"{initiator_name}: Hey {target_name}, how are you holding up?\n{target_name}: Could be better, but I'm managing.",
f"{initiator_name}: Nice weather today, huh?\n{target_name}: Yeah, at least something's going right.",
],
"share_food": [
f"{initiator_name}: Here, take some of my food.\n{target_name}: Really? Thanks, I appreciate it!",
f"{initiator_name}: You look hungry. Have some of this.\n{target_name}: You're a lifesaver!",
],
"help": [
f"{initiator_name}: Need a hand with that?\n{target_name}: Yes, thank you so much!",
f"{initiator_name}: Let me help you out.\n{target_name}: I owe you one!",
],
"argue": [
f"{initiator_name}: This is all your fault!\n{target_name}: My fault? You're the one who-",
f"{initiator_name}: I can't believe you did that!\n{target_name}: Just leave me alone!",
],
"comfort": [
f"{initiator_name}: Hey, are you okay?\n{target_name}: *sniff* I'll be fine... thanks for asking.",
f"{initiator_name}: Don't worry, we'll get through this.\n{target_name}: I hope you're right...",
]
}
return random.choice(dialogues.get(interaction_type, dialogues["chat"]))
try:
relationship_desc = {
"stranger": "barely know each other",
"acquaintance": "are getting to know each other",
"friend": "are friends",
"close_friend": "are close friends who trust each other",
"rival": "have tensions between them"
}.get(relationship_type, "are acquaintances")
interaction_desc = {
"chat": "having a casual conversation",
"share_food": "sharing food with",
"help": "helping with a task",
"argue": "having a disagreement with",
"comfort": "comforting"
}.get(interaction_type, "talking to")
system_prompt = (
f"You are writing dialogue for two survivors on a deserted island. "
f"{initiator_name} and {target_name} {relationship_desc}. "
f"It is {time_of_day} and the weather is {weather}. "
f"{initiator_name} is {interaction_desc} {target_name}. "
f"Write a brief, natural dialogue exchange (2-3 lines total). "
f"Format: '{initiator_name}: [line]\\n{target_name}: [response]'"
)
kwargs = {
"model": self._model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Write a {interaction_type} dialogue between {initiator_name} and {target_name}."}
],
"max_tokens": 100,
"temperature": 0.9,
}
if self._api_base:
kwargs["api_base"] = self._api_base
if self._api_key and not self._api_key_header:
kwargs["api_key"] = self._api_key
if self._extra_headers:
kwargs["extra_headers"] = self._extra_headers
response = await self._acompletion(**kwargs)
return response.choices[0].message.content.strip()
except Exception as e:
logger.error(f"LLM API error for social interaction: {e}")
return f"{initiator_name}: ...\n{target_name}: ..."
# Global instance for easy import
llm_service = LLMService()

View File

@@ -1,11 +1,11 @@
"""
SQLAlchemy ORM models for The Island.
Defines User (viewers), Agent (NPCs), and WorldState entities.
Defines User (viewers), Agent (NPCs), WorldState, GameConfig, and AgentRelationship.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, func
from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean, ForeignKey, UniqueConstraint, func
from .database import Base
@@ -29,7 +29,7 @@ class User(Base):
class Agent(Base):
"""
Represents an NPC survivor on the island.
Has personality, health, energy, and inventory.
Has personality, health, energy, mood, and social attributes.
"""
__tablename__ = "agents"
@@ -41,8 +41,23 @@ class Agent(Base):
energy = Column(Integer, default=100)
inventory = Column(String(500), default="{}") # JSON string
# Mood system (Phase 3)
mood = Column(Integer, default=70) # 0-100 scale
mood_state = Column(String(20), default="neutral") # happy, neutral, sad, anxious
# Revival tracking (Phase 1)
death_tick = Column(Integer, nullable=True) # Tick when agent died
# Social attributes (Phase 5)
social_tendency = Column(String(20), default="neutral") # introvert, extrovert, neutral
def __repr__(self):
return f"<Agent {self.name} ({self.personality}) HP={self.hp} Energy={self.energy} Status={self.status}>"
return f"<Agent {self.name} ({self.personality}) HP={self.hp} Energy={self.energy} Mood={self.mood}>"
@property
def is_alive(self) -> bool:
"""Check if agent is alive."""
return self.status == "Alive"
def to_dict(self):
"""Convert to dictionary for JSON serialization."""
@@ -53,14 +68,17 @@ class Agent(Base):
"status": self.status,
"hp": self.hp,
"energy": self.energy,
"inventory": self.inventory
"inventory": self.inventory,
"mood": self.mood,
"mood_state": self.mood_state,
"social_tendency": self.social_tendency
}
class WorldState(Base):
"""
Global state of the island environment.
Tracks day count, weather, and shared resources.
Tracks day count, time of day, weather, and resources.
"""
__tablename__ = "world_state"
@@ -69,13 +87,122 @@ class WorldState(Base):
weather = Column(String(20), default="Sunny")
resource_level = Column(Integer, default=100)
# Day/Night cycle (Phase 2)
current_tick_in_day = Column(Integer, default=0) # 0 to TICKS_PER_DAY
time_of_day = Column(String(10), default="day") # dawn, day, dusk, night
# Weather system (Phase 3)
weather_duration = Column(Integer, default=0) # Ticks since last weather change
def __repr__(self):
return f"<WorldState Day={self.day_count} Weather={self.weather} Resources={self.resource_level}>"
return f"<WorldState Day={self.day_count} {self.time_of_day} Weather={self.weather}>"
def to_dict(self):
"""Convert to dictionary for JSON serialization."""
return {
"day_count": self.day_count,
"weather": self.weather,
"resource_level": self.resource_level
"resource_level": self.resource_level,
"current_tick_in_day": self.current_tick_in_day,
"time_of_day": self.time_of_day
}
class GameConfig(Base):
"""
Game configuration for difficulty settings.
Supports casual and normal modes.
"""
__tablename__ = "game_config"
id = Column(Integer, primary_key=True, index=True)
difficulty = Column(String(20), default="casual") # normal, casual
# Decay multipliers (1.0 = normal, 0.5 = casual)
energy_decay_multiplier = Column(Float, default=0.5)
hp_decay_multiplier = Column(Float, default=0.5)
# Revival settings
auto_revive_enabled = Column(Boolean, default=True)
auto_revive_delay_ticks = Column(Integer, default=12) # 60 seconds at 5s/tick
revive_hp = Column(Integer, default=50)
revive_energy = Column(Integer, default=50)
# Social settings
social_interaction_probability = Column(Float, default=0.3)
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
def __repr__(self):
return f"<GameConfig difficulty={self.difficulty}>"
def to_dict(self):
"""Convert to dictionary for JSON serialization."""
return {
"difficulty": self.difficulty,
"energy_decay_multiplier": self.energy_decay_multiplier,
"hp_decay_multiplier": self.hp_decay_multiplier,
"auto_revive_enabled": self.auto_revive_enabled,
"auto_revive_delay_ticks": self.auto_revive_delay_ticks,
"social_interaction_probability": self.social_interaction_probability
}
class AgentRelationship(Base):
"""
Tracks relationships between agents.
Affection, trust determine relationship type.
"""
__tablename__ = "agent_relationships"
id = Column(Integer, primary_key=True, index=True)
agent_from_id = Column(Integer, ForeignKey("agents.id"), nullable=False)
agent_to_id = Column(Integer, ForeignKey("agents.id"), nullable=False)
# Relationship metrics (-100 to 100)
affection = Column(Integer, default=0) # Liking
trust = Column(Integer, default=0) # Trust level
# Derived from metrics
relationship_type = Column(String(30), default="stranger")
# Types: stranger, acquaintance, friend, close_friend, rival
# Interaction tracking
interaction_count = Column(Integer, default=0)
last_interaction_tick = Column(Integer, default=0)
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
__table_args__ = (
UniqueConstraint('agent_from_id', 'agent_to_id', name='unique_relationship'),
)
def __repr__(self):
return f"<Relationship {self.agent_from_id}->{self.agent_to_id} {self.relationship_type}>"
def to_dict(self):
"""Convert to dictionary for JSON serialization."""
return {
"agent_from_id": self.agent_from_id,
"agent_to_id": self.agent_to_id,
"affection": self.affection,
"trust": self.trust,
"relationship_type": self.relationship_type,
"interaction_count": self.interaction_count
}
def update_relationship_type(self):
"""Calculate and update relationship type based on metrics."""
total = self.affection + self.trust
if total <= -50:
self.relationship_type = "rival"
elif total <= 20:
self.relationship_type = "stranger"
elif total <= 50:
self.relationship_type = "acquaintance"
elif total <= 100:
self.relationship_type = "friend"
else:
self.relationship_type = "close_friend"

View File

@@ -15,6 +15,7 @@ class EventType(str, Enum):
TICK = "tick"
SYSTEM = "system"
ERROR = "error"
# Island survival events
AGENTS_UPDATE = "agents_update" # All agents status broadcast
AGENT_DIED = "agent_died" # An agent has died
@@ -24,6 +25,26 @@ class EventType(str, Enum):
WORLD_UPDATE = "world_update" # World state update
CHECK = "check" # Status check response
# Day/Night cycle (Phase 2)
TIME_UPDATE = "time_update" # Time tick update
PHASE_CHANGE = "phase_change" # Dawn/day/dusk/night transition
DAY_CHANGE = "day_change" # New day started
# Weather system (Phase 3)
WEATHER_CHANGE = "weather_change" # Weather changed
MOOD_UPDATE = "mood_update" # Agent mood changed
# New commands (Phase 4)
HEAL = "heal" # User healed an agent
TALK = "talk" # User talked to an agent
ENCOURAGE = "encourage" # User encouraged an agent
REVIVE = "revive" # User revived a dead agent
# Social system (Phase 5)
SOCIAL_INTERACTION = "social_interaction" # Agents interacted
RELATIONSHIP_CHANGE = "relationship_change" # Relationship status changed
AUTO_REVIVE = "auto_revive" # Agent auto-revived (casual mode)
class GameEvent(BaseModel):
"""