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):
"""

View File

@@ -12,6 +12,13 @@ let userGold = 100;
// Agents state
let agents = [];
// World state
let worldState = {
day_count: 1,
time_of_day: 'day',
weather: 'Sunny'
};
// DOM Elements
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
@@ -87,6 +94,9 @@ function handleGameEvent(event) {
updateAgentsUI(data.agents);
break;
case 'feed':
case 'heal':
case 'encourage':
case 'revive':
case 'user_update':
updateUserGold(data);
break;
@@ -97,7 +107,35 @@ function handleGameEvent(event) {
}
break;
case 'agent_speak':
showSpeechBubble(data.agent_id, data.agent_name, data.text);
case 'talk':
if (data.agent_id !== undefined) {
showSpeechBubble(data.agent_id, data.agent_name, data.text || data.response);
}
break;
case 'social_interaction':
showSocialInteraction(data);
break;
case 'world_update':
updateWorldState(data);
break;
case 'weather_change':
worldState.weather = data.new_weather;
updateWorldDisplay();
break;
case 'phase_change':
worldState.time_of_day = data.new_phase;
updateWorldDisplay();
break;
case 'day_change':
worldState.day_count = data.day;
updateWorldDisplay();
break;
case 'tick':
// Update world state from tick data
if (data.day) worldState.day_count = data.day;
if (data.time_of_day) worldState.time_of_day = data.time_of_day;
if (data.weather) worldState.weather = data.weather;
updateWorldDisplay();
break;
}
@@ -152,6 +190,10 @@ function createAgentCard(agent) {
const statusClass = isDead ? 'dead' : 'alive';
const statusText = isDead ? '已死亡' : '存活';
// Mood emoji and color
const moodEmoji = getMoodEmoji(agent.mood_state);
const moodColor = getMoodColor(agent.mood_state);
card.innerHTML = `
<div class="agent-header">
<div>
@@ -178,18 +220,175 @@ function createAgentCard(agent) {
<div class="stat-bar-fill energy" style="width: ${agent.energy}%"></div>
</div>
</div>
<button class="feed-btn" onclick="feedAgent('${agent.name}')" ${isDead ? 'disabled' : ''}>
🍖 投喂 (10金币)
</button>
<div class="stat-bar-container">
<div class="stat-bar-label">
<span>${moodEmoji} 心情</span>
<span>${agent.mood}/100</span>
</div>
<div class="stat-bar">
<div class="stat-bar-fill" style="width: ${agent.mood}%; background: ${moodColor}"></div>
</div>
</div>
<div class="agent-actions">
${isDead ? `
<button class="action-btn revive" onclick="reviveAgent('${agent.name}')">
💫 复活 (10g)
</button>
` : `
<button class="action-btn feed" onclick="feedAgent('${agent.name}')">
🍖 投喂 (10g)
</button>
<button class="action-btn heal" onclick="healAgent('${agent.name}')">
💊 治疗 (15g)
</button>
<button class="action-btn encourage" onclick="encourageAgent('${agent.name}')">
💪 鼓励 (5g)
</button>
<button class="action-btn talk" onclick="talkToAgent('${agent.name}')">
💬 交谈
</button>
`}
</div>
`;
return card;
}
/**
* Get mood emoji based on mood state
*/
function getMoodEmoji(moodState) {
const emojis = {
'happy': '😊',
'neutral': '😐',
'sad': '😢',
'anxious': '😰'
};
return emojis[moodState] || '😐';
}
/**
* Get mood color based on mood state
*/
function getMoodColor(moodState) {
const colors = {
'happy': '#4ade80',
'neutral': '#fbbf24',
'sad': '#60a5fa',
'anxious': '#f87171'
};
return colors[moodState] || '#fbbf24';
}
/**
* Update world state from server
*/
function updateWorldState(data) {
if (data.day_count) worldState.day_count = data.day_count;
if (data.time_of_day) worldState.time_of_day = data.time_of_day;
if (data.weather) worldState.weather = data.weather;
updateWorldDisplay();
}
/**
* Update the world display panel
*/
function updateWorldDisplay() {
const worldDisplay = document.getElementById('worldDisplay');
if (!worldDisplay) return;
const weatherEmojis = {
'Sunny': '☀️',
'Cloudy': '☁️',
'Rainy': '🌧️',
'Stormy': '⛈️',
'Hot': '🔥',
'Foggy': '🌫️'
};
const phaseEmojis = {
'dawn': '🌅',
'day': '☀️',
'dusk': '🌇',
'night': '🌙'
};
const phaseNames = {
'dawn': '黎明',
'day': '白天',
'dusk': '黄昏',
'night': '夜晚'
};
worldDisplay.innerHTML = `
<span>📅 第${worldState.day_count}天</span>
<span>${phaseEmojis[worldState.time_of_day] || '☀️'} ${phaseNames[worldState.time_of_day] || '白天'}</span>
<span>${weatherEmojis[worldState.weather] || '☀️'} ${worldState.weather}</span>
`;
}
/**
* Show social interaction notification
*/
function showSocialInteraction(data) {
const interactionNames = {
'chat': '聊天',
'share_food': '分享食物',
'help': '互相帮助',
'argue': '争吵',
'comfort': '安慰'
};
const message = `${data.initiator_name}${data.target_name} ${interactionNames[data.interaction_type] || '互动'}`;
logEvent({
event_type: 'social_interaction',
timestamp: Date.now() / 1000,
data: { message, dialogue: data.dialogue }
});
}
/**
* Feed an agent
*/
function feedAgent(agentName) {
sendCommand(`feed ${agentName}`);
}
/**
* Heal an agent
*/
function healAgent(agentName) {
sendCommand(`heal ${agentName}`);
}
/**
* Encourage an agent
*/
function encourageAgent(agentName) {
sendCommand(`encourage ${agentName}`);
}
/**
* Talk to an agent
*/
function talkToAgent(agentName) {
const topic = prompt(`想和 ${agentName} 聊什么?(留空则随便聊聊)`);
if (topic !== null) {
sendCommand(`talk ${agentName} ${topic}`.trim());
}
}
/**
* Revive a dead agent
*/
function reviveAgent(agentName) {
sendCommand(`revive ${agentName}`);
}
/**
* Send a command to the server
*/
function sendCommand(command) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert('未连接到服务器');
return;
@@ -198,7 +397,7 @@ function feedAgent(agentName) {
const user = getCurrentUser();
const payload = {
action: 'send_comment',
payload: { user, message: `feed ${agentName}` }
payload: { user, message: command }
};
ws.send(JSON.stringify(payload));
@@ -315,19 +514,38 @@ function formatEventData(eventType, data) {
case 'comment':
return `${data.user}: ${data.message}`;
case 'tick':
return `Tick #${data.tick} | 第${data.day}天 | 存活: ${data.alive_agents}`;
const phaseEmoji = { 'dawn': '🌅', 'day': '☀️', 'dusk': '🌇', 'night': '🌙' };
const weatherEmoji = { 'Sunny': '☀️', 'Cloudy': '☁️', 'Rainy': '🌧️', 'Stormy': '⛈️', 'Hot': '🔥', 'Foggy': '🌫️' };
return `${data.day}${phaseEmoji[data.time_of_day] || ''}${data.time_of_day} | ${weatherEmoji[data.weather] || ''}${data.weather} | 存活: ${data.alive_agents}`;
case 'system':
case 'error':
case 'feed':
case 'heal':
case 'encourage':
case 'revive':
case 'auto_revive':
case 'agent_died':
case 'check':
return data.message;
case 'agent_speak':
return `💬 ${data.agent_name}: "${data.text}"`;
case 'talk':
return `💬 ${data.agent_name}${data.user} 说: "${data.response}"`;
case 'agents_update':
return `角色状态已更新`;
case 'user_update':
return `${data.user} 金币: ${data.gold}`;
case 'weather_change':
return `🌤️ 天气变化: ${data.old_weather}${data.new_weather}`;
case 'phase_change':
return `🕐 ${data.message}`;
case 'day_change':
return `📅 ${data.message}`;
case 'social_interaction':
const dialogue = data.dialogue ? `\n"${data.dialogue}"` : '';
return `👥 ${data.message}${dialogue}`;
case 'world_update':
return `🌍 世界状态已更新`;
default:
return JSON.stringify(data);
}

View File

@@ -326,10 +326,64 @@
.event.feed { border-color: #ffaa00; background: rgba(255, 170, 0, 0.1); color: #ffcc66; }
.event.agent_died { border-color: #ff4444; background: rgba(255, 68, 68, 0.15); color: #ff8888; }
.event.agent_speak { border-color: #88ccff; background: rgba(136, 204, 255, 0.1); color: #aaddff; }
.event.talk { border-color: #88ccff; background: rgba(136, 204, 255, 0.1); color: #aaddff; }
.event.check { border-color: #88cc88; background: rgba(136, 204, 136, 0.1); }
.event.heal { border-color: #ff88cc; background: rgba(255, 136, 204, 0.1); color: #ffaadd; }
.event.encourage { border-color: #ffcc44; background: rgba(255, 204, 68, 0.1); color: #ffdd88; }
.event.revive { border-color: #cc88ff; background: rgba(204, 136, 255, 0.1); color: #ddaaff; }
.event.auto_revive { border-color: #cc88ff; background: rgba(204, 136, 255, 0.1); color: #ddaaff; }
.event.weather_change { border-color: #66cccc; background: rgba(102, 204, 204, 0.1); color: #88dddd; }
.event.phase_change { border-color: #ccaa44; background: rgba(204, 170, 68, 0.1); color: #ddcc88; }
.event.day_change { border-color: #88ccff; background: rgba(136, 204, 255, 0.15); color: #aaddff; }
.event.social_interaction { border-color: #ff8888; background: rgba(255, 136, 136, 0.1); color: #ffaaaa; }
.event-time { color: #888; font-size: 11px; }
.event-type { font-weight: bold; text-transform: uppercase; font-size: 11px; }
.event-data { margin-top: 5px; }
.event-data { margin-top: 5px; white-space: pre-wrap; }
/* World Display */
.world-panel {
background: rgba(102, 204, 204, 0.1);
border: 1px solid rgba(102, 204, 204, 0.3);
border-radius: 12px;
padding: 12px 20px;
margin-bottom: 20px;
display: flex;
justify-content: center;
gap: 30px;
font-size: 1rem;
}
.world-panel span {
display: flex;
align-items: center;
gap: 6px;
}
/* Agent Actions */
.agent-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 12px;
}
.action-btn {
padding: 6px 8px;
font-size: 0.8rem;
border-radius: 6px;
border: none;
cursor: pointer;
transition: all 0.2s;
font-weight: bold;
}
.action-btn.feed { background: #88cc88; color: #1a2a1a; }
.action-btn.feed:hover { background: #66aa66; }
.action-btn.heal { background: #ff88cc; color: #1a2a1a; }
.action-btn.heal:hover { background: #ff66bb; }
.action-btn.encourage { background: #ffcc44; color: #1a2a1a; }
.action-btn.encourage:hover { background: #ffbb22; }
.action-btn.talk { background: #88ccff; color: #1a2a1a; }
.action-btn.talk:hover { background: #66bbff; }
.action-btn.revive { background: #cc88ff; color: #1a2a1a; grid-column: span 2; }
.action-btn.revive:hover { background: #bb66ff; }
</style>
</head>
<body>
@@ -356,6 +410,13 @@
</div>
</div>
<!-- World State Panel -->
<div class="world-panel" id="worldDisplay">
<span>📅 第1天</span>
<span>☀️ 白天</span>
<span>☀️ Sunny</span>
</div>
<!-- Agents Section -->
<div class="agents-section">
<div class="speech-bubbles-overlay" id="speechBubblesOverlay"></div>
@@ -379,7 +440,7 @@
<button onclick="sendComment()">发送</button>
</div>
<p style="margin-top: 10px; font-size: 0.85rem; color: #888;">
指令: <code>feed [名字]</code> - 投喂 | <code>check</code> - 查询 | <code>reset</code> - 重新开始
指令: <code>feed/heal/encourage/revive [名字]</code> | <code>talk [名字] [话题]</code> | <code>check</code> | <code>reset</code>
</p>
</div>

View File

@@ -39,6 +39,12 @@ namespace TheIsland.Visual
[SerializeField] private Color hpLowColor = new Color(0.9f, 0.3f, 0.3f);
[SerializeField] private Color energyHighColor = new Color(1f, 0.8f, 0.2f);
[SerializeField] private Color energyLowColor = new Color(1f, 0.5f, 0.1f);
[Header("Mood Colors")]
[SerializeField] private Color moodHappyColor = new Color(0.3f, 0.9f, 0.5f);
[SerializeField] private Color moodNeutralColor = new Color(0.98f, 0.75f, 0.15f);
[SerializeField] private Color moodSadColor = new Color(0.4f, 0.65f, 0.98f);
[SerializeField] private Color moodAnxiousColor = new Color(0.97f, 0.53f, 0.53f);
#endregion
#region References
@@ -48,8 +54,11 @@ namespace TheIsland.Visual
private TextMeshProUGUI _personalityLabel;
private Image _hpBarFill;
private Image _energyBarFill;
private Image _moodBarFill;
private TextMeshProUGUI _hpText;
private TextMeshProUGUI _energyText;
private TextMeshProUGUI _moodText;
private TextMeshProUGUI _moodEmoji;
private GameObject _deathOverlay;
private SpeechBubble _speechBubble;
private Billboard _spriteBillboard;
@@ -61,6 +70,12 @@ namespace TheIsland.Visual
private int _agentId;
private AgentData _currentData;
private Coroutine _speechCoroutine;
// Animation state
private float _idleAnimTimer;
private float _breathScale = 1f;
private Vector3 _originalSpriteScale;
private float _bobOffset;
#endregion
#region Properties
@@ -76,6 +91,33 @@ namespace TheIsland.Visual
CreateVisuals();
}
private void Update()
{
if (!IsAlive) return;
// Idle breathing animation
_idleAnimTimer += Time.deltaTime;
_breathScale = 1f + Mathf.Sin(_idleAnimTimer * 2f) * 0.02f;
// Gentle bobbing
_bobOffset = Mathf.Sin(_idleAnimTimer * 1.5f) * 0.05f;
if (_spriteRenderer != null && _originalSpriteScale != Vector3.zero)
{
// Apply breathing scale
_spriteRenderer.transform.localScale = new Vector3(
_originalSpriteScale.x * _breathScale,
_originalSpriteScale.y * _breathScale,
_originalSpriteScale.z
);
// Apply bobbing
var pos = _spriteRenderer.transform.localPosition;
pos.y = 1f + _bobOffset;
_spriteRenderer.transform.localPosition = pos;
}
}
private void OnMouseDown()
{
if (!IsAlive)
@@ -162,8 +204,63 @@ namespace TheIsland.Visual
RegeneratePlaceholderSprite();
}
// Store original scale for animation
_originalSpriteScale = spriteObj.transform.localScale;
// Add billboard
_spriteBillboard = spriteObj.AddComponent<Billboard>();
// Add shadow
CreateShadow(spriteObj.transform);
}
private void CreateShadow(Transform spriteTransform)
{
var shadowObj = new GameObject("Shadow");
shadowObj.transform.SetParent(transform);
shadowObj.transform.localPosition = new Vector3(0, 0.01f, 0);
shadowObj.transform.localRotation = Quaternion.Euler(90, 0, 0);
shadowObj.transform.localScale = new Vector3(1.2f, 0.6f, 1f);
var shadowRenderer = shadowObj.AddComponent<SpriteRenderer>();
shadowRenderer.sprite = CreateShadowSprite();
shadowRenderer.sortingOrder = sortingOrder - 1;
shadowRenderer.color = new Color(0, 0, 0, 0.3f);
}
private Sprite CreateShadowSprite()
{
int size = 32;
Texture2D tex = new Texture2D(size, size);
tex.filterMode = FilterMode.Bilinear;
Vector2 center = new Vector2(size / 2f, size / 2f);
Color[] pixels = new Color[size * size];
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
float dx = (x - center.x) / (size * 0.4f);
float dy = (y - center.y) / (size * 0.4f);
float dist = dx * dx + dy * dy;
if (dist < 1)
{
float alpha = Mathf.Clamp01(1 - dist) * 0.5f;
pixels[y * size + x] = new Color(0, 0, 0, alpha);
}
else
{
pixels[y * size + x] = Color.clear;
}
}
}
tex.SetPixels(pixels);
tex.Apply();
return Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), 100f);
}
private void RegeneratePlaceholderSprite()
@@ -183,7 +280,7 @@ namespace TheIsland.Visual
private Texture2D CreatePlaceholderTexture(int width, int height)
{
Texture2D texture = new Texture2D(width, height, TextureFormat.RGBA32, false);
texture.filterMode = FilterMode.Point;
texture.filterMode = FilterMode.Bilinear;
// Clear to transparent
Color[] pixels = new Color[width * height];
@@ -192,30 +289,193 @@ namespace TheIsland.Visual
pixels[i] = Color.clear;
}
// Draw simple character shape
Vector2 center = new Vector2(width / 2f, height / 2f);
// Body (ellipse)
DrawEllipse(pixels, width, height, center + Vector2.down * 8, 14, 20, placeholderBodyColor);
// Create highlight and shadow colors
Color highlight = Color.Lerp(placeholderBodyColor, Color.white, 0.3f);
Color shadow = Color.Lerp(placeholderBodyColor, Color.black, 0.3f);
Color skinTone = new Color(0.95f, 0.8f, 0.7f);
Color skinShadow = new Color(0.85f, 0.65f, 0.55f);
// Head (circle)
DrawCircle(pixels, width, height, center + Vector2.up * 12, 12, placeholderBodyColor);
// Body (ellipse with shading)
Vector2 bodyCenter = center + Vector2.down * 6;
DrawShadedEllipse(pixels, width, height, bodyCenter, 16, 22, placeholderBodyColor, highlight, shadow);
// Outline
DrawCircleOutline(pixels, width, height, center + Vector2.up * 12, 12, placeholderOutlineColor, 2);
DrawEllipseOutline(pixels, width, height, center + Vector2.down * 8, 14, 20, placeholderOutlineColor, 2);
// Head (circle with skin tone)
Vector2 headCenter = center + Vector2.up * 14;
DrawShadedCircle(pixels, width, height, headCenter, 13, skinTone, Color.Lerp(skinTone, Color.white, 0.2f), skinShadow);
// Hair (top of head)
Color hairColor = placeholderOutlineColor;
DrawHair(pixels, width, height, headCenter, 13, hairColor);
// Eyes
DrawCircle(pixels, width, height, center + new Vector2(-4, 14), 2, Color.white);
DrawCircle(pixels, width, height, center + new Vector2(4, 14), 2, Color.white);
DrawCircle(pixels, width, height, center + new Vector2(-4, 14), 1, Color.black);
DrawCircle(pixels, width, height, center + new Vector2(4, 14), 1, Color.black);
DrawCircle(pixels, width, height, headCenter + new Vector2(-4, -1), 3, Color.white);
DrawCircle(pixels, width, height, headCenter + new Vector2(4, -1), 3, Color.white);
DrawCircle(pixels, width, height, headCenter + new Vector2(-4, -1), 1.5f, new Color(0.2f, 0.15f, 0.1f));
DrawCircle(pixels, width, height, headCenter + new Vector2(4, -1), 1.5f, new Color(0.2f, 0.15f, 0.1f));
// Eye highlights
DrawCircle(pixels, width, height, headCenter + new Vector2(-3, 0), 0.8f, Color.white);
DrawCircle(pixels, width, height, headCenter + new Vector2(5, 0), 0.8f, Color.white);
// Mouth (smile)
DrawSmile(pixels, width, height, headCenter + Vector2.down * 5, 4);
// Blush
DrawCircle(pixels, width, height, headCenter + new Vector2(-7, -3), 2, new Color(1f, 0.6f, 0.6f, 0.4f));
DrawCircle(pixels, width, height, headCenter + new Vector2(7, -3), 2, new Color(1f, 0.6f, 0.6f, 0.4f));
// Arms
DrawArm(pixels, width, height, bodyCenter + new Vector2(-14, 5), -30, skinTone);
DrawArm(pixels, width, height, bodyCenter + new Vector2(14, 5), 30, skinTone);
// Legs
DrawLeg(pixels, width, height, bodyCenter + new Vector2(-6, -20), placeholderBodyColor);
DrawLeg(pixels, width, height, bodyCenter + new Vector2(6, -20), placeholderBodyColor);
// Outline
AddOutline(pixels, width, height, placeholderOutlineColor);
texture.SetPixels(pixels);
texture.Apply();
return texture;
}
private void DrawShadedCircle(Color[] pixels, int width, int height, Vector2 center, float radius, Color baseColor, Color highlight, Color shadow)
{
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
float dist = Vector2.Distance(new Vector2(x, y), center);
if (dist <= radius)
{
// Shading based on position relative to light source (top-left)
float dx = (x - center.x) / radius;
float dy = (y - center.y) / radius;
float shade = (-dx * 0.3f + dy * 0.7f) * 0.5f + 0.5f;
Color color = Color.Lerp(highlight, shadow, shade);
color = Color.Lerp(color, baseColor, 0.5f);
pixels[y * width + x] = color;
}
}
}
}
private void DrawShadedEllipse(Color[] pixels, int width, int height, Vector2 center, float rx, float ry, Color baseColor, Color highlight, Color shadow)
{
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
float dx = (x - center.x) / rx;
float dy = (y - center.y) / ry;
if (dx * dx + dy * dy <= 1)
{
float shade = (-dx * 0.3f + dy * 0.5f) * 0.5f + 0.5f;
Color color = Color.Lerp(highlight, shadow, shade);
color = Color.Lerp(color, baseColor, 0.5f);
pixels[y * width + x] = color;
}
}
}
}
private void DrawHair(Color[] pixels, int width, int height, Vector2 headCenter, float headRadius, Color hairColor)
{
// Draw hair on top half of head
for (int y = (int)(headCenter.y); y < height; y++)
{
for (int x = 0; x < width; x++)
{
float dist = Vector2.Distance(new Vector2(x, y), headCenter);
if (dist <= headRadius + 2 && dist >= headRadius - 4 && y > headCenter.y - 2)
{
float noise = Mathf.PerlinNoise(x * 0.3f, y * 0.3f);
if (noise > 0.3f)
{
pixels[y * width + x] = Color.Lerp(hairColor, hairColor * 0.7f, noise);
}
}
}
}
}
private void DrawSmile(Color[] pixels, int width, int height, Vector2 center, float smileWidth)
{
Color mouthColor = new Color(0.8f, 0.4f, 0.4f);
for (int x = (int)(center.x - smileWidth); x <= (int)(center.x + smileWidth); x++)
{
float t = (x - center.x + smileWidth) / (smileWidth * 2);
int y = (int)(center.y - Mathf.Sin(t * Mathf.PI) * 2);
if (x >= 0 && x < width && y >= 0 && y < height)
{
pixels[y * width + x] = mouthColor;
if (y > 0) pixels[(y - 1) * width + x] = mouthColor;
}
}
}
private void DrawArm(Color[] pixels, int width, int height, Vector2 start, float angle, Color skinColor)
{
float rad = angle * Mathf.Deg2Rad;
int length = 10;
for (int i = 0; i < length; i++)
{
int x = (int)(start.x + Mathf.Sin(rad) * i);
int y = (int)(start.y - Mathf.Cos(rad) * i);
DrawCircle(pixels, width, height, new Vector2(x, y), 2, skinColor);
}
}
private void DrawLeg(Color[] pixels, int width, int height, Vector2 start, Color clothColor)
{
for (int i = 0; i < 8; i++)
{
int x = (int)start.x;
int y = (int)(start.y - i);
if (y >= 0 && y < height)
{
DrawCircle(pixels, width, height, new Vector2(x, y), 3, clothColor);
}
}
// Shoe
DrawCircle(pixels, width, height, start + Vector2.down * 8, 4, new Color(0.3f, 0.2f, 0.15f));
}
private void AddOutline(Color[] pixels, int width, int height, Color outlineColor)
{
Color[] newPixels = (Color[])pixels.Clone();
for (int y = 1; y < height - 1; y++)
{
for (int x = 1; x < width - 1; x++)
{
if (pixels[y * width + x].a < 0.1f)
{
// Check neighbors
bool hasNeighbor = false;
for (int dy = -1; dy <= 1; dy++)
{
for (int dx = -1; dx <= 1; dx++)
{
if (pixels[(y + dy) * width + (x + dx)].a > 0.5f)
{
hasNeighbor = true;
break;
}
}
if (hasNeighbor) break;
}
if (hasNeighbor)
{
newPixels[y * width + x] = outlineColor;
}
}
}
}
System.Array.Copy(newPixels, pixels, pixels.Length);
}
private void DrawCircle(Color[] pixels, int width, int height, Vector2 center, float radius, Color color)
{
for (int y = 0; y < height; y++)
@@ -294,31 +554,35 @@ namespace TheIsland.Visual
_uiCanvas.sortingOrder = sortingOrder + 1;
var canvasRect = canvasObj.GetComponent<RectTransform>();
canvasRect.sizeDelta = new Vector2(400, 150);
canvasRect.sizeDelta = new Vector2(400, 180);
// Add billboard to canvas (configured for UI - full facing)
_uiBillboard = canvasObj.AddComponent<Billboard>();
_uiBillboard.ConfigureForUI();
// Create UI panel
var panel = CreateUIPanel(canvasObj.transform, new Vector2(350, 120));
// Create UI panel (increased height for mood bar)
var panel = CreateUIPanel(canvasObj.transform, new Vector2(350, 150));
// Name label
_nameLabel = CreateUIText(panel.transform, "NameLabel", "Agent", 36, Color.white, FontStyles.Bold);
SetRectPosition(_nameLabel.rectTransform, 0, 45, 320, 45);
SetRectPosition(_nameLabel.rectTransform, 0, 60, 320, 45);
// Personality label
_personalityLabel = CreateUIText(panel.transform, "PersonalityLabel", "(Personality)", 20,
new Color(0.8f, 0.8f, 0.8f), FontStyles.Italic);
SetRectPosition(_personalityLabel.rectTransform, 0, 15, 320, 25);
SetRectPosition(_personalityLabel.rectTransform, 0, 30, 320, 25);
// HP Bar
var hpBar = CreateProgressBar(panel.transform, "HPBar", "HP", hpHighColor, out _hpBarFill, out _hpText);
SetRectPosition(hpBar, 0, -15, 280, 24);
SetRectPosition(hpBar, 0, 0, 280, 24);
// Energy Bar
var energyBar = CreateProgressBar(panel.transform, "EnergyBar", "Energy", energyHighColor, out _energyBarFill, out _energyText);
SetRectPosition(energyBar, 0, -45, 280, 24);
SetRectPosition(energyBar, 0, -30, 280, 24);
// Mood Bar
var moodBar = CreateProgressBar(panel.transform, "MoodBar", "Mood", moodNeutralColor, out _moodBarFill, out _moodText);
SetRectPosition(moodBar, 0, -60, 280, 24);
// Death overlay
_deathOverlay = CreateDeathOverlay(panel.transform);
@@ -338,11 +602,79 @@ namespace TheIsland.Visual
rect.anchoredPosition = Vector2.zero;
var bg = panel.AddComponent<Image>();
bg.color = new Color(0, 0, 0, 0.6f);
bg.sprite = CreateRoundedRectSprite(32, 32, 8);
bg.type = Image.Type.Sliced;
bg.color = new Color(0.1f, 0.12f, 0.18f, 0.85f);
// Add subtle border
var borderObj = new GameObject("Border");
borderObj.transform.SetParent(panel.transform);
borderObj.transform.localPosition = Vector3.zero;
borderObj.transform.localRotation = Quaternion.identity;
borderObj.transform.localScale = Vector3.one;
var borderRect = borderObj.AddComponent<RectTransform>();
borderRect.anchorMin = Vector2.zero;
borderRect.anchorMax = Vector2.one;
borderRect.offsetMin = new Vector2(-2, -2);
borderRect.offsetMax = new Vector2(2, 2);
borderRect.SetAsFirstSibling();
var borderImg = borderObj.AddComponent<Image>();
borderImg.sprite = CreateRoundedRectSprite(32, 32, 8);
borderImg.type = Image.Type.Sliced;
borderImg.color = new Color(0.3f, 0.35f, 0.45f, 0.5f);
return panel;
}
private Sprite CreateRoundedRectSprite(int width, int height, int radius)
{
Texture2D tex = new Texture2D(width, height);
tex.filterMode = FilterMode.Bilinear;
Color[] pixels = new Color[width * height];
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
bool inRect = true;
// Check corners for rounding
if (x < radius && y < radius)
{
// Bottom-left corner
inRect = Vector2.Distance(new Vector2(x, y), new Vector2(radius, radius)) <= radius;
}
else if (x >= width - radius && y < radius)
{
// Bottom-right corner
inRect = Vector2.Distance(new Vector2(x, y), new Vector2(width - radius - 1, radius)) <= radius;
}
else if (x < radius && y >= height - radius)
{
// Top-left corner
inRect = Vector2.Distance(new Vector2(x, y), new Vector2(radius, height - radius - 1)) <= radius;
}
else if (x >= width - radius && y >= height - radius)
{
// Top-right corner
inRect = Vector2.Distance(new Vector2(x, y), new Vector2(width - radius - 1, height - radius - 1)) <= radius;
}
pixels[y * width + x] = inRect ? Color.white : Color.clear;
}
}
tex.SetPixels(pixels);
tex.Apply();
// Create 9-sliced sprite
return Sprite.Create(tex, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f), 100f,
0, SpriteMeshType.FullRect, new Vector4(radius, radius, radius, radius));
}
private TextMeshProUGUI CreateUIText(Transform parent, string name, string text,
float fontSize, Color color, FontStyles style = FontStyles.Normal)
{
@@ -510,6 +842,26 @@ namespace TheIsland.Visual
_energyText.text = $"Energy: {data.energy}";
}
// Update Mood bar
float moodPercent = data.mood / 100f;
if (_moodBarFill != null)
{
_moodBarFill.rectTransform.anchorMax = new Vector2(moodPercent, 1);
_moodBarFill.color = GetMoodColor(data.mood_state);
}
if (_moodText != null)
{
string moodIndicator = GetMoodEmoji(data.mood_state);
string moodLabel = data.mood_state switch
{
"happy" => "Happy",
"sad" => "Sad",
"anxious" => "Anxious",
_ => "Neutral"
};
_moodText.text = $"{moodIndicator} {moodLabel}: {data.mood}";
}
// Update death state
if (!data.IsAlive)
{
@@ -521,6 +873,29 @@ namespace TheIsland.Visual
}
}
private Color GetMoodColor(string moodState)
{
return moodState switch
{
"happy" => moodHappyColor,
"sad" => moodSadColor,
"anxious" => moodAnxiousColor,
_ => moodNeutralColor
};
}
private string GetMoodEmoji(string moodState)
{
// Use text symbols instead of emoji for font compatibility
return moodState switch
{
"happy" => "<color=#5AE65A>+</color>",
"sad" => "<color=#6AA8FF>-</color>",
"anxious" => "<color=#FF7777>!</color>",
_ => "<color=#FFD700>~</color>"
};
}
private void OnDeath()
{
if (_deathOverlay != null) _deathOverlay.SetActive(true);

View File

@@ -54,10 +54,17 @@ namespace TheIsland.Core
private int _currentTick;
private int _currentDay;
private int _nextSpawnIndex;
// World state
private string _currentTimeOfDay = "day";
private string _currentWeather = "Sunny";
#endregion
#region Properties
public int PlayerGold => _playerGold;
public string CurrentTimeOfDay => _currentTimeOfDay;
public string CurrentWeather => _currentWeather;
public int CurrentDay => _currentDay;
public int AliveAgentCount
{
get
@@ -135,6 +142,16 @@ namespace TheIsland.Core
network.OnTick += HandleTick;
network.OnSystemMessage += HandleSystemMessage;
network.OnUserUpdate += HandleUserUpdate;
// New phase events
network.OnWeatherChange += HandleWeatherChange;
network.OnPhaseChange += HandlePhaseChange;
network.OnDayChange += HandleDayChange;
network.OnHeal += HandleHeal;
network.OnEncourage += HandleEncourage;
network.OnTalk += HandleTalk;
network.OnRevive += HandleRevive;
network.OnSocialInteraction += HandleSocialInteraction;
}
private void UnsubscribeFromNetworkEvents()
@@ -151,6 +168,16 @@ namespace TheIsland.Core
network.OnTick -= HandleTick;
network.OnSystemMessage -= HandleSystemMessage;
network.OnUserUpdate -= HandleUserUpdate;
// New phase events
network.OnWeatherChange -= HandleWeatherChange;
network.OnPhaseChange -= HandlePhaseChange;
network.OnDayChange -= HandleDayChange;
network.OnHeal -= HandleHeal;
network.OnEncourage -= HandleEncourage;
network.OnTalk -= HandleTalk;
network.OnRevive -= HandleRevive;
network.OnSocialInteraction -= HandleSocialInteraction;
}
#endregion
@@ -215,7 +242,17 @@ namespace TheIsland.Core
{
if (tickInfo == null) return;
tickInfo.text = $"Day {_currentDay} | Tick {_currentTick} | Alive: {AliveAgentCount}";
// Format time of day nicely
string timeDisplay = _currentTimeOfDay switch
{
"dawn" => "Dawn",
"day" => "Day",
"dusk" => "Dusk",
"night" => "Night",
_ => "Day"
};
tickInfo.text = $"Day {_currentDay} | {timeDisplay} | {_currentWeather} | Tick {_currentTick} | Alive: {AliveAgentCount}";
}
private void UpdateGoldDisplay()
@@ -324,6 +361,17 @@ namespace TheIsland.Core
{
_currentTick = data.tick;
_currentDay = data.day;
// Update weather and time of day from tick data
if (!string.IsNullOrEmpty(data.time_of_day))
{
_currentTimeOfDay = data.time_of_day;
}
if (!string.IsNullOrEmpty(data.weather))
{
_currentWeather = data.weather;
}
UpdateTickInfo();
}
@@ -341,6 +389,102 @@ namespace TheIsland.Core
UpdateGoldDisplay();
}
}
private void HandleWeatherChange(WeatherChangeData data)
{
Debug.Log($"[GameManager] Weather changed: {data.old_weather} -> {data.new_weather}");
_currentWeather = data.new_weather;
ShowNotification($"Weather: {data.new_weather}");
UpdateTickInfo();
}
private void HandlePhaseChange(PhaseChangeData data)
{
Debug.Log($"[GameManager] Phase changed: {data.old_phase} -> {data.new_phase}");
_currentTimeOfDay = data.new_phase;
ShowNotification($"The {data.new_phase} begins...");
UpdateTickInfo();
}
private void HandleDayChange(DayChangeData data)
{
Debug.Log($"[GameManager] New day: {data.day}");
_currentDay = data.day;
ShowNotification($"Day {data.day} begins!");
UpdateTickInfo();
}
private void HandleHeal(HealEventData data)
{
Debug.Log($"[GameManager] Heal event: {data.message}");
// Update gold if this was our action
if (data.user == NetworkManager.Instance.Username)
{
_playerGold = data.user_gold;
UpdateGoldDisplay();
}
ShowNotification(data.message);
}
private void HandleEncourage(EncourageEventData data)
{
Debug.Log($"[GameManager] Encourage event: {data.message}");
// Update gold if this was our action
if (data.user == NetworkManager.Instance.Username)
{
_playerGold = data.user_gold;
UpdateGoldDisplay();
}
ShowNotification(data.message);
}
private void HandleTalk(TalkEventData data)
{
Debug.Log($"[GameManager] Talk event: {data.agent_name} responds about '{data.topic}'");
// Show the agent's speech response
if (_agentVisuals.TryGetValue(GetAgentIdByName(data.agent_name), out AgentVisual agentVisual))
{
agentVisual.ShowSpeech(data.response);
}
else if (_agentUIs.TryGetValue(GetAgentIdByName(data.agent_name), out AgentUI agentUI))
{
agentUI.ShowSpeech(data.response);
}
}
private void HandleRevive(ReviveEventData data)
{
Debug.Log($"[GameManager] Revive event: {data.message}");
// Update gold if this was our action
if (data.user == NetworkManager.Instance.Username)
{
_playerGold = data.user_gold;
UpdateGoldDisplay();
}
ShowNotification(data.message);
}
private void HandleSocialInteraction(SocialInteractionData data)
{
Debug.Log($"[GameManager] Social: {data.initiator_name} -> {data.target_name} ({data.interaction_type})");
// Show dialogue from initiator
if (_agentVisuals.TryGetValue(data.initiator_id, out AgentVisual initiatorVisual))
{
initiatorVisual.ShowSpeech(data.dialogue);
}
else if (_agentUIs.TryGetValue(data.initiator_id, out AgentUI initiatorUI))
{
initiatorUI.ShowSpeech(data.dialogue);
}
}
#endregion
#region Agent Management
@@ -429,6 +573,41 @@ namespace TheIsland.Core
}
return null;
}
/// <summary>
/// Get agent ID by name (searches all agent systems).
/// </summary>
private int GetAgentIdByName(string name)
{
// Check AgentVisual first (newest system)
foreach (var kvp in _agentVisuals)
{
if (kvp.Value.CurrentData?.name == name)
{
return kvp.Key;
}
}
// Check AgentUI
foreach (var kvp in _agentUIs)
{
if (kvp.Value.CurrentData?.name == name)
{
return kvp.Key;
}
}
// Check AgentController (legacy)
foreach (var kvp in _agents)
{
if (kvp.Value.CurrentData?.name == name)
{
return kvp.Key;
}
}
return -1;
}
#endregion
#region UI Actions

View File

@@ -43,6 +43,11 @@ namespace TheIsland.Models
public int energy;
public string inventory;
// Mood system (Phase 3)
public int mood;
public string mood_state; // "happy", "neutral", "sad", "anxious"
public string social_tendency; // "introvert", "extrovert", "neutral"
public bool IsAlive => status == "Alive";
}
@@ -99,6 +104,8 @@ namespace TheIsland.Models
public int tick;
public int day;
public int alive_agents;
public string time_of_day; // "dawn", "day", "dusk", "night"
public string weather; // "Sunny", "Cloudy", "Rainy", etc.
}
/// <summary>
@@ -129,6 +136,108 @@ namespace TheIsland.Models
public int day_count;
public string weather;
public int resource_level;
public int current_tick_in_day;
public string time_of_day; // "dawn", "day", "dusk", "night"
}
/// <summary>
/// Weather change event data.
/// </summary>
[Serializable]
public class WeatherChangeData
{
public string old_weather;
public string new_weather;
public string message;
}
/// <summary>
/// Phase change event data (day/night cycle).
/// </summary>
[Serializable]
public class PhaseChangeData
{
public string old_phase;
public string new_phase;
public int day;
public string message;
}
/// <summary>
/// Day change event data.
/// </summary>
[Serializable]
public class DayChangeData
{
public int day;
public string message;
}
/// <summary>
/// Heal event data.
/// </summary>
[Serializable]
public class HealEventData
{
public string user;
public string agent_name;
public int hp_restored;
public int agent_hp;
public int user_gold;
public string message;
}
/// <summary>
/// Encourage event data.
/// </summary>
[Serializable]
public class EncourageEventData
{
public string user;
public string agent_name;
public int mood_boost;
public int agent_mood;
public int user_gold;
public string message;
}
/// <summary>
/// Talk event data.
/// </summary>
[Serializable]
public class TalkEventData
{
public string user;
public string agent_name;
public string topic;
public string response;
}
/// <summary>
/// Revive event data.
/// </summary>
[Serializable]
public class ReviveEventData
{
public string user;
public string agent_name;
public int user_gold;
public string message;
}
/// <summary>
/// Social interaction event data.
/// </summary>
[Serializable]
public class SocialInteractionData
{
public int initiator_id;
public string initiator_name;
public int target_id;
public string target_name;
public string interaction_type; // "chat", "share_food", "help", "argue", "comfort"
public string relationship_type; // "stranger", "friend", "rival", etc.
public string dialogue;
}
/// <summary>
@@ -164,5 +273,25 @@ namespace TheIsland.Models
public const string USER_UPDATE = "user_update";
public const string WORLD_UPDATE = "world_update";
public const string CHECK = "check";
// Day/Night cycle (Phase 2)
public const string TIME_UPDATE = "time_update";
public const string PHASE_CHANGE = "phase_change";
public const string DAY_CHANGE = "day_change";
// Weather system (Phase 3)
public const string WEATHER_CHANGE = "weather_change";
public const string MOOD_UPDATE = "mood_update";
// New commands (Phase 4)
public const string HEAL = "heal";
public const string TALK = "talk";
public const string ENCOURAGE = "encourage";
public const string REVIVE = "revive";
// Social system (Phase 5)
public const string SOCIAL_INTERACTION = "social_interaction";
public const string RELATIONSHIP_CHANGE = "relationship_change";
public const string AUTO_REVIVE = "auto_revive";
}
}

View File

@@ -56,6 +56,17 @@ namespace TheIsland.Network
public event Action<TickData> OnTick;
public event Action<SystemEventData> OnSystemMessage;
public event Action<UserUpdateData> OnUserUpdate;
// New Phase events
public event Action<WeatherChangeData> OnWeatherChange;
public event Action<PhaseChangeData> OnPhaseChange;
public event Action<DayChangeData> OnDayChange;
public event Action<HealEventData> OnHeal;
public event Action<EncourageEventData> OnEncourage;
public event Action<TalkEventData> OnTalk;
public event Action<ReviveEventData> OnRevive;
public event Action<SocialInteractionData> OnSocialInteraction;
public event Action<WorldStateData> OnWorldUpdate;
#endregion
#region Private Fields
@@ -286,6 +297,52 @@ namespace TheIsland.Network
OnUserUpdate?.Invoke(userData);
break;
case EventTypes.WORLD_UPDATE:
var worldData = JsonUtility.FromJson<WorldStateData>(dataJson);
OnWorldUpdate?.Invoke(worldData);
break;
case EventTypes.WEATHER_CHANGE:
var weatherData = JsonUtility.FromJson<WeatherChangeData>(dataJson);
OnWeatherChange?.Invoke(weatherData);
break;
case EventTypes.PHASE_CHANGE:
var phaseData = JsonUtility.FromJson<PhaseChangeData>(dataJson);
OnPhaseChange?.Invoke(phaseData);
break;
case EventTypes.DAY_CHANGE:
var dayData = JsonUtility.FromJson<DayChangeData>(dataJson);
OnDayChange?.Invoke(dayData);
break;
case EventTypes.HEAL:
var healData = JsonUtility.FromJson<HealEventData>(dataJson);
OnHeal?.Invoke(healData);
break;
case EventTypes.ENCOURAGE:
var encourageData = JsonUtility.FromJson<EncourageEventData>(dataJson);
OnEncourage?.Invoke(encourageData);
break;
case EventTypes.TALK:
var talkData = JsonUtility.FromJson<TalkEventData>(dataJson);
OnTalk?.Invoke(talkData);
break;
case EventTypes.REVIVE:
case EventTypes.AUTO_REVIVE:
var reviveData = JsonUtility.FromJson<ReviveEventData>(dataJson);
OnRevive?.Invoke(reviveData);
break;
case EventTypes.SOCIAL_INTERACTION:
var socialData = JsonUtility.FromJson<SocialInteractionData>(dataJson);
OnSocialInteraction?.Invoke(socialData);
break;
case EventTypes.COMMENT:
// Comments can be logged but typically not displayed in 3D
Debug.Log($"[Chat] {json}");
@@ -423,6 +480,29 @@ namespace TheIsland.Network
SendCommand($"feed {agentName}");
}
public void HealAgent(string agentName)
{
SendCommand($"heal {agentName}");
}
public void EncourageAgent(string agentName)
{
SendCommand($"encourage {agentName}");
}
public void TalkToAgent(string agentName, string topic = "")
{
string cmd = string.IsNullOrEmpty(topic)
? $"talk {agentName}"
: $"talk {agentName} {topic}";
SendCommand(cmd);
}
public void ReviveAgent(string agentName)
{
SendCommand($"revive {agentName}");
}
public void CheckStatus()
{
SendCommand("check");

View File

@@ -84,6 +84,9 @@ namespace TheIsland.Visual
// Add CanvasGroup for fading
_canvasGroup = gameObject.AddComponent<CanvasGroup>();
// Create rounded rect sprite for bubble
Sprite roundedSprite = CreateRoundedBubbleSprite(32, 32, 10);
// Create outline (slightly larger background)
var outlineObj = new GameObject("Outline");
outlineObj.transform.SetParent(transform);
@@ -92,6 +95,8 @@ namespace TheIsland.Visual
outlineObj.transform.localScale = Vector3.one;
_bubbleOutline = outlineObj.AddComponent<Image>();
_bubbleOutline.sprite = roundedSprite;
_bubbleOutline.type = Image.Type.Sliced;
_bubbleOutline.color = outlineColor;
var outlineRect = outlineObj.GetComponent<RectTransform>();
outlineRect.anchorMin = Vector2.zero;
@@ -107,6 +112,8 @@ namespace TheIsland.Visual
bgObj.transform.localScale = Vector3.one;
_bubbleBackground = bgObj.AddComponent<Image>();
_bubbleBackground.sprite = roundedSprite;
_bubbleBackground.type = Image.Type.Sliced;
_bubbleBackground.color = bubbleColor;
var bgRect = bgObj.GetComponent<RectTransform>();
bgRect.anchorMin = Vector2.zero;
@@ -153,17 +160,88 @@ namespace TheIsland.Visual
tailRect.anchoredPosition = new Vector2(0, 0);
tailRect.sizeDelta = new Vector2(24, 16);
// Create a simple triangle using UI Image with a sprite
// For now, use a simple downward-pointing shape
// Create triangle sprite for tail
var tailImage = tail.AddComponent<Image>();
tailImage.sprite = CreateTriangleSprite(24, 16);
tailImage.color = bubbleColor;
// Note: For a proper triangle, you'd use a custom sprite.
// This creates a simple rectangle as placeholder.
// In production, replace with a triangle sprite.
return tail;
}
private Sprite CreateRoundedBubbleSprite(int width, int height, int radius)
{
Texture2D tex = new Texture2D(width, height);
tex.filterMode = FilterMode.Bilinear;
Color[] pixels = new Color[width * height];
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
bool inRect = true;
// Check corners for rounding
if (x < radius && y < radius)
{
inRect = Vector2.Distance(new Vector2(x, y), new Vector2(radius, radius)) <= radius;
}
else if (x >= width - radius && y < radius)
{
inRect = Vector2.Distance(new Vector2(x, y), new Vector2(width - radius - 1, radius)) <= radius;
}
else if (x < radius && y >= height - radius)
{
inRect = Vector2.Distance(new Vector2(x, y), new Vector2(radius, height - radius - 1)) <= radius;
}
else if (x >= width - radius && y >= height - radius)
{
inRect = Vector2.Distance(new Vector2(x, y), new Vector2(width - radius - 1, height - radius - 1)) <= radius;
}
pixels[y * width + x] = inRect ? Color.white : Color.clear;
}
}
tex.SetPixels(pixels);
tex.Apply();
return Sprite.Create(tex, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f), 100f,
0, SpriteMeshType.FullRect, new Vector4(radius, radius, radius, radius));
}
private Sprite CreateTriangleSprite(int width, int height)
{
Texture2D tex = new Texture2D(width, height);
tex.filterMode = FilterMode.Bilinear;
Color[] pixels = new Color[width * height];
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
// Triangle pointing down
float t = (float)y / height;
float halfWidth = (width / 2f) * (1 - t);
float center = width / 2f;
if (x >= center - halfWidth && x <= center + halfWidth)
{
pixels[y * width + x] = Color.white;
}
else
{
pixels[y * width + x] = Color.clear;
}
}
}
tex.SetPixels(pixels);
tex.Apply();
return Sprite.Create(tex, new Rect(0, 0, width, height), new Vector2(0.5f, 1f), 100f);
}
#endregion
#region Public API

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ba6aed8ea8f684710867429092622258
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,627 @@
using UnityEngine;
using TheIsland.Core;
using TheIsland.Network;
using TheIsland.Models;
namespace TheIsland.Visual
{
/// <summary>
/// Manages the island environment visuals including sky, ground, water, and lighting.
/// Creates a beautiful dynamic background that responds to time of day and weather.
/// </summary>
public class EnvironmentManager : MonoBehaviour
{
#region Singleton
private static EnvironmentManager _instance;
public static EnvironmentManager Instance => _instance;
#endregion
#region Sky Colors by Time of Day
[Header("Dawn Colors")]
[SerializeField] private Color dawnSkyTop = new Color(0.98f, 0.65f, 0.45f);
[SerializeField] private Color dawnSkyBottom = new Color(1f, 0.85f, 0.6f);
[SerializeField] private Color dawnAmbient = new Color(1f, 0.8f, 0.6f);
[Header("Day Colors")]
[SerializeField] private Color daySkyTop = new Color(0.4f, 0.7f, 1f);
[SerializeField] private Color daySkyBottom = new Color(0.7f, 0.9f, 1f);
[SerializeField] private Color dayAmbient = new Color(1f, 1f, 0.95f);
[Header("Dusk Colors")]
[SerializeField] private Color duskSkyTop = new Color(0.3f, 0.2f, 0.5f);
[SerializeField] private Color duskSkyBottom = new Color(1f, 0.5f, 0.3f);
[SerializeField] private Color duskAmbient = new Color(1f, 0.6f, 0.4f);
[Header("Night Colors")]
[SerializeField] private Color nightSkyTop = new Color(0.05f, 0.05f, 0.15f);
[SerializeField] private Color nightSkyBottom = new Color(0.1f, 0.15f, 0.3f);
[SerializeField] private Color nightAmbient = new Color(0.3f, 0.35f, 0.5f);
#endregion
#region Weather Modifiers
[Header("Weather Color Modifiers")]
[SerializeField] private Color cloudyTint = new Color(0.7f, 0.7f, 0.75f);
[SerializeField] private Color rainyTint = new Color(0.5f, 0.55f, 0.6f);
[SerializeField] private Color stormyTint = new Color(0.35f, 0.35f, 0.4f);
[SerializeField] private Color foggyTint = new Color(0.8f, 0.8f, 0.85f);
[SerializeField] private Color hotTint = new Color(1.1f, 0.95f, 0.85f);
#endregion
#region Ground & Water
[Header("Ground Settings")]
[SerializeField] private Color sandColor = new Color(0.95f, 0.87f, 0.7f);
[SerializeField] private Color sandDarkColor = new Color(0.8f, 0.7f, 0.5f);
[Header("Water Settings")]
[SerializeField] private Color waterShallowColor = new Color(0.3f, 0.8f, 0.9f, 0.8f);
[SerializeField] private Color waterDeepColor = new Color(0.1f, 0.4f, 0.6f, 0.9f);
[SerializeField] private float waveSpeed = 0.5f;
[SerializeField] private float waveAmplitude = 0.1f;
#endregion
#region References
private Camera _mainCamera;
private Material _skyMaterial;
private GameObject _groundPlane;
private GameObject _waterPlane;
private Material _groundMaterial;
private Material _waterMaterial;
private Light _mainLight;
// Current state
private string _currentTimeOfDay = "day";
private string _currentWeather = "Sunny";
private float _transitionProgress = 1f;
private Color _targetSkyTop, _targetSkyBottom;
private Color _currentSkyTop, _currentSkyBottom;
#endregion
#region Unity Lifecycle
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
_mainCamera = Camera.main;
CreateEnvironment();
}
private void Start()
{
// Subscribe to network events
var network = NetworkManager.Instance;
if (network != null)
{
network.OnPhaseChange += HandlePhaseChange;
network.OnWeatherChange += HandleWeatherChange;
network.OnTick += HandleTick;
}
// Set initial sky
UpdateSkyColors();
}
private void Update()
{
// Smooth sky transition
if (_transitionProgress < 1f)
{
_transitionProgress += Time.deltaTime * 0.5f;
_currentSkyTop = Color.Lerp(_currentSkyTop, _targetSkyTop, _transitionProgress);
_currentSkyBottom = Color.Lerp(_currentSkyBottom, _targetSkyBottom, _transitionProgress);
UpdateSkyMaterial();
}
// Animate water
AnimateWater();
}
private void OnDestroy()
{
var network = NetworkManager.Instance;
if (network != null)
{
network.OnPhaseChange -= HandlePhaseChange;
network.OnWeatherChange -= HandleWeatherChange;
network.OnTick -= HandleTick;
}
}
#endregion
#region Environment Creation
private void CreateEnvironment()
{
CreateSky();
CreateGround();
CreateWater();
CreateLighting();
CreateDecorations();
}
private void CreateSky()
{
// Create a gradient sky using a camera background shader
_skyMaterial = new Material(Shader.Find("Unlit/Color"));
// Create sky quad that fills the background
var skyObj = GameObject.CreatePrimitive(PrimitiveType.Quad);
skyObj.name = "SkyBackground";
skyObj.transform.SetParent(transform);
skyObj.transform.position = new Vector3(0, 5, 20);
skyObj.transform.localScale = new Vector3(60, 30, 1);
// Remove collider
Destroy(skyObj.GetComponent<Collider>());
// Create gradient material
_skyMaterial = CreateGradientMaterial();
skyObj.GetComponent<Renderer>().material = _skyMaterial;
skyObj.GetComponent<Renderer>().sortingOrder = -100;
// Set initial colors
_currentSkyTop = daySkyTop;
_currentSkyBottom = daySkyBottom;
_targetSkyTop = daySkyTop;
_targetSkyBottom = daySkyBottom;
UpdateSkyMaterial();
}
private Material CreateGradientMaterial()
{
// Create a simple shader for vertical gradient
string shaderCode = @"
Shader ""Custom/SkyGradient"" {
Properties {
_TopColor (""Top Color"", Color) = (0.4, 0.7, 1, 1)
_BottomColor (""Bottom Color"", Color) = (0.7, 0.9, 1, 1)
}
SubShader {
Tags { ""Queue""=""Background"" ""RenderType""=""Opaque"" }
Pass {
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include ""UnityCG.cginc""
fixed4 _TopColor;
fixed4 _BottomColor;
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(appdata_base v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 frag(v2f i) : SV_Target {
return lerp(_BottomColor, _TopColor, i.uv.y);
}
ENDCG
}
}
}";
// Since we can't create shaders at runtime easily, use a texture-based approach
return CreateGradientTextureMaterial();
}
private Material CreateGradientTextureMaterial()
{
// Create gradient texture
Texture2D gradientTex = new Texture2D(1, 256);
gradientTex.wrapMode = TextureWrapMode.Clamp;
for (int y = 0; y < 256; y++)
{
float t = y / 255f;
Color color = Color.Lerp(_currentSkyBottom, _currentSkyTop, t);
gradientTex.SetPixel(0, y, color);
}
gradientTex.Apply();
Material mat = new Material(Shader.Find("Unlit/Texture"));
mat.mainTexture = gradientTex;
return mat;
}
private void UpdateSkyMaterial()
{
if (_skyMaterial == null || _skyMaterial.mainTexture == null) return;
Texture2D tex = (Texture2D)_skyMaterial.mainTexture;
for (int y = 0; y < 256; y++)
{
float t = y / 255f;
Color color = Color.Lerp(_currentSkyBottom, _currentSkyTop, t);
tex.SetPixel(0, y, color);
}
tex.Apply();
}
private void CreateGround()
{
// Create sandy beach ground
_groundPlane = GameObject.CreatePrimitive(PrimitiveType.Quad);
_groundPlane.name = "GroundPlane";
_groundPlane.transform.SetParent(transform);
_groundPlane.transform.position = new Vector3(0, -0.5f, 5);
_groundPlane.transform.rotation = Quaternion.Euler(90, 0, 0);
_groundPlane.transform.localScale = new Vector3(40, 20, 1);
// Create sand texture
_groundMaterial = new Material(Shader.Find("Unlit/Texture"));
_groundMaterial.mainTexture = CreateSandTexture();
_groundPlane.GetComponent<Renderer>().material = _groundMaterial;
_groundPlane.GetComponent<Renderer>().sortingOrder = -50;
// Remove collider (we don't need physics)
Destroy(_groundPlane.GetComponent<Collider>());
}
private Texture2D CreateSandTexture()
{
int size = 128;
Texture2D tex = new Texture2D(size, size);
tex.filterMode = FilterMode.Bilinear;
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
// Create sandy noise pattern
float noise = Mathf.PerlinNoise(x * 0.1f, y * 0.1f) * 0.3f;
float detail = Mathf.PerlinNoise(x * 0.3f, y * 0.3f) * 0.1f;
Color baseColor = Color.Lerp(sandDarkColor, sandColor, 0.5f + noise + detail);
// Add some sparkle/grain
if (Random.value > 0.95f)
{
baseColor = Color.Lerp(baseColor, Color.white, 0.3f);
}
tex.SetPixel(x, y, baseColor);
}
}
tex.Apply();
return tex;
}
private void CreateWater()
{
// Create water plane at the horizon
_waterPlane = GameObject.CreatePrimitive(PrimitiveType.Quad);
_waterPlane.name = "WaterPlane";
_waterPlane.transform.SetParent(transform);
_waterPlane.transform.position = new Vector3(0, -0.3f, 12);
_waterPlane.transform.rotation = Quaternion.Euler(90, 0, 0);
_waterPlane.transform.localScale = new Vector3(60, 15, 1);
// Create water material
_waterMaterial = new Material(Shader.Find("Unlit/Transparent"));
_waterMaterial.mainTexture = CreateWaterTexture();
_waterPlane.GetComponent<Renderer>().material = _waterMaterial;
_waterPlane.GetComponent<Renderer>().sortingOrder = -40;
Destroy(_waterPlane.GetComponent<Collider>());
}
private Texture2D CreateWaterTexture()
{
int size = 128;
Texture2D tex = new Texture2D(size, size);
tex.filterMode = FilterMode.Bilinear;
tex.wrapMode = TextureWrapMode.Repeat;
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
float t = (float)y / size;
Color baseColor = Color.Lerp(waterShallowColor, waterDeepColor, t);
// Add wave highlights
float wave = Mathf.Sin(x * 0.2f + y * 0.1f) * 0.5f + 0.5f;
baseColor = Color.Lerp(baseColor, Color.white, wave * 0.1f);
tex.SetPixel(x, y, baseColor);
}
}
tex.Apply();
return tex;
}
private void AnimateWater()
{
if (_waterMaterial == null) return;
// Simple UV scrolling for wave effect
float offset = Time.time * waveSpeed * 0.1f;
_waterMaterial.mainTextureOffset = new Vector2(offset, offset * 0.5f);
}
private void CreateLighting()
{
// Find or create main directional light
_mainLight = FindFirstObjectByType<Light>();
if (_mainLight == null)
{
var lightObj = new GameObject("MainLight");
lightObj.transform.SetParent(transform);
_mainLight = lightObj.AddComponent<Light>();
_mainLight.type = LightType.Directional;
}
_mainLight.transform.rotation = Quaternion.Euler(50, -30, 0);
_mainLight.intensity = 1f;
_mainLight.color = dayAmbient;
// Set ambient light
RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Flat;
RenderSettings.ambientLight = dayAmbient;
}
private void CreateDecorations()
{
// Create palm tree silhouettes
CreatePalmTree(new Vector3(-8, 0, 8), 2.5f);
CreatePalmTree(new Vector3(-10, 0, 10), 3f);
CreatePalmTree(new Vector3(9, 0, 7), 2.2f);
CreatePalmTree(new Vector3(11, 0, 9), 2.8f);
// Create rocks
CreateRock(new Vector3(-5, 0, 4), 0.5f);
CreateRock(new Vector3(6, 0, 5), 0.7f);
CreateRock(new Vector3(-7, 0, 6), 0.4f);
}
private void CreatePalmTree(Vector3 position, float scale)
{
var treeObj = new GameObject("PalmTree");
treeObj.transform.SetParent(transform);
treeObj.transform.position = position;
// Create trunk (stretched capsule-ish shape using sprite)
var trunkSprite = new GameObject("Trunk");
trunkSprite.transform.SetParent(treeObj.transform);
trunkSprite.transform.localPosition = new Vector3(0, scale * 0.5f, 0);
var trunkRenderer = trunkSprite.AddComponent<SpriteRenderer>();
trunkRenderer.sprite = CreateTreeSprite();
trunkRenderer.sortingOrder = -20;
trunkSprite.transform.localScale = new Vector3(scale * 0.5f, scale, 1);
}
private Sprite CreateTreeSprite()
{
int width = 64;
int height = 128;
Texture2D tex = new Texture2D(width, height);
Color trunk = new Color(0.4f, 0.25f, 0.15f);
Color trunkDark = new Color(0.3f, 0.18f, 0.1f);
Color leaf = new Color(0.2f, 0.5f, 0.2f);
Color leafBright = new Color(0.3f, 0.65f, 0.25f);
// Clear
Color[] pixels = new Color[width * height];
for (int i = 0; i < pixels.Length; i++) pixels[i] = Color.clear;
// Draw trunk
int trunkWidth = 8;
int trunkStart = width / 2 - trunkWidth / 2;
for (int y = 0; y < height * 0.6f; y++)
{
for (int x = trunkStart; x < trunkStart + trunkWidth; x++)
{
float noise = Mathf.PerlinNoise(x * 0.2f, y * 0.1f);
pixels[y * width + x] = Color.Lerp(trunkDark, trunk, noise);
}
}
// Draw palm fronds
DrawPalmFronds(pixels, width, height, leaf, leafBright);
tex.SetPixels(pixels);
tex.Apply();
tex.filterMode = FilterMode.Point;
return Sprite.Create(tex, new Rect(0, 0, width, height), new Vector2(0.5f, 0));
}
private void DrawPalmFronds(Color[] pixels, int width, int height, Color leaf, Color leafBright)
{
Vector2 center = new Vector2(width / 2, height * 0.65f);
// Draw several fronds
float[] angles = { -60, -30, 0, 30, 60, -80, 80 };
foreach (float angle in angles)
{
DrawFrond(pixels, width, height, center, angle, leaf, leafBright);
}
}
private void DrawFrond(Color[] pixels, int width, int height, Vector2 start, float angle, Color leaf, Color leafBright)
{
float rad = angle * Mathf.Deg2Rad;
int length = 35;
for (int i = 0; i < length; i++)
{
float t = i / (float)length;
float droop = t * t * 15; // Fronds droop more at the end
int x = (int)(start.x + Mathf.Sin(rad) * i);
int y = (int)(start.y + Mathf.Cos(rad) * i - droop);
// Draw thick frond
for (int dx = -2; dx <= 2; dx++)
{
for (int dy = -1; dy <= 1; dy++)
{
int px = x + dx;
int py = y + dy;
if (px >= 0 && px < width && py >= 0 && py < height)
{
float brightness = Mathf.PerlinNoise(px * 0.1f, py * 0.1f);
pixels[py * width + px] = Color.Lerp(leaf, leafBright, brightness);
}
}
}
}
}
private void CreateRock(Vector3 position, float scale)
{
var rockObj = new GameObject("Rock");
rockObj.transform.SetParent(transform);
rockObj.transform.position = position;
var rockRenderer = rockObj.AddComponent<SpriteRenderer>();
rockRenderer.sprite = CreateRockSprite();
rockRenderer.sortingOrder = -15;
rockObj.transform.localScale = Vector3.one * scale;
}
private Sprite CreateRockSprite()
{
int size = 32;
Texture2D tex = new Texture2D(size, size);
Color rockDark = new Color(0.3f, 0.3f, 0.35f);
Color rockLight = new Color(0.5f, 0.5f, 0.55f);
Color[] pixels = new Color[size * size];
for (int i = 0; i < pixels.Length; i++) pixels[i] = Color.clear;
// Draw rock shape
Vector2 center = new Vector2(size / 2, size / 3);
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
float dx = (x - center.x) / (size * 0.4f);
float dy = (y - center.y) / (size * 0.3f);
float dist = dx * dx + dy * dy;
if (dist < 1 && y < size * 0.7f)
{
float noise = Mathf.PerlinNoise(x * 0.2f, y * 0.2f);
pixels[y * size + x] = Color.Lerp(rockDark, rockLight, noise);
}
}
}
tex.SetPixels(pixels);
tex.Apply();
tex.filterMode = FilterMode.Point;
return Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0));
}
#endregion
#region Event Handlers
private void HandlePhaseChange(PhaseChangeData data)
{
_currentTimeOfDay = data.new_phase;
UpdateSkyColors();
}
private void HandleWeatherChange(WeatherChangeData data)
{
_currentWeather = data.new_weather;
UpdateSkyColors();
}
private void HandleTick(TickData data)
{
if (!string.IsNullOrEmpty(data.time_of_day) && data.time_of_day != _currentTimeOfDay)
{
_currentTimeOfDay = data.time_of_day;
UpdateSkyColors();
}
if (!string.IsNullOrEmpty(data.weather) && data.weather != _currentWeather)
{
_currentWeather = data.weather;
UpdateSkyColors();
}
}
private void UpdateSkyColors()
{
// Get base colors for time of day
Color baseTop, baseBottom, ambient;
switch (_currentTimeOfDay)
{
case "dawn":
baseTop = dawnSkyTop;
baseBottom = dawnSkyBottom;
ambient = dawnAmbient;
break;
case "dusk":
baseTop = duskSkyTop;
baseBottom = duskSkyBottom;
ambient = duskAmbient;
break;
case "night":
baseTop = nightSkyTop;
baseBottom = nightSkyBottom;
ambient = nightAmbient;
break;
default: // day
baseTop = daySkyTop;
baseBottom = daySkyBottom;
ambient = dayAmbient;
break;
}
// Apply weather tint
Color weatherTint = Color.white;
switch (_currentWeather)
{
case "Cloudy": weatherTint = cloudyTint; break;
case "Rainy": weatherTint = rainyTint; break;
case "Stormy": weatherTint = stormyTint; break;
case "Foggy": weatherTint = foggyTint; break;
case "Hot": weatherTint = hotTint; break;
}
_targetSkyTop = baseTop * weatherTint;
_targetSkyBottom = baseBottom * weatherTint;
_transitionProgress = 0f;
// Update lighting
if (_mainLight != null)
{
_mainLight.color = ambient * weatherTint;
_mainLight.intensity = _currentTimeOfDay == "night" ? 0.3f : 1f;
}
RenderSettings.ambientLight = ambient * weatherTint * 0.8f;
}
#endregion
#region Public API
/// <summary>
/// Force update the environment to specific conditions.
/// </summary>
public void SetEnvironment(string timeOfDay, string weather)
{
_currentTimeOfDay = timeOfDay;
_currentWeather = weather;
UpdateSkyColors();
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6aa9102a04d7544619ec0187e065eda9

View File

@@ -0,0 +1,95 @@
using UnityEngine;
namespace TheIsland.Visual
{
/// <summary>
/// Bootstraps the scene with all visual components.
/// Attach this to an empty GameObject in your scene to automatically
/// create the environment, weather effects, and other visual systems.
/// </summary>
public class SceneBootstrap : MonoBehaviour
{
[Header("Auto-Create Components")]
[SerializeField] private bool createEnvironment = true;
[SerializeField] private bool createWeatherEffects = true;
[Header("Camera Settings")]
[SerializeField] private bool configureCamera = true;
[SerializeField] private Vector3 cameraPosition = new Vector3(0, 3, -8);
[SerializeField] private Vector3 cameraRotation = new Vector3(15, 0, 0);
[SerializeField] private float cameraFieldOfView = 60f;
private void Awake()
{
// Configure camera
if (configureCamera)
{
ConfigureMainCamera();
}
// Create environment
if (createEnvironment && EnvironmentManager.Instance == null)
{
CreateEnvironmentManager();
}
// Create weather effects
if (createWeatherEffects && WeatherEffects.Instance == null)
{
CreateWeatherEffects();
}
Debug.Log("[SceneBootstrap] Visual systems initialized");
}
private void ConfigureMainCamera()
{
Camera mainCamera = Camera.main;
if (mainCamera == null)
{
var camObj = new GameObject("Main Camera");
mainCamera = camObj.AddComponent<Camera>();
camObj.AddComponent<AudioListener>();
camObj.tag = "MainCamera";
}
mainCamera.transform.position = cameraPosition;
mainCamera.transform.rotation = Quaternion.Euler(cameraRotation);
mainCamera.fieldOfView = cameraFieldOfView;
mainCamera.clearFlags = CameraClearFlags.SolidColor;
mainCamera.backgroundColor = new Color(0.4f, 0.6f, 0.9f); // Fallback sky color
Debug.Log("[SceneBootstrap] Camera configured");
}
private void CreateEnvironmentManager()
{
var envObj = new GameObject("EnvironmentManager");
envObj.AddComponent<EnvironmentManager>();
Debug.Log("[SceneBootstrap] EnvironmentManager created");
}
private void CreateWeatherEffects()
{
var weatherObj = new GameObject("WeatherEffects");
weatherObj.AddComponent<WeatherEffects>();
Debug.Log("[SceneBootstrap] WeatherEffects created");
}
/// <summary>
/// Call this to manually refresh all visual systems.
/// </summary>
public void RefreshVisuals()
{
if (EnvironmentManager.Instance != null)
{
EnvironmentManager.Instance.SetEnvironment("day", "Sunny");
}
if (WeatherEffects.Instance != null)
{
WeatherEffects.Instance.SetWeather("Sunny");
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 35dc7c6201b284023b5ab113ffab8add

View File

@@ -0,0 +1,473 @@
using UnityEngine;
using TheIsland.Network;
using TheIsland.Models;
namespace TheIsland.Visual
{
/// <summary>
/// Creates and manages weather particle effects.
/// Responds to weather changes from the server.
/// </summary>
public class WeatherEffects : MonoBehaviour
{
#region Singleton
private static WeatherEffects _instance;
public static WeatherEffects Instance => _instance;
#endregion
#region Configuration
[Header("Rain Settings")]
[SerializeField] private int rainParticleCount = 500;
[SerializeField] private int stormParticleCount = 1000;
[SerializeField] private Color rainColor = new Color(0.7f, 0.8f, 0.9f, 0.6f);
[Header("Sun Settings")]
[SerializeField] private int sunRayCount = 50;
[SerializeField] private Color sunRayColor = new Color(1f, 0.95f, 0.8f, 0.3f);
[Header("Fog Settings")]
[SerializeField] private Color fogColor = new Color(0.85f, 0.85f, 0.9f, 0.5f);
[Header("Hot Weather Settings")]
[SerializeField] private int heatWaveCount = 30;
[SerializeField] private Color heatColor = new Color(1f, 0.9f, 0.7f, 0.2f);
#endregion
#region References
private ParticleSystem _rainSystem;
private ParticleSystem _sunRaySystem;
private ParticleSystem _fogSystem;
private ParticleSystem _heatSystem;
private ParticleSystem _cloudSystem;
private string _currentWeather = "Sunny";
#endregion
#region Unity Lifecycle
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
CreateAllEffects();
}
private void Start()
{
var network = NetworkManager.Instance;
if (network != null)
{
network.OnWeatherChange += HandleWeatherChange;
network.OnTick += HandleTick;
}
// Start with sunny weather
SetWeather("Sunny");
}
private void OnDestroy()
{
var network = NetworkManager.Instance;
if (network != null)
{
network.OnWeatherChange -= HandleWeatherChange;
network.OnTick -= HandleTick;
}
}
#endregion
#region Effect Creation
private void CreateAllEffects()
{
CreateRainEffect();
CreateSunRayEffect();
CreateFogEffect();
CreateHeatEffect();
CreateCloudEffect();
}
private void CreateRainEffect()
{
var rainObj = new GameObject("RainEffect");
rainObj.transform.SetParent(transform);
rainObj.transform.position = new Vector3(0, 10, 5);
_rainSystem = rainObj.AddComponent<ParticleSystem>();
var main = _rainSystem.main;
main.maxParticles = stormParticleCount;
main.startLifetime = 1.5f;
main.startSpeed = 15f;
main.startSize = 0.05f;
main.startColor = rainColor;
main.simulationSpace = ParticleSystemSimulationSpace.World;
main.gravityModifier = 1.5f;
var emission = _rainSystem.emission;
emission.rateOverTime = rainParticleCount;
var shape = _rainSystem.shape;
shape.shapeType = ParticleSystemShapeType.Box;
shape.scale = new Vector3(25, 0.1f, 15);
// Renderer settings
var renderer = rainObj.GetComponent<ParticleSystemRenderer>();
renderer.material = CreateParticleMaterial(rainColor);
renderer.sortingOrder = 50;
// Start stopped
_rainSystem.Stop();
}
private void CreateSunRayEffect()
{
var sunObj = new GameObject("SunRayEffect");
sunObj.transform.SetParent(transform);
sunObj.transform.position = new Vector3(5, 8, 10);
sunObj.transform.rotation = Quaternion.Euler(45, -30, 0);
_sunRaySystem = sunObj.AddComponent<ParticleSystem>();
var main = _sunRaySystem.main;
main.maxParticles = sunRayCount;
main.startLifetime = 3f;
main.startSpeed = 0.5f;
main.startSize = new ParticleSystem.MinMaxCurve(0.5f, 2f);
main.startColor = sunRayColor;
main.simulationSpace = ParticleSystemSimulationSpace.World;
var emission = _sunRaySystem.emission;
emission.rateOverTime = 10;
var shape = _sunRaySystem.shape;
shape.shapeType = ParticleSystemShapeType.Cone;
shape.angle = 15;
shape.radius = 3;
var colorOverLifetime = _sunRaySystem.colorOverLifetime;
colorOverLifetime.enabled = true;
Gradient gradient = new Gradient();
gradient.SetKeys(
new GradientColorKey[] { new GradientColorKey(Color.white, 0), new GradientColorKey(Color.white, 1) },
new GradientAlphaKey[] { new GradientAlphaKey(0, 0), new GradientAlphaKey(0.3f, 0.3f), new GradientAlphaKey(0, 1) }
);
colorOverLifetime.color = gradient;
var sizeOverLifetime = _sunRaySystem.sizeOverLifetime;
sizeOverLifetime.enabled = true;
sizeOverLifetime.size = new ParticleSystem.MinMaxCurve(1f, new AnimationCurve(
new Keyframe(0, 0.5f), new Keyframe(0.5f, 1f), new Keyframe(1, 1.5f)));
var renderer = sunObj.GetComponent<ParticleSystemRenderer>();
renderer.material = CreateSunRayMaterial();
renderer.sortingOrder = 40;
_sunRaySystem.Stop();
}
private void CreateFogEffect()
{
var fogObj = new GameObject("FogEffect");
fogObj.transform.SetParent(transform);
fogObj.transform.position = new Vector3(0, 1, 5);
_fogSystem = fogObj.AddComponent<ParticleSystem>();
var main = _fogSystem.main;
main.maxParticles = 100;
main.startLifetime = 8f;
main.startSpeed = 0.3f;
main.startSize = new ParticleSystem.MinMaxCurve(3f, 6f);
main.startColor = fogColor;
main.simulationSpace = ParticleSystemSimulationSpace.World;
var emission = _fogSystem.emission;
emission.rateOverTime = 5;
var shape = _fogSystem.shape;
shape.shapeType = ParticleSystemShapeType.Box;
shape.scale = new Vector3(30, 2, 15);
var velocityOverLifetime = _fogSystem.velocityOverLifetime;
velocityOverLifetime.enabled = true;
velocityOverLifetime.x = new ParticleSystem.MinMaxCurve(-0.2f, 0.2f);
velocityOverLifetime.y = new ParticleSystem.MinMaxCurve(0.05f, 0.1f);
var colorOverLifetime = _fogSystem.colorOverLifetime;
colorOverLifetime.enabled = true;
Gradient gradient = new Gradient();
gradient.SetKeys(
new GradientColorKey[] { new GradientColorKey(Color.white, 0), new GradientColorKey(Color.white, 1) },
new GradientAlphaKey[] { new GradientAlphaKey(0, 0), new GradientAlphaKey(0.5f, 0.3f), new GradientAlphaKey(0.5f, 0.7f), new GradientAlphaKey(0, 1) }
);
colorOverLifetime.color = gradient;
var renderer = fogObj.GetComponent<ParticleSystemRenderer>();
renderer.material = CreateFogMaterial();
renderer.sortingOrder = 30;
_fogSystem.Stop();
}
private void CreateHeatEffect()
{
var heatObj = new GameObject("HeatEffect");
heatObj.transform.SetParent(transform);
heatObj.transform.position = new Vector3(0, 0, 5);
_heatSystem = heatObj.AddComponent<ParticleSystem>();
var main = _heatSystem.main;
main.maxParticles = heatWaveCount;
main.startLifetime = 4f;
main.startSpeed = 0.8f;
main.startSize = new ParticleSystem.MinMaxCurve(1f, 3f);
main.startColor = heatColor;
main.simulationSpace = ParticleSystemSimulationSpace.World;
var emission = _heatSystem.emission;
emission.rateOverTime = 8;
var shape = _heatSystem.shape;
shape.shapeType = ParticleSystemShapeType.Box;
shape.scale = new Vector3(20, 0.1f, 10);
var velocityOverLifetime = _heatSystem.velocityOverLifetime;
velocityOverLifetime.enabled = true;
velocityOverLifetime.y = 1f;
velocityOverLifetime.x = new ParticleSystem.MinMaxCurve(-0.3f, 0.3f);
var colorOverLifetime = _heatSystem.colorOverLifetime;
colorOverLifetime.enabled = true;
Gradient gradient = new Gradient();
gradient.SetKeys(
new GradientColorKey[] { new GradientColorKey(heatColor, 0), new GradientColorKey(heatColor, 1) },
new GradientAlphaKey[] { new GradientAlphaKey(0, 0), new GradientAlphaKey(0.2f, 0.3f), new GradientAlphaKey(0, 1) }
);
colorOverLifetime.color = gradient;
var renderer = heatObj.GetComponent<ParticleSystemRenderer>();
renderer.material = CreateHeatMaterial();
renderer.sortingOrder = 35;
_heatSystem.Stop();
}
private void CreateCloudEffect()
{
var cloudObj = new GameObject("CloudEffect");
cloudObj.transform.SetParent(transform);
cloudObj.transform.position = new Vector3(0, 8, 15);
_cloudSystem = cloudObj.AddComponent<ParticleSystem>();
var main = _cloudSystem.main;
main.maxParticles = 30;
main.startLifetime = 20f;
main.startSpeed = 0.2f;
main.startSize = new ParticleSystem.MinMaxCurve(5f, 10f);
main.startColor = new Color(1, 1, 1, 0.7f);
main.simulationSpace = ParticleSystemSimulationSpace.World;
var emission = _cloudSystem.emission;
emission.rateOverTime = 1;
var shape = _cloudSystem.shape;
shape.shapeType = ParticleSystemShapeType.Box;
shape.scale = new Vector3(40, 2, 5);
var velocityOverLifetime = _cloudSystem.velocityOverLifetime;
velocityOverLifetime.enabled = true;
velocityOverLifetime.x = 0.3f;
var renderer = cloudObj.GetComponent<ParticleSystemRenderer>();
renderer.material = CreateCloudMaterial();
renderer.sortingOrder = 25;
_cloudSystem.Stop();
}
#endregion
#region Material Creation
private Material CreateParticleMaterial(Color color)
{
Material mat = new Material(Shader.Find("Particles/Standard Unlit"));
mat.SetColor("_Color", color);
mat.SetFloat("_Mode", 2); // Fade mode
// Create simple white texture
Texture2D tex = new Texture2D(8, 8);
for (int i = 0; i < 64; i++) tex.SetPixel(i % 8, i / 8, Color.white);
tex.Apply();
mat.mainTexture = tex;
return mat;
}
private Material CreateSunRayMaterial()
{
Material mat = new Material(Shader.Find("Particles/Standard Unlit"));
mat.SetColor("_Color", sunRayColor);
mat.SetFloat("_Mode", 1); // Additive
// Create soft gradient texture
Texture2D tex = new Texture2D(32, 32);
Vector2 center = new Vector2(16, 16);
for (int y = 0; y < 32; y++)
{
for (int x = 0; x < 32; x++)
{
float dist = Vector2.Distance(new Vector2(x, y), center) / 16f;
float alpha = Mathf.Clamp01(1 - dist);
tex.SetPixel(x, y, new Color(1, 1, 1, alpha * alpha));
}
}
tex.Apply();
mat.mainTexture = tex;
return mat;
}
private Material CreateFogMaterial()
{
Material mat = new Material(Shader.Find("Particles/Standard Unlit"));
mat.SetColor("_Color", fogColor);
mat.SetFloat("_Mode", 2); // Fade
// Create soft cloud texture
Texture2D tex = new Texture2D(64, 64);
for (int y = 0; y < 64; y++)
{
for (int x = 0; x < 64; x++)
{
float noise = Mathf.PerlinNoise(x * 0.1f, y * 0.1f);
float dist = Vector2.Distance(new Vector2(x, y), new Vector2(32, 32)) / 32f;
float alpha = Mathf.Clamp01((1 - dist) * noise);
tex.SetPixel(x, y, new Color(1, 1, 1, alpha));
}
}
tex.Apply();
mat.mainTexture = tex;
return mat;
}
private Material CreateHeatMaterial()
{
Material mat = new Material(Shader.Find("Particles/Standard Unlit"));
mat.SetColor("_Color", heatColor);
mat.SetFloat("_Mode", 1); // Additive
// Create wavy heat texture
Texture2D tex = new Texture2D(32, 64);
for (int y = 0; y < 64; y++)
{
for (int x = 0; x < 32; x++)
{
float wave = Mathf.Sin((x + y * 0.3f) * 0.3f) * 0.5f + 0.5f;
float fade = 1 - Mathf.Abs(x - 16) / 16f;
float alpha = wave * fade * (1 - y / 64f);
tex.SetPixel(x, y, new Color(1, 1, 1, alpha * 0.3f));
}
}
tex.Apply();
mat.mainTexture = tex;
return mat;
}
private Material CreateCloudMaterial()
{
Material mat = new Material(Shader.Find("Particles/Standard Unlit"));
mat.SetColor("_Color", Color.white);
mat.SetFloat("_Mode", 2); // Fade
// Create fluffy cloud texture
Texture2D tex = new Texture2D(64, 64);
for (int y = 0; y < 64; y++)
{
for (int x = 0; x < 64; x++)
{
float noise1 = Mathf.PerlinNoise(x * 0.08f, y * 0.08f);
float noise2 = Mathf.PerlinNoise(x * 0.15f + 100, y * 0.15f + 100) * 0.5f;
float dist = Vector2.Distance(new Vector2(x, y), new Vector2(32, 32)) / 32f;
float alpha = Mathf.Clamp01((noise1 + noise2) * (1 - dist * dist));
tex.SetPixel(x, y, new Color(1, 1, 1, alpha * 0.8f));
}
}
tex.Apply();
tex.filterMode = FilterMode.Bilinear;
mat.mainTexture = tex;
return mat;
}
#endregion
#region Weather Control
private void HandleWeatherChange(WeatherChangeData data)
{
SetWeather(data.new_weather);
}
private void HandleTick(TickData data)
{
if (!string.IsNullOrEmpty(data.weather) && data.weather != _currentWeather)
{
SetWeather(data.weather);
}
}
public void SetWeather(string weather)
{
_currentWeather = weather;
// Stop all effects first
_rainSystem?.Stop();
_sunRaySystem?.Stop();
_fogSystem?.Stop();
_heatSystem?.Stop();
_cloudSystem?.Stop();
// Enable appropriate effects
switch (weather)
{
case "Sunny":
_sunRaySystem?.Play();
break;
case "Cloudy":
_cloudSystem?.Play();
break;
case "Rainy":
_rainSystem?.Play();
var rainMain = _rainSystem.main;
var rainEmission = _rainSystem.emission;
rainEmission.rateOverTime = rainParticleCount;
_cloudSystem?.Play();
break;
case "Stormy":
_rainSystem?.Play();
var stormMain = _rainSystem.main;
var stormEmission = _rainSystem.emission;
stormEmission.rateOverTime = stormParticleCount;
stormMain.startSpeed = 20f;
_cloudSystem?.Play();
// Could add lightning flashes here
break;
case "Foggy":
_fogSystem?.Play();
break;
case "Hot":
_heatSystem?.Play();
_sunRaySystem?.Play();
break;
}
Debug.Log($"[WeatherEffects] Weather set to: {weather}");
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d309e5fa265df414cba2779d11a0ed3c