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(
|
async def generate_idle_chat(
|
||||||
self,
|
self,
|
||||||
agent: "Agent",
|
agent: "Agent",
|
||||||
weather: str = "Sunny"
|
weather: str = "Sunny",
|
||||||
|
time_of_day: str = "day"
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Generate idle chatter for an agent based on current conditions.
|
Generate idle chatter for an agent based on current conditions.
|
||||||
@@ -228,6 +229,7 @@ class LLMService:
|
|||||||
Args:
|
Args:
|
||||||
agent: The Agent model instance
|
agent: The Agent model instance
|
||||||
weather: Current weather condition
|
weather: Current weather condition
|
||||||
|
time_of_day: Current time of day (dawn/day/dusk/night)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A spontaneous thought or comment from the agent
|
A spontaneous thought or comment from the agent
|
||||||
@@ -249,7 +251,7 @@ class LLMService:
|
|||||||
f"Personality: {agent.personality}. "
|
f"Personality: {agent.personality}. "
|
||||||
f"Current Status: HP={agent.hp}, Energy={agent.energy}. "
|
f"Current Status: HP={agent.hp}, Energy={agent.energy}. "
|
||||||
f"You are stranded on a survival island. "
|
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"Say something brief (under 15 words) about your situation or thoughts. "
|
||||||
f"Speak naturally, as if talking to yourself or nearby survivors."
|
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}")
|
logger.error(f"LLM API error for idle chat: {e}")
|
||||||
return self._get_mock_response(event_type)
|
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
|
# Global instance for easy import
|
||||||
llm_service = LLMService()
|
llm_service = LLMService()
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
SQLAlchemy ORM models for The Island.
|
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 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
|
from .database import Base
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ class User(Base):
|
|||||||
class Agent(Base):
|
class Agent(Base):
|
||||||
"""
|
"""
|
||||||
Represents an NPC survivor on the island.
|
Represents an NPC survivor on the island.
|
||||||
Has personality, health, energy, and inventory.
|
Has personality, health, energy, mood, and social attributes.
|
||||||
"""
|
"""
|
||||||
__tablename__ = "agents"
|
__tablename__ = "agents"
|
||||||
|
|
||||||
@@ -41,8 +41,23 @@ class Agent(Base):
|
|||||||
energy = Column(Integer, default=100)
|
energy = Column(Integer, default=100)
|
||||||
inventory = Column(String(500), default="{}") # JSON string
|
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):
|
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):
|
def to_dict(self):
|
||||||
"""Convert to dictionary for JSON serialization."""
|
"""Convert to dictionary for JSON serialization."""
|
||||||
@@ -53,14 +68,17 @@ class Agent(Base):
|
|||||||
"status": self.status,
|
"status": self.status,
|
||||||
"hp": self.hp,
|
"hp": self.hp,
|
||||||
"energy": self.energy,
|
"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):
|
class WorldState(Base):
|
||||||
"""
|
"""
|
||||||
Global state of the island environment.
|
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"
|
__tablename__ = "world_state"
|
||||||
|
|
||||||
@@ -69,13 +87,122 @@ class WorldState(Base):
|
|||||||
weather = Column(String(20), default="Sunny")
|
weather = Column(String(20), default="Sunny")
|
||||||
resource_level = Column(Integer, default=100)
|
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):
|
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):
|
def to_dict(self):
|
||||||
"""Convert to dictionary for JSON serialization."""
|
"""Convert to dictionary for JSON serialization."""
|
||||||
return {
|
return {
|
||||||
"day_count": self.day_count,
|
"day_count": self.day_count,
|
||||||
"weather": self.weather,
|
"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"
|
TICK = "tick"
|
||||||
SYSTEM = "system"
|
SYSTEM = "system"
|
||||||
ERROR = "error"
|
ERROR = "error"
|
||||||
|
|
||||||
# Island survival events
|
# Island survival events
|
||||||
AGENTS_UPDATE = "agents_update" # All agents status broadcast
|
AGENTS_UPDATE = "agents_update" # All agents status broadcast
|
||||||
AGENT_DIED = "agent_died" # An agent has died
|
AGENT_DIED = "agent_died" # An agent has died
|
||||||
@@ -24,6 +25,26 @@ class EventType(str, Enum):
|
|||||||
WORLD_UPDATE = "world_update" # World state update
|
WORLD_UPDATE = "world_update" # World state update
|
||||||
CHECK = "check" # Status check response
|
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):
|
class GameEvent(BaseModel):
|
||||||
"""
|
"""
|
||||||
|
|||||||
230
frontend/app.js
230
frontend/app.js
@@ -12,6 +12,13 @@ let userGold = 100;
|
|||||||
// Agents state
|
// Agents state
|
||||||
let agents = [];
|
let agents = [];
|
||||||
|
|
||||||
|
// World state
|
||||||
|
let worldState = {
|
||||||
|
day_count: 1,
|
||||||
|
time_of_day: 'day',
|
||||||
|
weather: 'Sunny'
|
||||||
|
};
|
||||||
|
|
||||||
// DOM Elements
|
// DOM Elements
|
||||||
const statusDot = document.getElementById('statusDot');
|
const statusDot = document.getElementById('statusDot');
|
||||||
const statusText = document.getElementById('statusText');
|
const statusText = document.getElementById('statusText');
|
||||||
@@ -87,6 +94,9 @@ function handleGameEvent(event) {
|
|||||||
updateAgentsUI(data.agents);
|
updateAgentsUI(data.agents);
|
||||||
break;
|
break;
|
||||||
case 'feed':
|
case 'feed':
|
||||||
|
case 'heal':
|
||||||
|
case 'encourage':
|
||||||
|
case 'revive':
|
||||||
case 'user_update':
|
case 'user_update':
|
||||||
updateUserGold(data);
|
updateUserGold(data);
|
||||||
break;
|
break;
|
||||||
@@ -97,7 +107,35 @@ function handleGameEvent(event) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'agent_speak':
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +190,10 @@ function createAgentCard(agent) {
|
|||||||
const statusClass = isDead ? 'dead' : 'alive';
|
const statusClass = isDead ? 'dead' : 'alive';
|
||||||
const statusText = isDead ? '已死亡' : '存活';
|
const statusText = isDead ? '已死亡' : '存活';
|
||||||
|
|
||||||
|
// Mood emoji and color
|
||||||
|
const moodEmoji = getMoodEmoji(agent.mood_state);
|
||||||
|
const moodColor = getMoodColor(agent.mood_state);
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="agent-header">
|
<div class="agent-header">
|
||||||
<div>
|
<div>
|
||||||
@@ -178,18 +220,175 @@ function createAgentCard(agent) {
|
|||||||
<div class="stat-bar-fill energy" style="width: ${agent.energy}%"></div>
|
<div class="stat-bar-fill energy" style="width: ${agent.energy}%"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="feed-btn" onclick="feedAgent('${agent.name}')" ${isDead ? 'disabled' : ''}>
|
<div class="stat-bar-container">
|
||||||
🍖 投喂 (10金币)
|
<div class="stat-bar-label">
|
||||||
</button>
|
<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;
|
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
|
* Feed an agent
|
||||||
*/
|
*/
|
||||||
function feedAgent(agentName) {
|
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) {
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
alert('未连接到服务器');
|
alert('未连接到服务器');
|
||||||
return;
|
return;
|
||||||
@@ -198,7 +397,7 @@ function feedAgent(agentName) {
|
|||||||
const user = getCurrentUser();
|
const user = getCurrentUser();
|
||||||
const payload = {
|
const payload = {
|
||||||
action: 'send_comment',
|
action: 'send_comment',
|
||||||
payload: { user, message: `feed ${agentName}` }
|
payload: { user, message: command }
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.send(JSON.stringify(payload));
|
ws.send(JSON.stringify(payload));
|
||||||
@@ -315,19 +514,38 @@ function formatEventData(eventType, data) {
|
|||||||
case 'comment':
|
case 'comment':
|
||||||
return `${data.user}: ${data.message}`;
|
return `${data.user}: ${data.message}`;
|
||||||
case 'tick':
|
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 'system':
|
||||||
case 'error':
|
case 'error':
|
||||||
case 'feed':
|
case 'feed':
|
||||||
|
case 'heal':
|
||||||
|
case 'encourage':
|
||||||
|
case 'revive':
|
||||||
|
case 'auto_revive':
|
||||||
case 'agent_died':
|
case 'agent_died':
|
||||||
case 'check':
|
case 'check':
|
||||||
return data.message;
|
return data.message;
|
||||||
case 'agent_speak':
|
case 'agent_speak':
|
||||||
return `💬 ${data.agent_name}: "${data.text}"`;
|
return `💬 ${data.agent_name}: "${data.text}"`;
|
||||||
|
case 'talk':
|
||||||
|
return `💬 ${data.agent_name} 对 ${data.user} 说: "${data.response}"`;
|
||||||
case 'agents_update':
|
case 'agents_update':
|
||||||
return `角色状态已更新`;
|
return `角色状态已更新`;
|
||||||
case 'user_update':
|
case 'user_update':
|
||||||
return `${data.user} 金币: ${data.gold}`;
|
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:
|
default:
|
||||||
return JSON.stringify(data);
|
return JSON.stringify(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -326,10 +326,64 @@
|
|||||||
.event.feed { border-color: #ffaa00; background: rgba(255, 170, 0, 0.1); color: #ffcc66; }
|
.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_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.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.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-time { color: #888; font-size: 11px; }
|
||||||
.event-type { font-weight: bold; text-transform: uppercase; 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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -356,6 +410,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- World State Panel -->
|
||||||
|
<div class="world-panel" id="worldDisplay">
|
||||||
|
<span>📅 第1天</span>
|
||||||
|
<span>☀️ 白天</span>
|
||||||
|
<span>☀️ Sunny</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Agents Section -->
|
<!-- Agents Section -->
|
||||||
<div class="agents-section">
|
<div class="agents-section">
|
||||||
<div class="speech-bubbles-overlay" id="speechBubblesOverlay"></div>
|
<div class="speech-bubbles-overlay" id="speechBubblesOverlay"></div>
|
||||||
@@ -379,7 +440,7 @@
|
|||||||
<button onclick="sendComment()">发送</button>
|
<button onclick="sendComment()">发送</button>
|
||||||
</div>
|
</div>
|
||||||
<p style="margin-top: 10px; font-size: 0.85rem; color: #888;">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ namespace TheIsland.Visual
|
|||||||
[SerializeField] private Color hpLowColor = new Color(0.9f, 0.3f, 0.3f);
|
[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 energyHighColor = new Color(1f, 0.8f, 0.2f);
|
||||||
[SerializeField] private Color energyLowColor = new Color(1f, 0.5f, 0.1f);
|
[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
|
#endregion
|
||||||
|
|
||||||
#region References
|
#region References
|
||||||
@@ -48,8 +54,11 @@ namespace TheIsland.Visual
|
|||||||
private TextMeshProUGUI _personalityLabel;
|
private TextMeshProUGUI _personalityLabel;
|
||||||
private Image _hpBarFill;
|
private Image _hpBarFill;
|
||||||
private Image _energyBarFill;
|
private Image _energyBarFill;
|
||||||
|
private Image _moodBarFill;
|
||||||
private TextMeshProUGUI _hpText;
|
private TextMeshProUGUI _hpText;
|
||||||
private TextMeshProUGUI _energyText;
|
private TextMeshProUGUI _energyText;
|
||||||
|
private TextMeshProUGUI _moodText;
|
||||||
|
private TextMeshProUGUI _moodEmoji;
|
||||||
private GameObject _deathOverlay;
|
private GameObject _deathOverlay;
|
||||||
private SpeechBubble _speechBubble;
|
private SpeechBubble _speechBubble;
|
||||||
private Billboard _spriteBillboard;
|
private Billboard _spriteBillboard;
|
||||||
@@ -61,6 +70,12 @@ namespace TheIsland.Visual
|
|||||||
private int _agentId;
|
private int _agentId;
|
||||||
private AgentData _currentData;
|
private AgentData _currentData;
|
||||||
private Coroutine _speechCoroutine;
|
private Coroutine _speechCoroutine;
|
||||||
|
|
||||||
|
// Animation state
|
||||||
|
private float _idleAnimTimer;
|
||||||
|
private float _breathScale = 1f;
|
||||||
|
private Vector3 _originalSpriteScale;
|
||||||
|
private float _bobOffset;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Properties
|
#region Properties
|
||||||
@@ -76,6 +91,33 @@ namespace TheIsland.Visual
|
|||||||
CreateVisuals();
|
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()
|
private void OnMouseDown()
|
||||||
{
|
{
|
||||||
if (!IsAlive)
|
if (!IsAlive)
|
||||||
@@ -162,8 +204,63 @@ namespace TheIsland.Visual
|
|||||||
RegeneratePlaceholderSprite();
|
RegeneratePlaceholderSprite();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store original scale for animation
|
||||||
|
_originalSpriteScale = spriteObj.transform.localScale;
|
||||||
|
|
||||||
// Add billboard
|
// Add billboard
|
||||||
_spriteBillboard = spriteObj.AddComponent<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()
|
private void RegeneratePlaceholderSprite()
|
||||||
@@ -183,7 +280,7 @@ namespace TheIsland.Visual
|
|||||||
private Texture2D CreatePlaceholderTexture(int width, int height)
|
private Texture2D CreatePlaceholderTexture(int width, int height)
|
||||||
{
|
{
|
||||||
Texture2D texture = new Texture2D(width, height, TextureFormat.RGBA32, false);
|
Texture2D texture = new Texture2D(width, height, TextureFormat.RGBA32, false);
|
||||||
texture.filterMode = FilterMode.Point;
|
texture.filterMode = FilterMode.Bilinear;
|
||||||
|
|
||||||
// Clear to transparent
|
// Clear to transparent
|
||||||
Color[] pixels = new Color[width * height];
|
Color[] pixels = new Color[width * height];
|
||||||
@@ -192,30 +289,193 @@ namespace TheIsland.Visual
|
|||||||
pixels[i] = Color.clear;
|
pixels[i] = Color.clear;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw simple character shape
|
|
||||||
Vector2 center = new Vector2(width / 2f, height / 2f);
|
Vector2 center = new Vector2(width / 2f, height / 2f);
|
||||||
|
|
||||||
// Body (ellipse)
|
// Create highlight and shadow colors
|
||||||
DrawEllipse(pixels, width, height, center + Vector2.down * 8, 14, 20, placeholderBodyColor);
|
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)
|
// Body (ellipse with shading)
|
||||||
DrawCircle(pixels, width, height, center + Vector2.up * 12, 12, placeholderBodyColor);
|
Vector2 bodyCenter = center + Vector2.down * 6;
|
||||||
|
DrawShadedEllipse(pixels, width, height, bodyCenter, 16, 22, placeholderBodyColor, highlight, shadow);
|
||||||
|
|
||||||
// Outline
|
// Head (circle with skin tone)
|
||||||
DrawCircleOutline(pixels, width, height, center + Vector2.up * 12, 12, placeholderOutlineColor, 2);
|
Vector2 headCenter = center + Vector2.up * 14;
|
||||||
DrawEllipseOutline(pixels, width, height, center + Vector2.down * 8, 14, 20, placeholderOutlineColor, 2);
|
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
|
// Eyes
|
||||||
DrawCircle(pixels, width, height, center + new Vector2(-4, 14), 2, Color.white);
|
DrawCircle(pixels, width, height, headCenter + new Vector2(-4, -1), 3, Color.white);
|
||||||
DrawCircle(pixels, width, height, center + new Vector2(4, 14), 2, Color.white);
|
DrawCircle(pixels, width, height, headCenter + new Vector2(4, -1), 3, Color.white);
|
||||||
DrawCircle(pixels, width, height, center + new Vector2(-4, 14), 1, Color.black);
|
DrawCircle(pixels, width, height, headCenter + new Vector2(-4, -1), 1.5f, new Color(0.2f, 0.15f, 0.1f));
|
||||||
DrawCircle(pixels, width, height, center + new Vector2(4, 14), 1, Color.black);
|
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.SetPixels(pixels);
|
||||||
texture.Apply();
|
texture.Apply();
|
||||||
return texture;
|
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)
|
private void DrawCircle(Color[] pixels, int width, int height, Vector2 center, float radius, Color color)
|
||||||
{
|
{
|
||||||
for (int y = 0; y < height; y++)
|
for (int y = 0; y < height; y++)
|
||||||
@@ -294,31 +554,35 @@ namespace TheIsland.Visual
|
|||||||
_uiCanvas.sortingOrder = sortingOrder + 1;
|
_uiCanvas.sortingOrder = sortingOrder + 1;
|
||||||
|
|
||||||
var canvasRect = canvasObj.GetComponent<RectTransform>();
|
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)
|
// Add billboard to canvas (configured for UI - full facing)
|
||||||
_uiBillboard = canvasObj.AddComponent<Billboard>();
|
_uiBillboard = canvasObj.AddComponent<Billboard>();
|
||||||
_uiBillboard.ConfigureForUI();
|
_uiBillboard.ConfigureForUI();
|
||||||
|
|
||||||
// Create UI panel
|
// Create UI panel (increased height for mood bar)
|
||||||
var panel = CreateUIPanel(canvasObj.transform, new Vector2(350, 120));
|
var panel = CreateUIPanel(canvasObj.transform, new Vector2(350, 150));
|
||||||
|
|
||||||
// Name label
|
// Name label
|
||||||
_nameLabel = CreateUIText(panel.transform, "NameLabel", "Agent", 36, Color.white, FontStyles.Bold);
|
_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
|
// Personality label
|
||||||
_personalityLabel = CreateUIText(panel.transform, "PersonalityLabel", "(Personality)", 20,
|
_personalityLabel = CreateUIText(panel.transform, "PersonalityLabel", "(Personality)", 20,
|
||||||
new Color(0.8f, 0.8f, 0.8f), FontStyles.Italic);
|
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
|
// HP Bar
|
||||||
var hpBar = CreateProgressBar(panel.transform, "HPBar", "HP", hpHighColor, out _hpBarFill, out _hpText);
|
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
|
// Energy Bar
|
||||||
var energyBar = CreateProgressBar(panel.transform, "EnergyBar", "Energy", energyHighColor, out _energyBarFill, out _energyText);
|
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
|
// Death overlay
|
||||||
_deathOverlay = CreateDeathOverlay(panel.transform);
|
_deathOverlay = CreateDeathOverlay(panel.transform);
|
||||||
@@ -338,11 +602,79 @@ namespace TheIsland.Visual
|
|||||||
rect.anchoredPosition = Vector2.zero;
|
rect.anchoredPosition = Vector2.zero;
|
||||||
|
|
||||||
var bg = panel.AddComponent<Image>();
|
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;
|
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,
|
private TextMeshProUGUI CreateUIText(Transform parent, string name, string text,
|
||||||
float fontSize, Color color, FontStyles style = FontStyles.Normal)
|
float fontSize, Color color, FontStyles style = FontStyles.Normal)
|
||||||
{
|
{
|
||||||
@@ -510,6 +842,26 @@ namespace TheIsland.Visual
|
|||||||
_energyText.text = $"Energy: {data.energy}";
|
_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
|
// Update death state
|
||||||
if (!data.IsAlive)
|
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()
|
private void OnDeath()
|
||||||
{
|
{
|
||||||
if (_deathOverlay != null) _deathOverlay.SetActive(true);
|
if (_deathOverlay != null) _deathOverlay.SetActive(true);
|
||||||
|
|||||||
@@ -54,10 +54,17 @@ namespace TheIsland.Core
|
|||||||
private int _currentTick;
|
private int _currentTick;
|
||||||
private int _currentDay;
|
private int _currentDay;
|
||||||
private int _nextSpawnIndex;
|
private int _nextSpawnIndex;
|
||||||
|
|
||||||
|
// World state
|
||||||
|
private string _currentTimeOfDay = "day";
|
||||||
|
private string _currentWeather = "Sunny";
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Properties
|
#region Properties
|
||||||
public int PlayerGold => _playerGold;
|
public int PlayerGold => _playerGold;
|
||||||
|
public string CurrentTimeOfDay => _currentTimeOfDay;
|
||||||
|
public string CurrentWeather => _currentWeather;
|
||||||
|
public int CurrentDay => _currentDay;
|
||||||
public int AliveAgentCount
|
public int AliveAgentCount
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -135,6 +142,16 @@ namespace TheIsland.Core
|
|||||||
network.OnTick += HandleTick;
|
network.OnTick += HandleTick;
|
||||||
network.OnSystemMessage += HandleSystemMessage;
|
network.OnSystemMessage += HandleSystemMessage;
|
||||||
network.OnUserUpdate += HandleUserUpdate;
|
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()
|
private void UnsubscribeFromNetworkEvents()
|
||||||
@@ -151,6 +168,16 @@ namespace TheIsland.Core
|
|||||||
network.OnTick -= HandleTick;
|
network.OnTick -= HandleTick;
|
||||||
network.OnSystemMessage -= HandleSystemMessage;
|
network.OnSystemMessage -= HandleSystemMessage;
|
||||||
network.OnUserUpdate -= HandleUserUpdate;
|
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
|
#endregion
|
||||||
|
|
||||||
@@ -215,7 +242,17 @@ namespace TheIsland.Core
|
|||||||
{
|
{
|
||||||
if (tickInfo == null) return;
|
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()
|
private void UpdateGoldDisplay()
|
||||||
@@ -324,6 +361,17 @@ namespace TheIsland.Core
|
|||||||
{
|
{
|
||||||
_currentTick = data.tick;
|
_currentTick = data.tick;
|
||||||
_currentDay = data.day;
|
_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();
|
UpdateTickInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,6 +389,102 @@ namespace TheIsland.Core
|
|||||||
UpdateGoldDisplay();
|
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
|
#endregion
|
||||||
|
|
||||||
#region Agent Management
|
#region Agent Management
|
||||||
@@ -429,6 +573,41 @@ namespace TheIsland.Core
|
|||||||
}
|
}
|
||||||
return null;
|
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
|
#endregion
|
||||||
|
|
||||||
#region UI Actions
|
#region UI Actions
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ namespace TheIsland.Models
|
|||||||
public int energy;
|
public int energy;
|
||||||
public string inventory;
|
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";
|
public bool IsAlive => status == "Alive";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +104,8 @@ namespace TheIsland.Models
|
|||||||
public int tick;
|
public int tick;
|
||||||
public int day;
|
public int day;
|
||||||
public int alive_agents;
|
public int alive_agents;
|
||||||
|
public string time_of_day; // "dawn", "day", "dusk", "night"
|
||||||
|
public string weather; // "Sunny", "Cloudy", "Rainy", etc.
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -129,6 +136,108 @@ namespace TheIsland.Models
|
|||||||
public int day_count;
|
public int day_count;
|
||||||
public string weather;
|
public string weather;
|
||||||
public int resource_level;
|
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>
|
/// <summary>
|
||||||
@@ -164,5 +273,25 @@ namespace TheIsland.Models
|
|||||||
public const string USER_UPDATE = "user_update";
|
public const string USER_UPDATE = "user_update";
|
||||||
public const string WORLD_UPDATE = "world_update";
|
public const string WORLD_UPDATE = "world_update";
|
||||||
public const string CHECK = "check";
|
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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,17 @@ namespace TheIsland.Network
|
|||||||
public event Action<TickData> OnTick;
|
public event Action<TickData> OnTick;
|
||||||
public event Action<SystemEventData> OnSystemMessage;
|
public event Action<SystemEventData> OnSystemMessage;
|
||||||
public event Action<UserUpdateData> OnUserUpdate;
|
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
|
#endregion
|
||||||
|
|
||||||
#region Private Fields
|
#region Private Fields
|
||||||
@@ -286,6 +297,52 @@ namespace TheIsland.Network
|
|||||||
OnUserUpdate?.Invoke(userData);
|
OnUserUpdate?.Invoke(userData);
|
||||||
break;
|
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:
|
case EventTypes.COMMENT:
|
||||||
// Comments can be logged but typically not displayed in 3D
|
// Comments can be logged but typically not displayed in 3D
|
||||||
Debug.Log($"[Chat] {json}");
|
Debug.Log($"[Chat] {json}");
|
||||||
@@ -423,6 +480,29 @@ namespace TheIsland.Network
|
|||||||
SendCommand($"feed {agentName}");
|
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()
|
public void CheckStatus()
|
||||||
{
|
{
|
||||||
SendCommand("check");
|
SendCommand("check");
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ namespace TheIsland.Visual
|
|||||||
// Add CanvasGroup for fading
|
// Add CanvasGroup for fading
|
||||||
_canvasGroup = gameObject.AddComponent<CanvasGroup>();
|
_canvasGroup = gameObject.AddComponent<CanvasGroup>();
|
||||||
|
|
||||||
|
// Create rounded rect sprite for bubble
|
||||||
|
Sprite roundedSprite = CreateRoundedBubbleSprite(32, 32, 10);
|
||||||
|
|
||||||
// Create outline (slightly larger background)
|
// Create outline (slightly larger background)
|
||||||
var outlineObj = new GameObject("Outline");
|
var outlineObj = new GameObject("Outline");
|
||||||
outlineObj.transform.SetParent(transform);
|
outlineObj.transform.SetParent(transform);
|
||||||
@@ -92,6 +95,8 @@ namespace TheIsland.Visual
|
|||||||
outlineObj.transform.localScale = Vector3.one;
|
outlineObj.transform.localScale = Vector3.one;
|
||||||
|
|
||||||
_bubbleOutline = outlineObj.AddComponent<Image>();
|
_bubbleOutline = outlineObj.AddComponent<Image>();
|
||||||
|
_bubbleOutline.sprite = roundedSprite;
|
||||||
|
_bubbleOutline.type = Image.Type.Sliced;
|
||||||
_bubbleOutline.color = outlineColor;
|
_bubbleOutline.color = outlineColor;
|
||||||
var outlineRect = outlineObj.GetComponent<RectTransform>();
|
var outlineRect = outlineObj.GetComponent<RectTransform>();
|
||||||
outlineRect.anchorMin = Vector2.zero;
|
outlineRect.anchorMin = Vector2.zero;
|
||||||
@@ -107,6 +112,8 @@ namespace TheIsland.Visual
|
|||||||
bgObj.transform.localScale = Vector3.one;
|
bgObj.transform.localScale = Vector3.one;
|
||||||
|
|
||||||
_bubbleBackground = bgObj.AddComponent<Image>();
|
_bubbleBackground = bgObj.AddComponent<Image>();
|
||||||
|
_bubbleBackground.sprite = roundedSprite;
|
||||||
|
_bubbleBackground.type = Image.Type.Sliced;
|
||||||
_bubbleBackground.color = bubbleColor;
|
_bubbleBackground.color = bubbleColor;
|
||||||
var bgRect = bgObj.GetComponent<RectTransform>();
|
var bgRect = bgObj.GetComponent<RectTransform>();
|
||||||
bgRect.anchorMin = Vector2.zero;
|
bgRect.anchorMin = Vector2.zero;
|
||||||
@@ -153,17 +160,88 @@ namespace TheIsland.Visual
|
|||||||
tailRect.anchoredPosition = new Vector2(0, 0);
|
tailRect.anchoredPosition = new Vector2(0, 0);
|
||||||
tailRect.sizeDelta = new Vector2(24, 16);
|
tailRect.sizeDelta = new Vector2(24, 16);
|
||||||
|
|
||||||
// Create a simple triangle using UI Image with a sprite
|
// Create triangle sprite for tail
|
||||||
// For now, use a simple downward-pointing shape
|
|
||||||
var tailImage = tail.AddComponent<Image>();
|
var tailImage = tail.AddComponent<Image>();
|
||||||
|
tailImage.sprite = CreateTriangleSprite(24, 16);
|
||||||
tailImage.color = bubbleColor;
|
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;
|
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
|
#endregion
|
||||||
|
|
||||||
#region Public API
|
#region Public API
|
||||||
|
|||||||
8
unity-client/Assets/Scripts/Visual.meta
Normal file
8
unity-client/Assets/Scripts/Visual.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ba6aed8ea8f684710867429092622258
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
627
unity-client/Assets/Scripts/Visual/EnvironmentManager.cs
Normal file
627
unity-client/Assets/Scripts/Visual/EnvironmentManager.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6aa9102a04d7544619ec0187e065eda9
|
||||||
95
unity-client/Assets/Scripts/Visual/SceneBootstrap.cs
Normal file
95
unity-client/Assets/Scripts/Visual/SceneBootstrap.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 35dc7c6201b284023b5ab113ffab8add
|
||||||
473
unity-client/Assets/Scripts/Visual/WeatherEffects.cs
Normal file
473
unity-client/Assets/Scripts/Visual/WeatherEffects.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d309e5fa265df414cba2779d11a0ed3c
|
||||||
Reference in New Issue
Block a user