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:
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user