Compare commits
10 Commits
1f29010de6
...
f270a8b099
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f270a8b099 | ||
|
|
0187c5ecbe | ||
|
|
01abd3c2dc | ||
|
|
119afede55 | ||
|
|
b0e6a3488c | ||
|
|
91694ba802 | ||
|
|
8277778106 | ||
|
|
432f178fc5 | ||
|
|
20c82276fa | ||
|
|
d1b02b4dfd |
31
README.md
31
README.md
@@ -14,6 +14,7 @@ the-island/
|
||||
│ ├── models.py # SQLAlchemy 数据模型
|
||||
│ ├── schemas.py # Pydantic 消息模式
|
||||
│ ├── llm.py # LLM 集成 (对话生成)
|
||||
│ ├── memory_service.py # Agent 记忆管理服务
|
||||
│ ├── twitch_service.py # Twitch 聊天机器人
|
||||
│ └── database.py # 数据库配置
|
||||
├── frontend/ # Web 调试客户端
|
||||
@@ -35,6 +36,13 @@ the-island/
|
||||
- **天气系统**: 晴天、多云、雨天、暴风雨、炎热、雾天
|
||||
- **社交系统**: 角色间自主社交互动
|
||||
- **休闲模式**: 自动复活、降低难度
|
||||
- **自主行动**: 角色会自动进行采集、休息、社交等行为
|
||||
- **疾病机制**: 恶劣天气和低免疫力可能导致生病
|
||||
- **制作系统**: 使用草药制作药品治愈疾病
|
||||
- **资源稀缺**: 树木果实有限,每日再生
|
||||
- **社交角色**: 领导者、追随者、独行者动态关系
|
||||
- **记忆系统**: Agent 会记住重要的互动和事件
|
||||
- **随机事件**: 风暴破坏、发现宝藏、野兽袭击等
|
||||
|
||||
### 玩家命令
|
||||
| 命令 | 格式 | 金币消耗 | 效果 |
|
||||
@@ -54,6 +62,17 @@ the-island/
|
||||
|
||||
每个角色有独特性格,会根据性格做出不同反应和社交行为。
|
||||
|
||||
#### 角色属性
|
||||
| 属性 | 说明 |
|
||||
|------|------|
|
||||
| HP | 生命值,归零则死亡 |
|
||||
| 能量 | 行动力,过低会影响行动 |
|
||||
| 心情 | 情绪状态,影响社交和决策 |
|
||||
| 免疫力 | 抵抗疾病的能力 (0-100) |
|
||||
| 社交角色 | leader/follower/loner/neutral |
|
||||
| 当前行动 | Idle/Gather/Sleep/Socialize 等 |
|
||||
| 位置 | tree_left/tree_right/campfire/herb_patch 等 |
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 后端
|
||||
@@ -97,12 +116,13 @@ uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
| 脚本 | 功能 |
|
||||
|------|------|
|
||||
| `NetworkManager.cs` | WebSocket 连接管理、消息收发 |
|
||||
| `GameManager.cs` | 游戏状态管理、角色生成 |
|
||||
| `GameManager.cs` | 游戏状态管理、角色生成、行动系统 |
|
||||
| `UIManager.cs` | 主 UI 界面 (顶部状态栏、底部命令输入) |
|
||||
| `EventLog.cs` | 事件日志面板 (显示游戏事件) |
|
||||
| `AgentVisual.cs` | 角色视觉组件 (精灵、血条、对话框) |
|
||||
| `AgentVisual.cs` | 角色视觉组件 (精灵、血条、对话框、状态图标) |
|
||||
| `EnvironmentManager.cs` | 环境场景 (沙滩、海洋、天空) |
|
||||
| `WeatherEffects.cs` | 天气粒子效果 (雨、雾、热浪) |
|
||||
| `Models.cs` | 数据模型 (Agent、WorldState、事件数据) |
|
||||
|
||||
### 视觉特性
|
||||
- 程序化生成的 2.5D 角色精灵
|
||||
@@ -147,10 +167,17 @@ HEAL # 治疗反馈
|
||||
TALK # 对话反馈
|
||||
ENCOURAGE # 鼓励反馈
|
||||
REVIVE # 复活反馈
|
||||
GIFT_EFFECT # Bits 打赏特效
|
||||
|
||||
# 社交系统
|
||||
SOCIAL_INTERACTION # 角色间社交
|
||||
AUTO_REVIVE # 自动复活 (休闲模式)
|
||||
|
||||
# 自主行动系统 (Phase 13+)
|
||||
AGENT_ACTION # 角色执行行动 (采集/休息/社交等)
|
||||
CRAFT # 制作物品 (药品等)
|
||||
USE_ITEM # 使用物品
|
||||
RANDOM_EVENT # 随机事件 (风暴/宝藏/野兽等)
|
||||
```
|
||||
|
||||
## Twitch 直播集成
|
||||
|
||||
@@ -14,6 +14,7 @@ from .schemas import GameEvent, EventType
|
||||
from .database import init_db, get_db_session
|
||||
from .models import User, Agent, WorldState, GameConfig, AgentRelationship
|
||||
from .llm import llm_service
|
||||
from .memory_service import memory_service
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .server import ConnectionManager
|
||||
@@ -214,6 +215,35 @@ class GameEngine:
|
||||
# =========================================================================
|
||||
# Day/Night cycle (Phase 2)
|
||||
# =========================================================================
|
||||
async def _process_weather(self) -> None:
|
||||
"""Process weather transitions."""
|
||||
with get_db_session() as db:
|
||||
world = db.query(WorldState).first()
|
||||
if not world:
|
||||
return
|
||||
|
||||
world.weather_duration += 1
|
||||
|
||||
# Should we transition?
|
||||
if world.weather_duration >= random.randint(WEATHER_MIN_DURATION, WEATHER_MAX_DURATION):
|
||||
old_weather = world.weather
|
||||
transitions = WEATHER_TRANSITIONS.get(old_weather, {"Sunny": 1.0})
|
||||
|
||||
# Biased random choice
|
||||
choices = list(transitions.keys())
|
||||
weights = list(transitions.values())
|
||||
new_weather = random.choices(choices, weights=weights, k=1)[0]
|
||||
|
||||
if new_weather != old_weather:
|
||||
world.weather = new_weather
|
||||
world.weather_duration = 0
|
||||
|
||||
await self._broadcast_event(EventType.WEATHER_CHANGE, {
|
||||
"old_weather": old_weather,
|
||||
"new_weather": new_weather,
|
||||
"message": f"The weather is changing to {new_weather}."
|
||||
})
|
||||
|
||||
async def _advance_time(self) -> Optional[dict]:
|
||||
"""Advance time and return phase change info if applicable."""
|
||||
with get_db_session() as db:
|
||||
@@ -228,9 +258,14 @@ class GameEngine:
|
||||
if world.current_tick_in_day >= TICKS_PER_DAY:
|
||||
world.current_tick_in_day = 0
|
||||
world.day_count += 1
|
||||
|
||||
# Phase 17-A: Regenerate resources
|
||||
world.tree_left_fruit = min(5, world.tree_left_fruit + 2)
|
||||
world.tree_right_fruit = min(5, world.tree_right_fruit + 2)
|
||||
|
||||
await self._broadcast_event(EventType.DAY_CHANGE, {
|
||||
"day": world.day_count,
|
||||
"message": f"Day {world.day_count} begins!"
|
||||
"message": f"Day {world.day_count} begins! Trees have new fruit."
|
||||
})
|
||||
|
||||
# Determine current phase
|
||||
@@ -299,6 +334,55 @@ class GameEngine:
|
||||
else:
|
||||
agent.mood_state = "anxious"
|
||||
|
||||
# =========================================================================
|
||||
# Relationship 2.0 (Phase 17-B)
|
||||
# =========================================================================
|
||||
async def _assign_social_roles(self) -> None:
|
||||
"""Assign social roles based on personality and social tendency."""
|
||||
with get_db_session() as db:
|
||||
agents = db.query(Agent).filter(Agent.status == "Alive").all()
|
||||
|
||||
for agent in agents:
|
||||
if agent.social_role != "neutral":
|
||||
continue # Already assigned
|
||||
|
||||
# Role assignment based on personality and tendency
|
||||
if agent.social_tendency == "extrovert" and agent.mood > 60:
|
||||
agent.social_role = "leader"
|
||||
elif agent.social_tendency == "introvert" and agent.mood < 40:
|
||||
agent.social_role = "loner"
|
||||
elif random.random() < 0.3:
|
||||
agent.social_role = "follower"
|
||||
# Otherwise stays neutral
|
||||
|
||||
async def _process_clique_behavior(self) -> None:
|
||||
"""Leaders influence followers' actions."""
|
||||
# Run occasionally
|
||||
if self._tick_count % 10 != 0:
|
||||
return
|
||||
|
||||
with get_db_session() as db:
|
||||
leaders = db.query(Agent).filter(
|
||||
Agent.status == "Alive",
|
||||
Agent.social_role == "leader"
|
||||
).all()
|
||||
|
||||
followers = db.query(Agent).filter(
|
||||
Agent.status == "Alive",
|
||||
Agent.social_role == "follower"
|
||||
).all()
|
||||
|
||||
for leader in leaders:
|
||||
# Followers near leader copy their action
|
||||
for follower in followers:
|
||||
if follower.current_action == "Idle" and leader.current_action not in ["Idle", None]:
|
||||
follower.current_action = leader.current_action
|
||||
follower.location = leader.location
|
||||
await self._broadcast_event(EventType.COMMENT, {
|
||||
"user": "System",
|
||||
"message": f"{follower.name} follows {leader.name}'s lead!"
|
||||
})
|
||||
|
||||
# =========================================================================
|
||||
# Survival mechanics
|
||||
# =========================================================================
|
||||
@@ -315,6 +399,45 @@ class GameEngine:
|
||||
alive_agents = db.query(Agent).filter(Agent.status == "Alive").all()
|
||||
|
||||
for agent in alive_agents:
|
||||
# --- Sickness Mechanics (Phase 15) ---
|
||||
# 1. Contracting Sickness
|
||||
if not agent.is_sick:
|
||||
sickness_chance = 0.01 # Base 1% per tick (every 5s)
|
||||
|
||||
# Weather impact
|
||||
current_weather = world.weather if world else "Sunny"
|
||||
if current_weather == "Rainy":
|
||||
sickness_chance += 0.05
|
||||
elif current_weather == "Stormy":
|
||||
sickness_chance += 0.10
|
||||
|
||||
# Immunity impact (Higher immunity = lower chance)
|
||||
# Immunity 50 -> -2.5%, Immunity 100 -> -5%
|
||||
sickness_chance -= (agent.immunity / 2000.0)
|
||||
|
||||
if random.random() < sickness_chance:
|
||||
agent.is_sick = True
|
||||
agent.mood -= 20
|
||||
logger.info(f"Agent {agent.name} has fallen sick!")
|
||||
# We could broadcast a specific event, but AGENTS_UPDATE will handle visual state
|
||||
# Just log it or maybe a system message?
|
||||
await self._broadcast_event(EventType.COMMENT, {
|
||||
"user": "System",
|
||||
"message": f"{agent.name} is looking pale... (Sick)"
|
||||
})
|
||||
|
||||
# 2. Sickness Effects
|
||||
if agent.is_sick:
|
||||
# Decay HP and Energy faster
|
||||
agent.hp = max(0, agent.hp - 2)
|
||||
agent.energy = max(0, agent.energy - 2)
|
||||
|
||||
# Lower mood over time
|
||||
if self._tick_count % 5 == 0:
|
||||
agent.mood = max(0, agent.mood - 1)
|
||||
|
||||
# --- End Sickness ---
|
||||
|
||||
# Calculate energy decay with all modifiers
|
||||
base_decay = BASE_ENERGY_DECAY_PER_TICK
|
||||
decay = base_decay * config.energy_decay_multiplier
|
||||
@@ -323,9 +446,9 @@ class GameEngine:
|
||||
|
||||
agent.energy = max(0, agent.energy - int(decay))
|
||||
|
||||
# HP recovery during day phases
|
||||
# HP recovery during day phases (Only if NOT sick)
|
||||
hp_recovery = phase_mod.get("hp_recovery", 0)
|
||||
if hp_recovery > 0 and agent.energy > 20:
|
||||
if hp_recovery > 0 and agent.energy > 20 and not agent.is_sick:
|
||||
agent.hp = min(100, agent.hp + hp_recovery)
|
||||
|
||||
# Starvation damage
|
||||
@@ -337,6 +460,9 @@ class GameEngine:
|
||||
if agent.hp <= 0:
|
||||
agent.status = "Dead"
|
||||
agent.death_tick = self._tick_count
|
||||
if agent.is_sick:
|
||||
# Clear sickness on death
|
||||
agent.is_sick = False
|
||||
deaths.append({"name": agent.name, "personality": agent.personality})
|
||||
logger.info(f"Agent {agent.name} has died!")
|
||||
|
||||
@@ -346,6 +472,18 @@ class GameEngine:
|
||||
"agent_name": death["name"],
|
||||
"message": f"{death['name']} ({death['personality']}) has died..."
|
||||
})
|
||||
|
||||
# Phase 14: Alive agents remember the death
|
||||
with get_db_session() as db:
|
||||
witnesses = db.query(Agent).filter(Agent.status == "Alive").all()
|
||||
for witness in witnesses:
|
||||
await memory_service.add_memory(
|
||||
agent_id=witness.id,
|
||||
description=f"{death['name']} died. It was a sad day.",
|
||||
importance=8,
|
||||
related_entity_name=death["name"],
|
||||
memory_type="event"
|
||||
)
|
||||
|
||||
async def _process_auto_revive(self) -> None:
|
||||
"""Auto-revive dead agents in casual mode."""
|
||||
@@ -497,6 +635,237 @@ class GameEngine:
|
||||
except Exception as e:
|
||||
logger.error(f"Error in social dialogue: {e}")
|
||||
|
||||
# =========================================================================
|
||||
# Autonomous Agency (Phase 13)
|
||||
# =========================================================================
|
||||
async def _process_activity_tick(self) -> None:
|
||||
"""Decide and execute autonomous agent actions."""
|
||||
# Only process activity every few ticks to avoid chaotic movement
|
||||
if self._tick_count % 3 != 0:
|
||||
return
|
||||
|
||||
with get_db_session() as db:
|
||||
world = db.query(WorldState).first()
|
||||
if not world:
|
||||
return
|
||||
|
||||
agents = db.query(Agent).filter(Agent.status == "Alive").all()
|
||||
|
||||
for agent in agents:
|
||||
new_action = agent.current_action
|
||||
new_location = agent.location
|
||||
target_name = None
|
||||
should_update = False
|
||||
|
||||
# 1. Critical Needs (Override everything)
|
||||
if world.time_of_day == "night":
|
||||
if agent.current_action != "Sleep":
|
||||
new_action = "Sleep"
|
||||
new_location = "campfire"
|
||||
should_update = True
|
||||
elif agent.energy < 30:
|
||||
if agent.current_action != "Gather":
|
||||
new_action = "Gather"
|
||||
new_location = random.choice(["tree_left", "tree_right"])
|
||||
should_update = True
|
||||
|
||||
# 1.5. Sickness Handling (Phase 16)
|
||||
elif agent.is_sick:
|
||||
inv = self._get_inventory(agent)
|
||||
if inv.get("medicine", 0) > 0:
|
||||
# Use medicine immediately
|
||||
await self._use_medicine(agent)
|
||||
new_action = "Use Medicine"
|
||||
new_location = agent.location
|
||||
should_update = True
|
||||
elif inv.get("herb", 0) >= 3:
|
||||
# Craft medicine
|
||||
await self._craft_medicine(agent)
|
||||
new_action = "Craft Medicine"
|
||||
new_location = agent.location
|
||||
should_update = True
|
||||
elif agent.current_action != "Gather Herb":
|
||||
# Go gather herbs
|
||||
new_action = "Gather Herb"
|
||||
new_location = "herb_patch"
|
||||
should_update = True
|
||||
|
||||
# 2. Mood / Social Needs
|
||||
elif agent.mood < 40 and agent.current_action not in ["Sleep", "Gather", "Socialize", "Gather Herb"]:
|
||||
new_action = "Socialize"
|
||||
potential_friends = [a for a in agents if a.id != agent.id]
|
||||
if potential_friends:
|
||||
friend = random.choice(potential_friends)
|
||||
new_location = "agent"
|
||||
target_name = friend.name
|
||||
should_update = True
|
||||
|
||||
# 3. Boredom / Wandering
|
||||
elif agent.current_action == "Idle" or agent.current_action is None:
|
||||
if random.random() < 0.3:
|
||||
new_action = "Wander"
|
||||
new_location = "nearby"
|
||||
should_update = True
|
||||
|
||||
# 4. Finish Tasks (Simulation)
|
||||
elif agent.current_action == "Gather" and agent.energy >= 90:
|
||||
new_action = "Idle"
|
||||
new_location = "center"
|
||||
should_update = True
|
||||
elif agent.current_action == "Gather" and agent.location in ["tree_left", "tree_right"]:
|
||||
# Phase 17-A: Consume fruit when gathering
|
||||
fruit_available = await self._consume_fruit(world, agent.location)
|
||||
if fruit_available:
|
||||
agent.energy = min(100, agent.energy + 30)
|
||||
new_action = "Idle"
|
||||
new_location = "center"
|
||||
should_update = True
|
||||
else:
|
||||
# No fruit! Try other tree or express frustration
|
||||
other_tree = "tree_right" if agent.location == "tree_left" else "tree_left"
|
||||
other_fruit = world.tree_right_fruit if agent.location == "tree_left" else world.tree_left_fruit
|
||||
if other_fruit > 0:
|
||||
new_action = "Gather"
|
||||
new_location = other_tree
|
||||
should_update = True
|
||||
else:
|
||||
# All trees empty!
|
||||
new_action = "Hungry"
|
||||
new_location = "center"
|
||||
should_update = True
|
||||
await self._broadcast_event(EventType.COMMENT, {
|
||||
"user": "System",
|
||||
"message": f"{agent.name} can't find any fruit! The trees are empty..."
|
||||
})
|
||||
elif agent.current_action == "Sleep" and world.time_of_day != "night":
|
||||
new_action = "Wake Up"
|
||||
new_location = "center"
|
||||
should_update = True
|
||||
elif agent.current_action == "Gather Herb":
|
||||
# Simulate herb gathering (add herbs)
|
||||
await self._gather_herb(agent)
|
||||
new_action = "Idle"
|
||||
new_location = "center"
|
||||
should_update = True
|
||||
|
||||
# Execute Update
|
||||
if should_update:
|
||||
agent.current_action = new_action
|
||||
agent.location = new_location
|
||||
|
||||
# Generate simple thought/bark
|
||||
dialogue = self._get_action_bark(agent, new_action, target_name)
|
||||
|
||||
await self._broadcast_event(EventType.AGENT_ACTION, {
|
||||
"agent_id": agent.id,
|
||||
"agent_name": agent.name,
|
||||
"action_type": new_action,
|
||||
"location": new_location,
|
||||
"target_name": target_name,
|
||||
"dialogue": dialogue
|
||||
})
|
||||
|
||||
def _get_action_bark(self, agent: Agent, action: str, target: str = None) -> str:
|
||||
"""Get a simple bark text for an action."""
|
||||
if action == "Sleep":
|
||||
return random.choice(["Yawn... sleepy...", "Time to rest.", "Zzz..."])
|
||||
elif action == "Gather":
|
||||
return random.choice(["Hungry!", "Need food.", "Looking for coconuts..."])
|
||||
elif action == "Gather Herb":
|
||||
return random.choice(["I need herbs...", "Looking for medicine plants.", "Feeling sick..."])
|
||||
elif action == "Craft Medicine":
|
||||
return random.choice(["Let me make some medicine.", "Mixing herbs...", "Almost done!"])
|
||||
elif action == "Use Medicine":
|
||||
return random.choice(["Ahh, much better!", "Medicine tastes awful but works!", "Feeling cured!"])
|
||||
elif action == "Socialize":
|
||||
return f"Looking for {target}..." if target else "Need a friend."
|
||||
elif action == "Wander":
|
||||
return random.choice(["Hmm...", "Nice weather.", "Taking a walk."])
|
||||
elif action == "Wake Up":
|
||||
return "Good morning!"
|
||||
return ""
|
||||
|
||||
# =========================================================================
|
||||
# Inventory & Crafting (Phase 16)
|
||||
# =========================================================================
|
||||
def _get_inventory(self, agent: Agent) -> dict:
|
||||
"""Parse agent inventory JSON."""
|
||||
import json
|
||||
try:
|
||||
return json.loads(agent.inventory) if agent.inventory else {}
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
|
||||
def _set_inventory(self, agent: Agent, inv: dict) -> None:
|
||||
"""Set agent inventory from dict."""
|
||||
import json
|
||||
agent.inventory = json.dumps(inv)
|
||||
|
||||
async def _consume_fruit(self, world: WorldState, location: str) -> bool:
|
||||
"""Consume fruit from a tree. Returns True if successful."""
|
||||
if location == "tree_left":
|
||||
if world.tree_left_fruit > 0:
|
||||
world.tree_left_fruit -= 1
|
||||
logger.info(f"Fruit consumed from tree_left. Remaining: {world.tree_left_fruit}")
|
||||
return True
|
||||
elif location == "tree_right":
|
||||
if world.tree_right_fruit > 0:
|
||||
world.tree_right_fruit -= 1
|
||||
logger.info(f"Fruit consumed from tree_right. Remaining: {world.tree_right_fruit}")
|
||||
return True
|
||||
return False
|
||||
|
||||
async def _gather_herb(self, agent: Agent) -> None:
|
||||
"""Agent gathers herbs."""
|
||||
inv = self._get_inventory(agent)
|
||||
herbs_found = random.randint(1, 2)
|
||||
inv["herb"] = inv.get("herb", 0) + herbs_found
|
||||
self._set_inventory(agent, inv)
|
||||
|
||||
await self._broadcast_event(EventType.AGENT_ACTION, {
|
||||
"agent_id": agent.id,
|
||||
"agent_name": agent.name,
|
||||
"action_type": "Gather Herb",
|
||||
"location": "herb_patch",
|
||||
"dialogue": f"Found {herbs_found} herbs!"
|
||||
})
|
||||
logger.info(f"Agent {agent.name} gathered {herbs_found} herbs. Total: {inv['herb']}")
|
||||
|
||||
async def _craft_medicine(self, agent: Agent) -> None:
|
||||
"""Agent crafts medicine from herbs."""
|
||||
inv = self._get_inventory(agent)
|
||||
if inv.get("herb", 0) >= 3:
|
||||
inv["herb"] -= 3
|
||||
inv["medicine"] = inv.get("medicine", 0) + 1
|
||||
self._set_inventory(agent, inv)
|
||||
|
||||
await self._broadcast_event(EventType.CRAFT, {
|
||||
"agent_id": agent.id,
|
||||
"agent_name": agent.name,
|
||||
"item": "medicine",
|
||||
"ingredients": {"herb": 3}
|
||||
})
|
||||
logger.info(f"Agent {agent.name} crafted medicine. Inventory: {inv}")
|
||||
|
||||
async def _use_medicine(self, agent: Agent) -> None:
|
||||
"""Agent uses medicine to cure sickness."""
|
||||
inv = self._get_inventory(agent)
|
||||
if inv.get("medicine", 0) > 0:
|
||||
inv["medicine"] -= 1
|
||||
self._set_inventory(agent, inv)
|
||||
|
||||
agent.is_sick = False
|
||||
agent.hp = min(100, agent.hp + 20)
|
||||
agent.mood = min(100, agent.mood + 10)
|
||||
|
||||
await self._broadcast_event(EventType.USE_ITEM, {
|
||||
"agent_id": agent.id,
|
||||
"agent_name": agent.name,
|
||||
"item": "medicine",
|
||||
"effect": "cured sickness"
|
||||
})
|
||||
logger.info(f"Agent {agent.name} used medicine and is cured!")
|
||||
|
||||
# =========================================================================
|
||||
# LLM-powered agent speech
|
||||
# =========================================================================
|
||||
@@ -648,12 +1017,19 @@ class GameEngine:
|
||||
|
||||
user.gold -= HEAL_COST
|
||||
old_hp = agent.hp
|
||||
was_sick = agent.is_sick
|
||||
|
||||
agent.hp = min(100, agent.hp + HEAL_HP_RESTORE)
|
||||
agent.is_sick = False # Cure sickness
|
||||
|
||||
msg = f"{username} healed {agent.name}!"
|
||||
if was_sick:
|
||||
msg = f"{username} cured {agent.name}'s sickness!"
|
||||
|
||||
await self._broadcast_event(EventType.HEAL, {
|
||||
"user": username, "agent_name": agent.name,
|
||||
"hp_restored": agent.hp - old_hp, "agent_hp": agent.hp,
|
||||
"user_gold": user.gold, "message": f"{username} healed {agent.name}!"
|
||||
"user_gold": user.gold, "message": msg
|
||||
})
|
||||
await self._broadcast_event(EventType.USER_UPDATE, {"user": username, "gold": user.gold})
|
||||
|
||||
@@ -848,6 +1224,77 @@ class GameEngine:
|
||||
await self._handle_reset(user)
|
||||
return
|
||||
|
||||
# =========================================================================
|
||||
# Random Events (Phase 17-C)
|
||||
# =========================================================================
|
||||
RANDOM_EVENTS = {
|
||||
"storm_damage": {"weight": 30, "description": "A sudden storm damages the island!"},
|
||||
"treasure_found": {"weight": 25, "description": "Someone found a buried treasure!"},
|
||||
"beast_attack": {"weight": 20, "description": "A wild beast attacks the camp!"},
|
||||
"rumor_spread": {"weight": 25, "description": "A rumor starts spreading..."},
|
||||
}
|
||||
|
||||
async def _process_random_events(self) -> None:
|
||||
"""Process random events (10% chance per day at dawn)."""
|
||||
# Only trigger at dawn (once per day)
|
||||
if self._tick_count % 100 != 1: # Roughly once every ~100 ticks
|
||||
return
|
||||
|
||||
if random.random() > 0.10: # 10% chance
|
||||
return
|
||||
|
||||
# Pick random event
|
||||
events = list(self.RANDOM_EVENTS.keys())
|
||||
weights = [self.RANDOM_EVENTS[e]["weight"] for e in events]
|
||||
event_type = random.choices(events, weights=weights)[0]
|
||||
|
||||
with get_db_session() as db:
|
||||
world = db.query(WorldState).first()
|
||||
agents = db.query(Agent).filter(Agent.status == "Alive").all()
|
||||
|
||||
if not agents:
|
||||
return
|
||||
|
||||
event_data = {"event_type": event_type, "message": ""}
|
||||
|
||||
if event_type == "storm_damage":
|
||||
# All agents lose HP and resources depleted
|
||||
for agent in agents:
|
||||
agent.hp = max(0, agent.hp - 15)
|
||||
if world:
|
||||
world.tree_left_fruit = max(0, world.tree_left_fruit - 2)
|
||||
world.tree_right_fruit = max(0, world.tree_right_fruit - 2)
|
||||
event_data["message"] = "A violent storm hits! Everyone is injured and fruit trees are damaged."
|
||||
|
||||
elif event_type == "treasure_found":
|
||||
# Random agent finds treasure (bonus herbs/medicine)
|
||||
lucky = random.choice(agents)
|
||||
inv = self._get_inventory(lucky)
|
||||
inv["medicine"] = inv.get("medicine", 0) + 2
|
||||
inv["herb"] = inv.get("herb", 0) + 3
|
||||
self._set_inventory(lucky, inv)
|
||||
event_data["message"] = f"{lucky.name} found a buried treasure with medicine and herbs!"
|
||||
event_data["agent_name"] = lucky.name
|
||||
|
||||
elif event_type == "beast_attack":
|
||||
# Random agent gets attacked
|
||||
victim = random.choice(agents)
|
||||
victim.hp = max(0, victim.hp - 25)
|
||||
victim.mood = max(0, victim.mood - 20)
|
||||
event_data["message"] = f"A wild beast attacked {victim.name}!"
|
||||
event_data["agent_name"] = victim.name
|
||||
|
||||
elif event_type == "rumor_spread":
|
||||
# Random relationship impact
|
||||
if len(agents) >= 2:
|
||||
a1, a2 = random.sample(list(agents), 2)
|
||||
a1.mood = max(0, a1.mood - 10)
|
||||
a2.mood = max(0, a2.mood - 10)
|
||||
event_data["message"] = f"A rumor about {a1.name} and {a2.name} is spreading..."
|
||||
|
||||
await self._broadcast_event(EventType.RANDOM_EVENT, event_data)
|
||||
logger.info(f"Random event triggered: {event_type}")
|
||||
|
||||
# =========================================================================
|
||||
# Game loop
|
||||
# =========================================================================
|
||||
@@ -888,9 +1335,19 @@ class GameEngine:
|
||||
# 5. Update moods (Phase 3)
|
||||
await self._update_moods()
|
||||
|
||||
# 6. Social interactions (Phase 5)
|
||||
# 6. Autonomous Activity (Phase 13)
|
||||
await self._process_activity_tick()
|
||||
|
||||
# 7. Social interactions (Phase 5)
|
||||
await self._process_social_tick()
|
||||
|
||||
# 8. Random Events (Phase 17-C)
|
||||
await self._process_random_events()
|
||||
|
||||
# 9. Clique Behavior (Phase 17-B)
|
||||
await self._assign_social_roles()
|
||||
await self._process_clique_behavior()
|
||||
|
||||
# 7. Idle chat
|
||||
with get_db_session() as db:
|
||||
alive_count = db.query(Agent).filter(Agent.status == "Alive").count()
|
||||
@@ -948,11 +1405,17 @@ class GameEngine:
|
||||
# Use the existing process_comment method to handle commands
|
||||
await self.process_comment(user, text)
|
||||
|
||||
async def process_bits(self, user: str, amount: int) -> None:
|
||||
"""Process Twitch bits and convert to game gold."""
|
||||
# 1 Bit = 1 Gold conversion rate
|
||||
gold_added = amount
|
||||
async def handle_gift(self, user: str, amount: int, gift_type: str = "bits") -> None:
|
||||
"""
|
||||
Handle a gift/donation (bits, subscription, or test).
|
||||
|
||||
Args:
|
||||
user: Name of the donor
|
||||
amount: Value of the gift
|
||||
gift_type: Type of gift (bits, test, etc.)
|
||||
"""
|
||||
# 1. Add gold to user
|
||||
gold_added = amount
|
||||
with get_db_session() as db:
|
||||
user_obj = self._get_or_create_user(db, user)
|
||||
user_obj.gold += gold_added
|
||||
@@ -960,14 +1423,49 @@ class GameEngine:
|
||||
await self._broadcast_event(EventType.USER_UPDATE, {
|
||||
"user": user,
|
||||
"gold": user_obj.gold,
|
||||
"message": f"{user} received {gold_added} gold from {amount} bits!"
|
||||
"message": f"{user} received {gold_added} gold!"
|
||||
})
|
||||
|
||||
# Also broadcast a special bits event for UI effects
|
||||
await self._broadcast_event("bits_received", {
|
||||
"user": user,
|
||||
"bits": amount,
|
||||
"gold": gold_added
|
||||
})
|
||||
|
||||
logger.info(f"Processed bits: {user} -> {amount} bits -> {gold_added} gold")
|
||||
|
||||
# Check for alive agents for reaction
|
||||
alive_agents = db.query(Agent).filter(Agent.status == "Alive").all()
|
||||
agent = random.choice(alive_agents) if alive_agents else None
|
||||
# Extract data immediately to avoid DetachedInstanceError after session closes
|
||||
agent_name = agent.name if agent else "Survivor"
|
||||
agent_personality = agent.personality if agent else "friendly"
|
||||
|
||||
# 2. Generate AI gratitude
|
||||
gratitude = await llm_service.generate_gratitude(
|
||||
user=user,
|
||||
amount=amount,
|
||||
agent_name=agent_name,
|
||||
agent_personality=agent_personality,
|
||||
gift_name=gift_type
|
||||
)
|
||||
|
||||
# 3. Store Memory (Phase 14)
|
||||
if agent:
|
||||
memory_text = f"User {user} gave me {amount} {gift_type}. I felt grateful."
|
||||
await memory_service.add_memory(
|
||||
agent_id=agent.id,
|
||||
description=memory_text,
|
||||
importance=random.randint(6, 9), # Gifts are important
|
||||
related_entity_name=user,
|
||||
memory_type="gift"
|
||||
)
|
||||
|
||||
# 4. Broadcast gift effect to Unity
|
||||
await self._broadcast_event("gift_effect", {
|
||||
"user": user,
|
||||
"gift_type": gift_type,
|
||||
"value": amount,
|
||||
"message": f"{user} sent {amount} {gift_type}!",
|
||||
"agent_name": agent_name if agent else None,
|
||||
"gratitude": gratitude,
|
||||
"duration": 8.0
|
||||
})
|
||||
|
||||
logger.info(f"Processed gift: {user} -> {amount} {gift_type} (Gratitude: {gratitude})")
|
||||
|
||||
async def process_bits(self, user: str, amount: int) -> None:
|
||||
"""Deprecated: Use handle_gift instead."""
|
||||
await self.handle_gift(user, amount, "bits")
|
||||
|
||||
@@ -27,6 +27,9 @@ from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .models import Agent
|
||||
from .memory_service import MemoryService
|
||||
|
||||
from .memory_service import memory_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -56,6 +59,24 @@ MOCK_REACTIONS = {
|
||||
"My stomach is eating itself...",
|
||||
"Is this how it ends? Starving on a beach?",
|
||||
],
|
||||
"gratitude_arrogant": [
|
||||
"Finally! A worthy tribute! {user}, you understand greatness!",
|
||||
"About time someone recognized my value! Thanks, {user}!",
|
||||
"Hmph, {user} knows quality when they see it. Much appreciated!",
|
||||
"A gift for ME? Well, obviously. Thank you, {user}!",
|
||||
],
|
||||
"gratitude_humble": [
|
||||
"Oh my gosh, {user}! You're too kind! Thank you so much!",
|
||||
"Wow, {user}, I don't deserve this! You're amazing!",
|
||||
"*tears up* {user}... this means the world to me!",
|
||||
"Thank you, thank you {user}! You're the best!",
|
||||
],
|
||||
"gratitude_neutral": [
|
||||
"Hey, thanks {user}! That's really generous of you!",
|
||||
"Wow, {user}! Thank you so much for the support!",
|
||||
"Appreciate it, {user}! You're awesome!",
|
||||
"{user}, you're a legend! Thank you!",
|
||||
],
|
||||
}
|
||||
|
||||
# Default model configuration
|
||||
@@ -182,11 +203,16 @@ class LLMService:
|
||||
return self._get_mock_response(event_type)
|
||||
|
||||
try:
|
||||
# Retrieve relevant memories
|
||||
memories = await memory_service.get_relevant_memories(agent.id, event_description)
|
||||
memory_context = "\n".join(memories) if memories else "No relevant memories."
|
||||
|
||||
system_prompt = (
|
||||
f"You are {agent.name}. "
|
||||
f"Personality: {agent.personality}. "
|
||||
f"Current Status: HP={agent.hp}, Energy={agent.energy}. "
|
||||
f"You live on a survival island. "
|
||||
f"Relevant Memories:\n{memory_context}\n"
|
||||
f"React to the following event briefly (under 20 words). "
|
||||
f"Respond in first person, as if speaking out loud."
|
||||
)
|
||||
@@ -329,10 +355,15 @@ class LLMService:
|
||||
"calm and neutral" if agent_mood >= 40 else \
|
||||
"a bit down" if agent_mood >= 20 else "anxious and worried"
|
||||
|
||||
# Retrieve relevant memories
|
||||
memories = await memory_service.get_relevant_memories(agent.id, topic)
|
||||
memory_context = "\n".join(memories) if memories else "No relevant memories."
|
||||
|
||||
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"Relevant Memories:\n{memory_context}\n"
|
||||
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."
|
||||
@@ -461,6 +492,92 @@ class LLMService:
|
||||
logger.error(f"LLM API error for social interaction: {e}")
|
||||
return f"{initiator_name}: ...\n{target_name}: ..."
|
||||
|
||||
async def generate_gratitude(
|
||||
self,
|
||||
user: str,
|
||||
amount: int,
|
||||
agent_name: str = "Survivor",
|
||||
agent_personality: str = "friendly",
|
||||
gift_name: str = "bits"
|
||||
) -> str:
|
||||
"""
|
||||
Generate a special gratitude response for donations/gifts.
|
||||
|
||||
Args:
|
||||
user: Name of the user who gave the gift
|
||||
amount: Amount of the gift
|
||||
agent_name: Name of the agent (optional)
|
||||
agent_personality: Personality of the agent (optional)
|
||||
gift_name: Type of gift (bits, subscription, etc.)
|
||||
|
||||
Returns:
|
||||
An excited, grateful response from the agent
|
||||
"""
|
||||
personality = agent_personality.lower() if agent_personality else "friendly"
|
||||
|
||||
|
||||
if self._mock_mode:
|
||||
if "arrogant" in personality or "proud" in personality:
|
||||
responses = MOCK_REACTIONS.get("gratitude_arrogant", [])
|
||||
elif "humble" in personality or "shy" in personality or "kind" in personality:
|
||||
responses = MOCK_REACTIONS.get("gratitude_humble", [])
|
||||
else:
|
||||
responses = MOCK_REACTIONS.get("gratitude_neutral", [])
|
||||
|
||||
if responses:
|
||||
return random.choice(responses).format(user=user, amount=amount)
|
||||
return f"Thank you so much, {user}! You're amazing!"
|
||||
|
||||
try:
|
||||
# Customize tone based on personality
|
||||
if "arrogant" in personality or "proud" in personality:
|
||||
tone_instruction = (
|
||||
"You are somewhat arrogant but still grateful. "
|
||||
"React with confident excitement, like 'Finally, a worthy tribute!' "
|
||||
"but still thank them."
|
||||
)
|
||||
elif "humble" in personality or "shy" in personality:
|
||||
tone_instruction = (
|
||||
"You are humble and easily moved. "
|
||||
"React with overwhelming gratitude, maybe even get teary-eyed."
|
||||
)
|
||||
else:
|
||||
tone_instruction = (
|
||||
"React with genuine excitement and gratitude."
|
||||
)
|
||||
|
||||
system_prompt = (
|
||||
f"You are {agent_name}, a survivor on a deserted island. "
|
||||
f"Personality: {personality if personality else 'friendly'}. "
|
||||
f"A wealthy patron named {user} just gave you {amount} {gift_name}! "
|
||||
f"{tone_instruction} "
|
||||
f"Respond with extreme excitement and gratitude (max 15 words). "
|
||||
f"Keep it fun and energetic!"
|
||||
)
|
||||
|
||||
kwargs = {
|
||||
"model": self._model,
|
||||
"messages": [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": f"{user} just gave you {amount} {gift_name}! React!"}
|
||||
],
|
||||
"max_tokens": 40,
|
||||
"temperature": 0.95,
|
||||
}
|
||||
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 gratitude: {e}")
|
||||
return f"Wow, thank you so much {user}! You're amazing!"
|
||||
|
||||
|
||||
# Global instance for easy import
|
||||
llm_service = LLMService()
|
||||
|
||||
@@ -140,6 +140,13 @@ async def websocket_endpoint(websocket: WebSocket):
|
||||
text = message.payload.get("message", "")
|
||||
await engine.process_comment(user, text)
|
||||
|
||||
# Handle test gift action
|
||||
elif message.action == "test_gift":
|
||||
user = message.payload.get("user", "TestUser")
|
||||
bits = int(message.payload.get("bits", 100))
|
||||
# Trigger handle_gift in engine
|
||||
await engine.handle_gift(user, bits, "bits")
|
||||
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(websocket)
|
||||
except Exception as e:
|
||||
|
||||
74
backend/app/memory_service.py
Normal file
74
backend/app/memory_service.py
Normal file
@@ -0,0 +1,74 @@
|
||||
|
||||
import logging
|
||||
import random
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from .database import get_db_session
|
||||
from .models import Agent, AgentMemory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MemoryService:
|
||||
"""
|
||||
Manages long-term memories for agents.
|
||||
Responsible for:
|
||||
1. Storing new memories.
|
||||
2. Retrieving relevant memories for context.
|
||||
3. Pruning/Summarizing old memories (future).
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
async def add_memory(self, agent_id: int, description: str, importance: int = 1,
|
||||
related_entity_id: int = None, related_entity_name: str = None,
|
||||
memory_type: str = "general") -> AgentMemory:
|
||||
"""
|
||||
Record a new memory for an agent.
|
||||
"""
|
||||
with get_db_session() as db:
|
||||
memory = AgentMemory(
|
||||
agent_id=agent_id,
|
||||
description=description,
|
||||
importance=importance,
|
||||
related_entity_id=related_entity_id,
|
||||
related_entity_name=related_entity_name,
|
||||
memory_type=memory_type
|
||||
)
|
||||
db.add(memory)
|
||||
db.commit() # Ensure ID is generated
|
||||
db.refresh(memory)
|
||||
logger.info(f"Agent {agent_id} remembered: {description} (Imp: {importance})")
|
||||
return memory
|
||||
|
||||
async def get_relevant_memories(self, agent_id: int, context: str, limit: int = 3) -> List[str]:
|
||||
"""
|
||||
Retrieve memories relevant to the current context.
|
||||
For MVP, we just return the most recent high-importance memories
|
||||
and any memories related to the entities in context.
|
||||
"""
|
||||
memories = []
|
||||
with get_db_session() as db:
|
||||
# 1. Get recent important memories (Short-term / working memory)
|
||||
recent_memories = db.query(AgentMemory).filter(
|
||||
AgentMemory.agent_id == agent_id,
|
||||
AgentMemory.importance >= 5
|
||||
).order_by(AgentMemory.created_at.desc()).limit(limit).all()
|
||||
|
||||
# 2. Get entity-specific memories (e.g. if talking to "User1")
|
||||
# Simple keyword matching for now (Vector DB is Phase 14+)
|
||||
entity_memories = []
|
||||
if context:
|
||||
# Naive search for names in context
|
||||
# In real prod, use embeddings.
|
||||
search_term = f"%{context}%" # Very naive
|
||||
# Let's just fallback to recent for MVP to ensure stability
|
||||
|
||||
for mem in recent_memories:
|
||||
memories.append(f"- {mem.description}")
|
||||
|
||||
return memories
|
||||
|
||||
# Global instance
|
||||
memory_service = MemoryService()
|
||||
@@ -51,6 +51,18 @@ class Agent(Base):
|
||||
# Social attributes (Phase 5)
|
||||
social_tendency = Column(String(20), default="neutral") # introvert, extrovert, neutral
|
||||
|
||||
# Autonomous Action (Phase 13)
|
||||
current_action = Column(String(50), default="Idle")
|
||||
location = Column(String(50), default="center") # logical location: tree_left, tree_right, center, etc.
|
||||
target_agent_id = Column(Integer, nullable=True) # if action targets another agent
|
||||
|
||||
# Survival (Phase 15)
|
||||
is_sick = Column(Boolean, default=False)
|
||||
immunity = Column(Integer, default=50) # 0-100, higher = less chance to get sick
|
||||
|
||||
# Relationship 2.0 (Phase 17-B)
|
||||
social_role = Column(String(20), default="neutral") # leader, follower, loner, neutral
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Agent {self.name} ({self.personality}) HP={self.hp} Energy={self.energy} Mood={self.mood}>"
|
||||
|
||||
@@ -71,7 +83,12 @@ class Agent(Base):
|
||||
"inventory": self.inventory,
|
||||
"mood": self.mood,
|
||||
"mood_state": self.mood_state,
|
||||
"social_tendency": self.social_tendency
|
||||
"social_tendency": self.social_tendency,
|
||||
"current_action": self.current_action,
|
||||
"location": self.location,
|
||||
"is_sick": self.is_sick,
|
||||
"immunity": self.immunity,
|
||||
"social_role": self.social_role
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +111,10 @@ class WorldState(Base):
|
||||
# Weather system (Phase 3)
|
||||
weather_duration = Column(Integer, default=0) # Ticks since last weather change
|
||||
|
||||
# Resource Scarcity (Phase 17-A)
|
||||
tree_left_fruit = Column(Integer, default=5) # Max 5 fruit
|
||||
tree_right_fruit = Column(Integer, default=5) # Max 5 fruit
|
||||
|
||||
def __repr__(self):
|
||||
return f"<WorldState Day={self.day_count} {self.time_of_day} Weather={self.weather}>"
|
||||
|
||||
@@ -104,7 +125,9 @@ class WorldState(Base):
|
||||
"weather": self.weather,
|
||||
"resource_level": self.resource_level,
|
||||
"current_tick_in_day": self.current_tick_in_day,
|
||||
"time_of_day": self.time_of_day
|
||||
"time_of_day": self.time_of_day,
|
||||
"tree_left_fruit": self.tree_left_fruit,
|
||||
"tree_right_fruit": self.tree_right_fruit
|
||||
}
|
||||
|
||||
|
||||
@@ -206,3 +229,39 @@ class AgentRelationship(Base):
|
||||
self.relationship_type = "friend"
|
||||
else:
|
||||
self.relationship_type = "close_friend"
|
||||
|
||||
|
||||
class AgentMemory(Base):
|
||||
"""
|
||||
Long-term memory for agents.
|
||||
Stores significant events, conversations, and interactions.
|
||||
"""
|
||||
__tablename__ = "agent_memories"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
agent_id = Column(Integer, ForeignKey("agents.id"), nullable=False)
|
||||
|
||||
# The memory content
|
||||
description = Column(String(500), nullable=False)
|
||||
|
||||
# Metadata for retrieval
|
||||
importance = Column(Integer, default=1) # 1-10
|
||||
related_entity_id = Column(Integer, nullable=True) # ID of user or other agent involved
|
||||
related_entity_name = Column(String(50), nullable=True)
|
||||
memory_type = Column(String(20), default="general") # chat, gift, event, social
|
||||
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
tick_created = Column(Integer, nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Memory {self.agent_id}: {self.description[:20]}... ({self.importance})>"
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"agent_id": self.agent_id,
|
||||
"description": self.description,
|
||||
"importance": self.importance,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,16 @@ class EventType(str, Enum):
|
||||
RELATIONSHIP_CHANGE = "relationship_change" # Relationship status changed
|
||||
AUTO_REVIVE = "auto_revive" # Agent auto-revived (casual mode)
|
||||
|
||||
# Autonomous Agency (Phase 13)
|
||||
AGENT_ACTION = "agent_action" # Agent performs an action (move, gather, etc.)
|
||||
|
||||
# Crafting System (Phase 16)
|
||||
CRAFT = "craft" # Agent crafted an item
|
||||
USE_ITEM = "use_item" # Agent used an item
|
||||
|
||||
# Random Events (Phase 17-C)
|
||||
RANDOM_EVENT = "random_event" # Random event occurred
|
||||
|
||||
|
||||
class GameEvent(BaseModel):
|
||||
"""
|
||||
|
||||
@@ -7,6 +7,7 @@ Compatible with twitchio 2.x (IRC-based)
|
||||
|
||||
import os
|
||||
import logging
|
||||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from twitchio.ext import commands
|
||||
@@ -83,20 +84,19 @@ class TwitchBot(commands.Bot):
|
||||
if hasattr(message, 'tags') and message.tags:
|
||||
bits = message.tags.get('bits')
|
||||
if bits:
|
||||
try:
|
||||
bits_amount = int(bits)
|
||||
logger.info(f"Received {bits_amount} bits from {username}")
|
||||
await self._game_engine.process_bits(username, bits_amount)
|
||||
await self._handle_bits(username, int(bits))
|
||||
|
||||
# Send special gift effect to Unity
|
||||
await self._game_engine._broadcast_event("gift_effect", {
|
||||
"type": "gift_effect",
|
||||
"user": username,
|
||||
"value": bits_amount,
|
||||
"message": f"{username} cheered {bits_amount} bits!"
|
||||
})
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.error(f"Error parsing bits amount: {e}")
|
||||
async def _handle_bits(self, username: str, bits_amount: int):
|
||||
"""
|
||||
Handle bits donation.
|
||||
Delegates to game engine's unified gift handling.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Received {bits_amount} bits from {username}")
|
||||
await self._game_engine.handle_gift(username, bits_amount, "bits")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling bits: {e}")
|
||||
|
||||
async def event_command_error(self, context, error):
|
||||
"""Called when a command error occurs."""
|
||||
@@ -110,3 +110,4 @@ class TwitchBot(commands.Bot):
|
||||
logger.error(f"Twitch bot error: {error}")
|
||||
if data:
|
||||
logger.debug(f"Error data: {data}")
|
||||
|
||||
|
||||
74
unity-client/Assets/Editor/SetupVisuals.cs
Normal file
74
unity-client/Assets/Editor/SetupVisuals.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using UnityEngine.Rendering.PostProcessing;
|
||||
|
||||
public class SetupVisuals
|
||||
{
|
||||
[MenuItem("TheIsland/Setup PostProcessing")]
|
||||
public static void Setup()
|
||||
{
|
||||
// 1. Setup Camera
|
||||
var camera = Camera.main;
|
||||
if (camera == null) { Debug.LogError("No Main Camera!"); return; }
|
||||
|
||||
var layer = camera.gameObject.GetComponent<PostProcessLayer>();
|
||||
if (layer == null) layer = camera.gameObject.AddComponent<PostProcessLayer>();
|
||||
|
||||
// Use Default layer
|
||||
layer.volumeLayer = LayerMask.GetMask("Default");
|
||||
// Simple AA
|
||||
layer.antialiasingMode = PostProcessLayer.Antialiasing.SubpixelMorphologicalAntialiasing;
|
||||
|
||||
// 2. Create Global Volume
|
||||
var volumeGo = GameObject.Find("GlobalVolume");
|
||||
if (volumeGo == null)
|
||||
{
|
||||
volumeGo = new GameObject("GlobalVolume");
|
||||
volumeGo.layer = LayerMask.NameToLayer("Default");
|
||||
}
|
||||
|
||||
var volume = volumeGo.GetComponent<PostProcessVolume>();
|
||||
if (volume == null) volume = volumeGo.AddComponent<PostProcessVolume>();
|
||||
|
||||
volume.isGlobal = true;
|
||||
|
||||
// 3. Create Profile if not exists
|
||||
var profilePath = "Assets/GlobalProfile.asset";
|
||||
var profile = AssetDatabase.LoadAssetAtPath<PostProcessProfile>(profilePath);
|
||||
if (profile == null)
|
||||
{
|
||||
profile = ScriptableObject.CreateInstance<PostProcessProfile>();
|
||||
AssetDatabase.CreateAsset(profile, profilePath);
|
||||
}
|
||||
volume.profile = profile;
|
||||
|
||||
// 4. Clean existing settings
|
||||
profile.settings.Clear();
|
||||
|
||||
// 5. Add Effects
|
||||
// Bloom - Glow effect
|
||||
var bloom = profile.AddSettings<Bloom>();
|
||||
bloom.enabled.value = true;
|
||||
bloom.intensity.value = 3.0f;
|
||||
bloom.threshold.value = 1.0f;
|
||||
bloom.softKnee.value = 0.5f;
|
||||
|
||||
// Color Grading - Better colors
|
||||
var colorGrading = profile.AddSettings<ColorGrading>();
|
||||
colorGrading.enabled.value = true;
|
||||
colorGrading.tonemapper.value = Tonemapper.ACES;
|
||||
colorGrading.postExposure.value = 0.5f; // Slightly brighter
|
||||
colorGrading.saturation.value = 20f; // More vibrant
|
||||
colorGrading.contrast.value = 15f; // More pop
|
||||
|
||||
// Vignette - Focus center
|
||||
var vignette = profile.AddSettings<Vignette>();
|
||||
vignette.enabled.value = true;
|
||||
vignette.intensity.value = 0.35f;
|
||||
vignette.smoothness.value = 0.4f;
|
||||
|
||||
EditorUtility.SetDirty(profile);
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log("Visuals Setup Complete! Bloom, ColorGrading, and Vignette configured.");
|
||||
}
|
||||
}
|
||||
2
unity-client/Assets/Editor/SetupVisuals.cs.meta
Normal file
2
unity-client/Assets/Editor/SetupVisuals.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f42deb3551fc40dfbe902fe1945f25b
|
||||
File diff suppressed because one or more lines are too long
18
unity-client/Assets/GlobalProfile.asset
Normal file
18
unity-client/Assets/GlobalProfile.asset
Normal file
@@ -0,0 +1,18 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 8e6292b2c06870d4495f009f912b9600, type: 3}
|
||||
m_Name: GlobalProfile
|
||||
m_EditorClassIdentifier: Unity.Postprocessing.Runtime::UnityEngine.Rendering.PostProcessing.PostProcessProfile
|
||||
settings:
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
- {fileID: 0}
|
||||
8
unity-client/Assets/GlobalProfile.asset.meta
Normal file
8
unity-client/Assets/GlobalProfile.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a9d5cd213c7f4bebb196509d67c68e3
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -151,13 +151,47 @@ namespace TheIsland.UI
|
||||
rect.sizeDelta = size;
|
||||
rect.anchoredPosition = Vector2.zero;
|
||||
|
||||
// Semi-transparent background
|
||||
// Semi-transparent glass background
|
||||
var bg = panel.AddComponent<Image>();
|
||||
bg.color = new Color(0, 0, 0, 0.5f);
|
||||
bg.color = new Color(0.1f, 0.12f, 0.18f, 0.6f);
|
||||
|
||||
// Add border highlight
|
||||
var border = new GameObject("Border");
|
||||
border.transform.SetParent(panel.transform);
|
||||
var bRect = border.AddComponent<RectTransform>();
|
||||
bRect.anchorMin = Vector2.zero;
|
||||
bRect.anchorMax = Vector2.one;
|
||||
bRect.offsetMin = new Vector2(-1, -1);
|
||||
bRect.offsetMax = new Vector2(1, 1);
|
||||
var bImg = border.AddComponent<Image>();
|
||||
bImg.color = new Color(1f, 1f, 1f, 0.1f);
|
||||
border.transform.SetAsFirstSibling();
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private float _currentHp;
|
||||
private float _currentEnergy;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_hpBarFill != null && _currentData != null)
|
||||
{
|
||||
_currentHp = Mathf.Lerp(_currentHp, _currentData.hp, Time.deltaTime * 5f);
|
||||
float hpPercent = _currentHp / 100f;
|
||||
_hpBarFill.rectTransform.anchorMax = new Vector2(hpPercent, 1);
|
||||
_hpBarFill.color = Color.Lerp(hpLowColor, hpHighColor, hpPercent);
|
||||
}
|
||||
|
||||
if (_energyBarFill != null && _currentData != null)
|
||||
{
|
||||
_currentEnergy = Mathf.Lerp(_currentEnergy, _currentData.energy, Time.deltaTime * 5f);
|
||||
float energyPercent = _currentEnergy / 100f;
|
||||
_energyBarFill.rectTransform.anchorMax = new Vector2(energyPercent, 1);
|
||||
_energyBarFill.color = Color.Lerp(energyLowColor, energyHighColor, energyPercent);
|
||||
}
|
||||
}
|
||||
|
||||
private TextMeshProUGUI CreateText(GameObject parent, string name, string text,
|
||||
float fontSize, Color color, FontStyles style = FontStyles.Normal)
|
||||
{
|
||||
@@ -296,25 +330,11 @@ namespace TheIsland.UI
|
||||
{
|
||||
_currentData = data;
|
||||
|
||||
// Update HP
|
||||
float hpPercent = data.hp / 100f;
|
||||
if (_hpBarFill != null)
|
||||
{
|
||||
_hpBarFill.rectTransform.anchorMax = new Vector2(hpPercent, 1);
|
||||
_hpBarFill.color = Color.Lerp(hpLowColor, hpHighColor, hpPercent);
|
||||
}
|
||||
if (_hpText != null)
|
||||
{
|
||||
_hpText.text = $"HP: {data.hp}";
|
||||
}
|
||||
|
||||
// Update Energy
|
||||
float energyPercent = data.energy / 100f;
|
||||
if (_energyBarFill != null)
|
||||
{
|
||||
_energyBarFill.rectTransform.anchorMax = new Vector2(energyPercent, 1);
|
||||
_energyBarFill.color = Color.Lerp(energyLowColor, energyHighColor, energyPercent);
|
||||
}
|
||||
if (_energyText != null)
|
||||
{
|
||||
_energyText.text = $"Energy: {data.energy}";
|
||||
|
||||
@@ -64,18 +64,33 @@ namespace TheIsland.Visual
|
||||
private Billboard _spriteBillboard;
|
||||
private Billboard _uiBillboard;
|
||||
private Camera _mainCamera;
|
||||
private AgentAnimator _animator;
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
private int _agentId;
|
||||
private AgentData _currentData;
|
||||
private string _moodState = "neutral";
|
||||
private Coroutine _speechCoroutine;
|
||||
|
||||
// Animation state
|
||||
private float _idleAnimTimer;
|
||||
private float _breathScale = 1f;
|
||||
private Vector3 _originalSpriteScale;
|
||||
private float _bobOffset;
|
||||
|
||||
// Movement state
|
||||
private Vector3 _targetPosition;
|
||||
private bool _isMoving;
|
||||
private float _moveSpeed = 3f;
|
||||
private Vector3 _lastPosition;
|
||||
|
||||
// UI Smoothing (Phase 19)
|
||||
private float _currentHpPercent;
|
||||
private float _currentEnergyPercent;
|
||||
private float _currentMoodPercent;
|
||||
private float _targetHpPercent;
|
||||
private float _targetEnergyPercent;
|
||||
private float _targetMoodPercent;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
@@ -88,33 +103,202 @@ namespace TheIsland.Visual
|
||||
private void Awake()
|
||||
{
|
||||
_mainCamera = Camera.main;
|
||||
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||
|
||||
// Phase 19-B: Ensure AgentAnimator is present
|
||||
_animator = GetComponent<AgentAnimator>();
|
||||
if (_animator == null) _animator = gameObject.AddComponent<AgentAnimator>();
|
||||
|
||||
CreateVisuals();
|
||||
_lastPosition = transform.position;
|
||||
}
|
||||
|
||||
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)
|
||||
// Phase 19-D: Apply soft-repulsion to prevent crowding
|
||||
Vector3 repulsion = CalculateRepulsion();
|
||||
|
||||
// Handle Movement
|
||||
if (_isMoving)
|
||||
{
|
||||
// Apply breathing scale
|
||||
_spriteRenderer.transform.localScale = new Vector3(
|
||||
_originalSpriteScale.x * _breathScale,
|
||||
_originalSpriteScale.y * _breathScale,
|
||||
_originalSpriteScale.z
|
||||
);
|
||||
// Simple steering toward target
|
||||
Vector3 moveDir = (_targetPosition - transform.position).normalized;
|
||||
Vector3 finalVelocity = (moveDir * _moveSpeed) + repulsion;
|
||||
|
||||
transform.position += finalVelocity * Time.deltaTime;
|
||||
|
||||
// Apply bobbing
|
||||
// Flip sprite based on direction
|
||||
if (_spriteRenderer != null && Mathf.Abs(moveDir.x) > 0.01f)
|
||||
{
|
||||
_spriteRenderer.flipX = moveDir.x < 0;
|
||||
}
|
||||
|
||||
if (Vector3.Distance(transform.position, _targetPosition) < 0.1f)
|
||||
{
|
||||
_isMoving = false;
|
||||
}
|
||||
}
|
||||
else if (repulsion.sqrMagnitude > 0.001f)
|
||||
{
|
||||
// Push away even when idle
|
||||
transform.position += repulsion * Time.deltaTime;
|
||||
}
|
||||
|
||||
// Phase 19-D: Dynamic Z-Sorting
|
||||
if (_spriteRenderer != null)
|
||||
{
|
||||
// In world space, higher Z (further) should have lower sorting order
|
||||
// Z typically ranges from -10 to 10 on the island
|
||||
_spriteRenderer.sortingOrder = Mathf.RoundToInt(-transform.position.z * 100);
|
||||
}
|
||||
|
||||
// Phase 19-B/D/E: Use AgentAnimator
|
||||
if (_animator != null)
|
||||
{
|
||||
// Calculate world velocity based on position change
|
||||
Vector3 currentVelocity = (transform.position - _lastPosition) / (Time.deltaTime > 0 ? Time.deltaTime : 0.001f);
|
||||
_animator.SetMovement(currentVelocity, _moveSpeed);
|
||||
_lastPosition = transform.position;
|
||||
}
|
||||
|
||||
// Phase 19-E: Social Orientation (Interaction Facing)
|
||||
if (!_isMoving)
|
||||
{
|
||||
FaceInteractionTarget();
|
||||
}
|
||||
|
||||
// Phase 19: Smooth UI Bar Transitions
|
||||
UpdateSmoothBars();
|
||||
}
|
||||
|
||||
private void FaceInteractionTarget()
|
||||
{
|
||||
// If the agent is talking or near others, turn to face them
|
||||
float socialRange = 2.5f;
|
||||
AgentVisual nearestAgent = null;
|
||||
float minDist = socialRange;
|
||||
|
||||
var allAgents = FindObjectsByType<AgentVisual>(FindObjectsSortMode.None);
|
||||
foreach (var other in allAgents)
|
||||
{
|
||||
if (other == this || !other.IsAlive) continue;
|
||||
float d = Vector3.Distance(transform.position, other.transform.position);
|
||||
if (d < minDist)
|
||||
{
|
||||
minDist = d;
|
||||
nearestAgent = other;
|
||||
}
|
||||
}
|
||||
|
||||
if (nearestAgent != null && _spriteRenderer != null)
|
||||
{
|
||||
float dx = nearestAgent.transform.position.x - transform.position.x;
|
||||
if (Mathf.Abs(dx) > 0.1f)
|
||||
{
|
||||
_spriteRenderer.flipX = dx < 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3 CalculateRepulsion()
|
||||
{
|
||||
Vector3 force = Vector3.zero;
|
||||
float radius = 1.2f; // Social distancing radius
|
||||
float strength = 1.5f;
|
||||
|
||||
var allAgents = FindObjectsByType<AgentVisual>(FindObjectsSortMode.None);
|
||||
foreach (var other in allAgents)
|
||||
{
|
||||
if (other == this || !other.IsAlive) continue;
|
||||
|
||||
Vector3 diff = transform.position - other.transform.position;
|
||||
float dist = diff.magnitude;
|
||||
|
||||
if (dist < radius && dist > 0.01f)
|
||||
{
|
||||
// Linear falloff repulsion
|
||||
force += diff.normalized * (1.0f - (dist / radius)) * strength;
|
||||
}
|
||||
}
|
||||
return force;
|
||||
}
|
||||
|
||||
private void UpdateSmoothBars()
|
||||
{
|
||||
float lerpSpeed = 5f * Time.deltaTime;
|
||||
|
||||
if (_hpBarFill != null)
|
||||
{
|
||||
_currentHpPercent = Mathf.Lerp(_currentHpPercent, _targetHpPercent, lerpSpeed);
|
||||
_hpBarFill.rectTransform.anchorMax = new Vector2(_currentHpPercent, 1);
|
||||
_hpBarFill.color = Color.Lerp(hpLowColor, hpHighColor, _currentHpPercent);
|
||||
}
|
||||
|
||||
if (_energyBarFill != null)
|
||||
{
|
||||
_currentEnergyPercent = Mathf.Lerp(_currentEnergyPercent, _targetEnergyPercent, lerpSpeed);
|
||||
_energyBarFill.rectTransform.anchorMax = new Vector2(_currentEnergyPercent, 1);
|
||||
_energyBarFill.color = Color.Lerp(energyLowColor, energyHighColor, _currentEnergyPercent);
|
||||
}
|
||||
|
||||
if (_moodBarFill != null)
|
||||
{
|
||||
_currentMoodPercent = Mathf.Lerp(_currentMoodPercent, _targetMoodPercent, lerpSpeed);
|
||||
_moodBarFill.rectTransform.anchorMax = new Vector2(_currentMoodPercent, 1);
|
||||
_moodBarFill.color = GetMoodColor(_currentData?.mood_state ?? "neutral");
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger a jump animation (to be called by events)
|
||||
public void DoJump()
|
||||
{
|
||||
StartCoroutine(JumpRoutine());
|
||||
}
|
||||
|
||||
public void MoveTo(Vector3 target)
|
||||
{
|
||||
_targetPosition = target;
|
||||
// Keep current Y (height) to avoid sinking/flying, unless target specifies it
|
||||
// Actually our agents are on navmesh or free moving? Free moving for now.
|
||||
// But we want to keep them on the "ground" plane roughly.
|
||||
// Let's preserve current Y if target Y is 0 (which usually means undefined in 2D topdown logic, but here we are 2.5D)
|
||||
// The spawn positions have Y=0.
|
||||
_targetPosition.y = transform.position.y;
|
||||
_isMoving = true;
|
||||
}
|
||||
|
||||
private void MoveTowardsTarget()
|
||||
{
|
||||
Vector3 direction = (_targetPosition - transform.position).normalized;
|
||||
transform.position = Vector3.MoveTowards(transform.position, _targetPosition, _moveSpeed * Time.deltaTime);
|
||||
|
||||
// Stop when close enough
|
||||
if (Vector3.Distance(transform.position, _targetPosition) < 0.1f)
|
||||
{
|
||||
_isMoving = false;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator JumpRoutine()
|
||||
{
|
||||
float timer = 0;
|
||||
float duration = 0.4f;
|
||||
Vector3 startPos = _spriteRenderer.transform.localPosition;
|
||||
|
||||
while (timer < duration)
|
||||
{
|
||||
timer += Time.deltaTime;
|
||||
float t = timer / duration;
|
||||
|
||||
// Parabolic jump height
|
||||
float height = Mathf.Sin(t * Mathf.PI) * 0.5f;
|
||||
|
||||
var pos = _spriteRenderer.transform.localPosition;
|
||||
pos.y = 1f + _bobOffset;
|
||||
pos.y = startPos.y + height;
|
||||
_spriteRenderer.transform.localPosition = pos;
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +322,10 @@ namespace TheIsland.Visual
|
||||
_currentData = data;
|
||||
gameObject.name = $"Agent_{data.id}_{data.name}";
|
||||
|
||||
// Apply unique color based on agent ID
|
||||
// Loading premium assets (Phase 19)
|
||||
TryLoadPremiumSprite(data.id);
|
||||
|
||||
// Apply unique color based on agent ID (as fallback/tint)
|
||||
ApplyAgentColor(data.id);
|
||||
|
||||
// Set UI text
|
||||
@@ -149,6 +336,85 @@ namespace TheIsland.Visual
|
||||
Debug.Log($"[AgentVisual] Initialized: {data.name}");
|
||||
}
|
||||
|
||||
private void TryLoadPremiumSprite(int id)
|
||||
{
|
||||
// Load the collection texture from Assets
|
||||
// Note: In a real build, we'd use Resources.Load or Addressables.
|
||||
// For this environment, we'll try to find it in the path or use a static reference.
|
||||
// Since we can't easily use Resources.Load at runtime for arbitrary paths,
|
||||
// we'll implement a simple runtime texture loader if needed, or assume it's assigned to a manager.
|
||||
|
||||
// For now, let's assume the texture is assigned or loaded.
|
||||
// I'll add a static reference to the collection texture in NetworkManager or AgentVisual.
|
||||
|
||||
if (characterSprite != null) return; // Already has a sprite
|
||||
|
||||
StartCoroutine(LoadSpriteCoroutine(id));
|
||||
}
|
||||
|
||||
private IEnumerator LoadSpriteCoroutine(int id)
|
||||
{
|
||||
// This is a simplified runtime loader for the demonstration
|
||||
string path = Application.dataPath + "/Sprites/Characters.png";
|
||||
if (!System.IO.File.Exists(path)) yield break;
|
||||
|
||||
byte[] fileData = System.IO.File.ReadAllBytes(path);
|
||||
Texture2D sourceTex = new Texture2D(2, 2);
|
||||
sourceTex.LoadImage(fileData);
|
||||
|
||||
// Phase 19-C: Fix black/white background with robust transcoding
|
||||
Texture2D tex = ProcessTransparency(sourceTex);
|
||||
|
||||
// Slice the 1x3 collection (3 characters in a row)
|
||||
int charIndex = id % 3;
|
||||
float charWidth = tex.width / 3f;
|
||||
Rect rect = new Rect(charIndex * charWidth, 0, charWidth, tex.height);
|
||||
|
||||
characterSprite = Sprite.Create(tex, rect, new Vector2(0.5f, 0.5f), 100f);
|
||||
if (_spriteRenderer != null)
|
||||
{
|
||||
_spriteRenderer.sprite = characterSprite;
|
||||
_spriteRenderer.color = Color.white;
|
||||
|
||||
// Phase 19-C: Normalize scale. Target height approx 2.0 units.
|
||||
float spriteHeightUnits = characterSprite.rect.height / characterSprite.pixelsPerUnit;
|
||||
float normScale = 2.0f / spriteHeightUnits; // Desired height is 2.0 units
|
||||
_spriteRenderer.transform.localScale = new Vector3(normScale, normScale, 1);
|
||||
|
||||
// Update original scale for animator
|
||||
_originalSpriteScale = _spriteRenderer.transform.localScale;
|
||||
}
|
||||
}
|
||||
|
||||
private Texture2D ProcessTransparency(Texture2D source)
|
||||
{
|
||||
if (source == null) return null;
|
||||
|
||||
// Create a new texture with Alpha channel
|
||||
Texture2D tex = new Texture2D(source.width, source.height, TextureFormat.RGBA32, false);
|
||||
Color[] pixels = source.GetPixels();
|
||||
|
||||
for (int i = 0; i < pixels.Length; i++)
|
||||
{
|
||||
Color p = pixels[i];
|
||||
// Chroma-key: If pixel is very close to white, make it transparent
|
||||
// Using 0.9f as threshold to catch almost-white artifacts
|
||||
if (p.r > 0.9f && p.g > 0.9f && p.b > 0.9f)
|
||||
{
|
||||
pixels[i] = new Color(0, 0, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ensure full opacity for others
|
||||
pixels[i] = new Color(p.r, p.g, p.b, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
tex.SetPixels(pixels);
|
||||
tex.Apply();
|
||||
return tex;
|
||||
}
|
||||
|
||||
private void ApplyAgentColor(int agentId)
|
||||
{
|
||||
// Generate unique color per agent
|
||||
@@ -407,7 +673,27 @@ namespace TheIsland.Visual
|
||||
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);
|
||||
int y = (int)center.y;
|
||||
|
||||
// Mouth shape based on mood
|
||||
if (_moodState == "happy")
|
||||
{
|
||||
y = (int)(center.y - Mathf.Sin(t * Mathf.PI) * 2);
|
||||
}
|
||||
else if (_moodState == "sad")
|
||||
{
|
||||
y = (int)(center.y - 2 + Mathf.Sin(t * Mathf.PI) * 2);
|
||||
}
|
||||
else if (_moodState == "anxious")
|
||||
{
|
||||
// Wavy mouth
|
||||
y = (int)(center.y + Mathf.Sin(t * Mathf.PI * 3) * 1);
|
||||
}
|
||||
else // neutral
|
||||
{
|
||||
y = (int)(center.y);
|
||||
}
|
||||
|
||||
if (x >= 0 && x < width && y >= 0 && y < height)
|
||||
{
|
||||
pixels[y * width + x] = mouthColor;
|
||||
@@ -602,28 +888,23 @@ namespace TheIsland.Visual
|
||||
rect.anchoredPosition = Vector2.zero;
|
||||
|
||||
var bg = panel.AddComponent<Image>();
|
||||
bg.sprite = CreateRoundedRectSprite(32, 32, 8);
|
||||
bg.sprite = CreateRoundedRectSprite(32, 32, 12);
|
||||
bg.type = Image.Type.Sliced;
|
||||
bg.color = new Color(0.1f, 0.12f, 0.18f, 0.85f);
|
||||
bg.color = new Color(0.05f, 0.08f, 0.15f, 0.45f); // Darker, more transparent glass
|
||||
|
||||
// Add subtle border
|
||||
// Add inner glow border (Phase 19)
|
||||
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();
|
||||
|
||||
borderRect.offsetMin = new Vector2(1, 1);
|
||||
borderRect.offsetMax = new Vector2(-1, -1);
|
||||
|
||||
var borderImg = borderObj.AddComponent<Image>();
|
||||
borderImg.sprite = CreateRoundedRectSprite(32, 32, 8);
|
||||
borderImg.sprite = CreateRoundedRectSprite(32, 32, 12);
|
||||
borderImg.type = Image.Type.Sliced;
|
||||
borderImg.color = new Color(0.3f, 0.35f, 0.45f, 0.5f);
|
||||
borderImg.color = new Color(1f, 1f, 1f, 0.15f); // Subtle highlight
|
||||
|
||||
return panel;
|
||||
}
|
||||
@@ -818,36 +1099,30 @@ namespace TheIsland.Visual
|
||||
{
|
||||
_currentData = data;
|
||||
|
||||
// Update HP bar
|
||||
float hpPercent = data.hp / 100f;
|
||||
if (_hpBarFill != null)
|
||||
{
|
||||
_hpBarFill.rectTransform.anchorMax = new Vector2(hpPercent, 1);
|
||||
_hpBarFill.color = Color.Lerp(hpLowColor, hpHighColor, hpPercent);
|
||||
}
|
||||
// Set targets for smooth lerping (Phase 19)
|
||||
_targetHpPercent = data.hp / 100f;
|
||||
_targetEnergyPercent = data.energy / 100f;
|
||||
_targetMoodPercent = data.mood / 100f;
|
||||
|
||||
if (_hpText != null)
|
||||
{
|
||||
_hpText.text = $"HP: {data.hp}";
|
||||
}
|
||||
|
||||
// Update Energy bar
|
||||
float energyPercent = data.energy / 100f;
|
||||
if (_energyBarFill != null)
|
||||
{
|
||||
_energyBarFill.rectTransform.anchorMax = new Vector2(energyPercent, 1);
|
||||
_energyBarFill.color = Color.Lerp(energyLowColor, energyHighColor, energyPercent);
|
||||
}
|
||||
if (_energyText != null)
|
||||
{
|
||||
_energyText.text = $"Energy: {data.energy}";
|
||||
}
|
||||
|
||||
// Update Mood bar
|
||||
float moodPercent = data.mood / 100f;
|
||||
if (_moodBarFill != null)
|
||||
// Check for mood change (Visual Expression)
|
||||
if (_moodState != data.mood_state)
|
||||
{
|
||||
_moodBarFill.rectTransform.anchorMax = new Vector2(moodPercent, 1);
|
||||
_moodBarFill.color = GetMoodColor(data.mood_state);
|
||||
_moodState = data.mood_state;
|
||||
// Only regenerate if using placeholder sprite
|
||||
if (characterSprite == null && _spriteRenderer != null)
|
||||
{
|
||||
RegeneratePlaceholderSprite();
|
||||
}
|
||||
}
|
||||
if (_moodText != null)
|
||||
{
|
||||
@@ -912,19 +1187,56 @@ namespace TheIsland.Visual
|
||||
{
|
||||
if (_deathOverlay != null) _deathOverlay.SetActive(false);
|
||||
|
||||
// Restore sprite color
|
||||
// Restore sprite color based on state
|
||||
if (_spriteRenderer != null)
|
||||
{
|
||||
_spriteRenderer.color = Color.white;
|
||||
Color targetColor = spriteColor;
|
||||
|
||||
// Phase 15: Sickness visual (Green tint)
|
||||
if (_currentData != null && _currentData.is_sick)
|
||||
{
|
||||
targetColor = Color.Lerp(targetColor, Color.green, 0.4f);
|
||||
}
|
||||
|
||||
_spriteRenderer.color = targetColor;
|
||||
}
|
||||
|
||||
// Phase 17-B: Update social role display
|
||||
UpdateSocialRoleDisplay();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display social role indicator based on agent's role.
|
||||
/// </summary>
|
||||
private void UpdateSocialRoleDisplay()
|
||||
{
|
||||
if (_currentData == null || _nameLabel == null) return;
|
||||
|
||||
string roleIcon = _currentData.social_role switch
|
||||
{
|
||||
"leader" => " <color=#FFD700>★</color>", // Gold star
|
||||
"loner" => " <color=#808080>☁</color>", // Gray cloud
|
||||
"follower" => " <color=#87CEEB>→</color>", // Sky blue arrow
|
||||
_ => ""
|
||||
};
|
||||
|
||||
// Append role icon to name (strip any existing icons first)
|
||||
string baseName = _currentData.name;
|
||||
_nameLabel.text = baseName + roleIcon;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Speech
|
||||
public void ShowSpeech(string text)
|
||||
{
|
||||
ShowSpeech(text, speechDuration);
|
||||
}
|
||||
|
||||
public void ShowSpeech(string text, float duration)
|
||||
{
|
||||
if (_speechBubble == null || !IsAlive) return;
|
||||
|
||||
_speechBubble.DisplayDuration = duration;
|
||||
_speechBubble.Setup(text);
|
||||
Debug.Log($"[AgentVisual] {_currentData?.name} says: \"{text}\"");
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ namespace TheIsland.UI
|
||||
private List<GameObject> _entries = new List<GameObject>();
|
||||
private bool _visible = true;
|
||||
private int _unread = 0;
|
||||
private bool _ready = false;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -83,7 +82,6 @@ namespace TheIsland.UI
|
||||
if (NetworkManager.Instance != null)
|
||||
{
|
||||
SubscribeEvents();
|
||||
_ready = true;
|
||||
AddLog("事件日志已就绪", Color.yellow);
|
||||
Debug.Log("[EventLog] 初始化完成");
|
||||
}
|
||||
@@ -189,7 +187,7 @@ namespace TheIsland.UI
|
||||
panelRect.offsetMax = new Vector2(360, -80);
|
||||
|
||||
var panelImg = _panel.AddComponent<Image>();
|
||||
panelImg.color = new Color(0.05f, 0.07f, 0.1f, 0.95f);
|
||||
panelImg.color = new Color(0f, 0f, 0f, 0.0f); // 完全透明背景
|
||||
|
||||
// 标题
|
||||
var header = new GameObject("Header");
|
||||
@@ -201,7 +199,7 @@ namespace TheIsland.UI
|
||||
headerRect.sizeDelta = new Vector2(0, 28);
|
||||
headerRect.anchoredPosition = Vector2.zero;
|
||||
|
||||
header.AddComponent<Image>().color = new Color(0.12f, 0.15f, 0.2f);
|
||||
header.AddComponent<Image>().color = new Color(0.12f, 0.15f, 0.2f, 0.8f);
|
||||
|
||||
var titleObj = new GameObject("Title");
|
||||
titleObj.transform.SetParent(header.transform, false);
|
||||
@@ -296,8 +294,8 @@ namespace TheIsland.UI
|
||||
entry.transform.SetParent(_content, false);
|
||||
|
||||
entry.AddComponent<Image>().color = _entries.Count % 2 == 0
|
||||
? new Color(0.08f, 0.1f, 0.13f, 0.9f)
|
||||
: new Color(0.06f, 0.08f, 0.11f, 0.9f);
|
||||
? new Color(0f, 0f, 0f, 0.2f)
|
||||
: new Color(0f, 0f, 0f, 0.1f);
|
||||
|
||||
var le = entry.AddComponent<LayoutElement>();
|
||||
le.minHeight = 36;
|
||||
|
||||
@@ -152,6 +152,9 @@ namespace TheIsland.Core
|
||||
network.OnTalk += HandleTalk;
|
||||
network.OnRevive += HandleRevive;
|
||||
network.OnSocialInteraction += HandleSocialInteraction;
|
||||
network.OnGiftEffect += HandleGiftEffect; // Phase 8
|
||||
network.OnAgentAction += HandleAgentAction; // Phase 13
|
||||
network.OnRandomEvent += HandleRandomEvent; // Phase 17-C
|
||||
}
|
||||
|
||||
private void UnsubscribeFromNetworkEvents()
|
||||
@@ -178,6 +181,8 @@ namespace TheIsland.Core
|
||||
network.OnTalk -= HandleTalk;
|
||||
network.OnRevive -= HandleRevive;
|
||||
network.OnSocialInteraction -= HandleSocialInteraction;
|
||||
network.OnGiftEffect -= HandleGiftEffect; // Phase 8
|
||||
network.OnRandomEvent -= HandleRandomEvent; // Phase 17-C
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -320,6 +325,7 @@ namespace TheIsland.Core
|
||||
if (_agentVisuals.TryGetValue(data.agent_id, out AgentVisual agentVisual))
|
||||
{
|
||||
agentVisual.ShowSpeech(data.text);
|
||||
agentVisual.DoJump(); // Add jump effect
|
||||
}
|
||||
// Check AgentUI (programmatic UI system)
|
||||
else if (_agentUIs.TryGetValue(data.agent_id, out AgentUI agentUI))
|
||||
@@ -485,6 +491,140 @@ namespace TheIsland.Core
|
||||
initiatorUI.ShowSpeech(data.dialogue);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle gift effect event (Twitch bits, subscriptions).
|
||||
/// Plays VFX and shows gratitude speech.
|
||||
/// </summary>
|
||||
private void HandleGiftEffect(GiftEffectData data)
|
||||
{
|
||||
Debug.Log($"[GameManager] Gift: {data.user} sent {data.value} {data.gift_type}");
|
||||
|
||||
// Find target agent position for VFX
|
||||
Vector3 effectPosition = Vector3.zero;
|
||||
int agentId = GetAgentIdByName(data.agent_name);
|
||||
|
||||
if (agentId >= 0 && _agentVisuals.TryGetValue(agentId, out AgentVisual agentVisual))
|
||||
{
|
||||
effectPosition = agentVisual.transform.position;
|
||||
|
||||
// Show gratitude speech with extended duration
|
||||
if (!string.IsNullOrEmpty(data.gratitude))
|
||||
{
|
||||
float duration = data.duration > 0 ? data.duration : 8f;
|
||||
agentVisual.ShowSpeech(data.gratitude, duration);
|
||||
}
|
||||
}
|
||||
else if (agentId >= 0 && _agentUIs.TryGetValue(agentId, out AgentUI agentUI))
|
||||
{
|
||||
effectPosition = agentUI.transform.position;
|
||||
|
||||
if (!string.IsNullOrEmpty(data.gratitude))
|
||||
{
|
||||
agentUI.ShowSpeech(data.gratitude);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default to center if no agent found
|
||||
effectPosition = new Vector3(0, 1, 0);
|
||||
}
|
||||
|
||||
// Play VFX
|
||||
if (VFXManager.Instance != null)
|
||||
{
|
||||
VFXManager.Instance.PlayEffect(data.gift_type, effectPosition);
|
||||
}
|
||||
|
||||
// Show notification
|
||||
ShowNotification(data.message);
|
||||
}
|
||||
#region Agent Action (Phase 13)
|
||||
private void HandleAgentAction(AgentActionData data)
|
||||
{
|
||||
Debug.Log($"[GameManager] Action: {data.agent_name} -> {data.action_type} at {data.location}");
|
||||
|
||||
// Resolve target position
|
||||
Vector3 targetPos = GetLocationPosition(data.location, data.target_name);
|
||||
|
||||
// Find agent and command movement
|
||||
if (_agentVisuals.TryGetValue(data.agent_id, out AgentVisual agentVisual))
|
||||
{
|
||||
agentVisual.MoveTo(targetPos);
|
||||
|
||||
// Optional: Show thought bubble or speech
|
||||
if (!string.IsNullOrEmpty(data.dialogue))
|
||||
{
|
||||
agentVisual.ShowSpeech(data.dialogue, 3f);
|
||||
}
|
||||
}
|
||||
else if (_agentUIs.TryGetValue(data.agent_id, out AgentUI agentUI))
|
||||
{
|
||||
// Fallback for UI-only agents (just show speech)
|
||||
if (!string.IsNullOrEmpty(data.dialogue))
|
||||
{
|
||||
agentUI.ShowSpeech(data.dialogue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3 GetLocationPosition(string location, string targetName)
|
||||
{
|
||||
// Map logical locations to world coordinates
|
||||
switch (location.ToLower())
|
||||
{
|
||||
case "tree_left":
|
||||
return new Vector3(-10f, 0f, 8f);
|
||||
case "tree_right":
|
||||
return new Vector3(10f, 0f, 8f);
|
||||
case "campfire":
|
||||
case "center":
|
||||
return new Vector3(0f, 0f, 0f);
|
||||
case "water":
|
||||
case "beach":
|
||||
return new Vector3(Random.Range(-5, 5), 0f, 4f);
|
||||
case "nearby":
|
||||
// Move to random nearby spot (wandering)
|
||||
return new Vector3(Random.Range(-12, 12), 0f, Random.Range(-2, 6));
|
||||
case "herb_patch":
|
||||
// Phase 16: Herb gathering location
|
||||
return new Vector3(-8f, 0f, -5f);
|
||||
case "agent":
|
||||
// Move to another agent
|
||||
int targetId = GetAgentIdByName(targetName);
|
||||
if (targetId >= 0 && _agentVisuals.TryGetValue(targetId, out AgentVisual target))
|
||||
{
|
||||
// Stand slightly offset from target
|
||||
return target.transform.position + new Vector3(1.5f, 0, 0);
|
||||
}
|
||||
return Vector3.zero;
|
||||
default:
|
||||
return new Vector3(0f, 0f, 0f); // Fallback to center
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Random Events (Phase 17-C)
|
||||
private void HandleRandomEvent(RandomEventData data)
|
||||
{
|
||||
Debug.Log($"[GameManager] Random Event: {data.event_type} - {data.message}");
|
||||
|
||||
// Display global notification banner
|
||||
string eventIcon = data.event_type switch
|
||||
{
|
||||
"storm_damage" => "⛈️ ",
|
||||
"treasure_found" => "💎 ",
|
||||
"beast_attack" => "🐺 ",
|
||||
"rumor_spread" => "💬 ",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
ShowNotification(eventIcon + data.message);
|
||||
|
||||
// Optional: Trigger visual effects based on event type
|
||||
// (Could add screen shake for storm, highlight agent for treasure, etc.)
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Agent Management
|
||||
|
||||
@@ -48,6 +48,17 @@ namespace TheIsland.Models
|
||||
public string mood_state; // "happy", "neutral", "sad", "anxious"
|
||||
public string social_tendency; // "introvert", "extrovert", "neutral"
|
||||
|
||||
// Survival (Phase 15)
|
||||
public bool is_sick;
|
||||
public int immunity;
|
||||
|
||||
// Autonomous Agency (Phase 13)
|
||||
public string current_action;
|
||||
public string location;
|
||||
|
||||
// Relationship 2.0 (Phase 17-B)
|
||||
public string social_role; // "leader", "follower", "loner", "neutral"
|
||||
|
||||
public bool IsAlive => status == "Alive";
|
||||
}
|
||||
|
||||
@@ -138,6 +149,10 @@ namespace TheIsland.Models
|
||||
public int resource_level;
|
||||
public int current_tick_in_day;
|
||||
public string time_of_day; // "dawn", "day", "dusk", "night"
|
||||
|
||||
// Resource Scarcity (Phase 17-A)
|
||||
public int tree_left_fruit;
|
||||
public int tree_right_fruit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -240,6 +255,21 @@ namespace TheIsland.Models
|
||||
public string dialogue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gift effect event data (Twitch bits, subscriptions, etc.).
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class GiftEffectData
|
||||
{
|
||||
public string user;
|
||||
public string gift_type; // "bits", "heart", "sub"
|
||||
public int value;
|
||||
public string message;
|
||||
public string agent_name; // Target agent for the effect
|
||||
public string gratitude; // AI-generated thank you message
|
||||
public float duration; // How long to show the speech bubble (default 8s)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client message structure for sending to server.
|
||||
/// </summary>
|
||||
@@ -293,5 +323,67 @@ namespace TheIsland.Models
|
||||
public const string SOCIAL_INTERACTION = "social_interaction";
|
||||
public const string RELATIONSHIP_CHANGE = "relationship_change";
|
||||
public const string AUTO_REVIVE = "auto_revive";
|
||||
|
||||
// Gift/Donation system (Phase 8)
|
||||
public const string GIFT_EFFECT = "gift_effect";
|
||||
|
||||
// Autonomous Agency (Phase 13)
|
||||
public const string AGENT_ACTION = "agent_action";
|
||||
|
||||
// Crafting System (Phase 16)
|
||||
public const string CRAFT = "craft";
|
||||
public const string USE_ITEM = "use_item";
|
||||
|
||||
// Random Events (Phase 17-C)
|
||||
public const string RANDOM_EVENT = "random_event";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Agent action event data (Phase 13).
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class AgentActionData
|
||||
{
|
||||
public int agent_id;
|
||||
public string agent_name;
|
||||
public string action_type; // "Gather", "Sleep", "Socialize", "Wander", "Gather Herb", etc.
|
||||
public string location; // "tree_left", "campfire", "herb_patch", etc.
|
||||
public string target_name; // For social actions
|
||||
public string dialogue; // Bark text
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Craft event data (Phase 16).
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class CraftEventData
|
||||
{
|
||||
public int agent_id;
|
||||
public string agent_name;
|
||||
public string item; // "medicine"
|
||||
public string ingredients; // JSON string of ingredients used
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use item event data (Phase 16).
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class UseItemEventData
|
||||
{
|
||||
public int agent_id;
|
||||
public string agent_name;
|
||||
public string item; // "medicine"
|
||||
public string effect; // "cured sickness"
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Random event data (Phase 17-C).
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class RandomEventData
|
||||
{
|
||||
public string event_type; // "storm_damage", "treasure_found", "beast_attack", "rumor_spread"
|
||||
public string message;
|
||||
public string agent_name; // Optional: affected agent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,11 @@ namespace TheIsland.Network
|
||||
public event Action<ReviveEventData> OnRevive;
|
||||
public event Action<SocialInteractionData> OnSocialInteraction;
|
||||
public event Action<WorldStateData> OnWorldUpdate;
|
||||
public event Action<GiftEffectData> OnGiftEffect; // Phase 8: Gift/Donation effects
|
||||
public event Action<AgentActionData> OnAgentAction; // Phase 13: Autonomous Actions
|
||||
public event Action<CraftEventData> OnCraft; // Phase 16: Crafting
|
||||
public event Action<UseItemEventData> OnUseItem; // Phase 16: Using items
|
||||
public event Action<RandomEventData> OnRandomEvent; // Phase 17-C: Random Events
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
@@ -343,11 +348,37 @@ namespace TheIsland.Network
|
||||
OnSocialInteraction?.Invoke(socialData);
|
||||
break;
|
||||
|
||||
case EventTypes.GIFT_EFFECT:
|
||||
var giftData = JsonUtility.FromJson<GiftEffectData>(dataJson);
|
||||
OnGiftEffect?.Invoke(giftData);
|
||||
break;
|
||||
|
||||
case EventTypes.AGENT_ACTION:
|
||||
var actionData = JsonUtility.FromJson<AgentActionData>(dataJson);
|
||||
OnAgentAction?.Invoke(actionData);
|
||||
break;
|
||||
|
||||
case EventTypes.CRAFT:
|
||||
var craftData = JsonUtility.FromJson<CraftEventData>(dataJson);
|
||||
OnCraft?.Invoke(craftData);
|
||||
break;
|
||||
|
||||
case EventTypes.USE_ITEM:
|
||||
var useItemData = JsonUtility.FromJson<UseItemEventData>(dataJson);
|
||||
OnUseItem?.Invoke(useItemData);
|
||||
break;
|
||||
|
||||
case EventTypes.COMMENT:
|
||||
// Comments can be logged but typically not displayed in 3D
|
||||
Debug.Log($"[Chat] {json}");
|
||||
break;
|
||||
|
||||
case EventTypes.RANDOM_EVENT:
|
||||
var randomEventData = JsonUtility.FromJson<RandomEventData>(dataJson);
|
||||
OnRandomEvent?.Invoke(randomEventData);
|
||||
Debug.Log($"[Random Event] {randomEventData.event_type}: {randomEventData.message}");
|
||||
break;
|
||||
|
||||
default:
|
||||
Debug.Log($"[NetworkManager] Unhandled event type: {baseMessage.event_type}");
|
||||
break;
|
||||
|
||||
@@ -164,7 +164,7 @@ namespace TheIsland.UI
|
||||
topBar.offsetMax = new Vector2(-10, -10);
|
||||
|
||||
var topBarImg = topBar.gameObject.AddComponent<Image>();
|
||||
topBarImg.color = new Color(0, 0, 0, 0.7f);
|
||||
topBarImg.color = new Color(0, 0, 0, 0.0f); // 透明顶部栏
|
||||
|
||||
// Connection Status (Left)
|
||||
_connectionStatus = CreateText(topBar, "ConnectionStatus", "● Disconnected",
|
||||
@@ -205,7 +205,7 @@ namespace TheIsland.UI
|
||||
bottomBar.offsetMax = new Vector2(-10, 70);
|
||||
|
||||
var bottomBarImg = bottomBar.gameObject.AddComponent<Image>();
|
||||
bottomBarImg.color = new Color(0, 0, 0, 0.7f);
|
||||
bottomBarImg.color = new Color(0, 0, 0, 0.2f); // 低透明度底部栏
|
||||
|
||||
// Command Input
|
||||
var inputObj = new GameObject("CommandInput");
|
||||
|
||||
162
unity-client/Assets/Scripts/Visual/AgentAnimator.cs
Normal file
162
unity-client/Assets/Scripts/Visual/AgentAnimator.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
|
||||
namespace TheIsland
|
||||
{
|
||||
/// <summary>
|
||||
/// Procedural 2D animator for agents.
|
||||
/// Handles idle breathing, movement bopping, and action-based squash/stretch.
|
||||
/// Phase 19-E: Added banking turns and anticipation/overshoot.
|
||||
/// </summary>
|
||||
public class AgentAnimator : MonoBehaviour
|
||||
{
|
||||
[Header("Animation Settings")]
|
||||
public float idleSpeed = 2f;
|
||||
public float idleAmount = 0.04f;
|
||||
public float moveBopSpeed = 12f;
|
||||
public float moveBopAmount = 0.1f;
|
||||
public float moveTiltAmount = 10f;
|
||||
public float bankingAmount = 15f;
|
||||
|
||||
private SpriteRenderer _spriteRenderer;
|
||||
private Vector3 _originalScale;
|
||||
private Vector3 _targetLocalPos;
|
||||
private Quaternion _targetLocalRot;
|
||||
private Vector3 _targetScale;
|
||||
|
||||
private Vector3 _currentVelocity;
|
||||
private float _velocityPercentage; // 0 to 1
|
||||
private bool _isMoving;
|
||||
private float _transitionTimer;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_spriteRenderer != null)
|
||||
{
|
||||
_originalScale = _spriteRenderer.transform.localScale;
|
||||
}
|
||||
else
|
||||
{
|
||||
_originalScale = Vector3.one;
|
||||
}
|
||||
_targetScale = _originalScale;
|
||||
}
|
||||
|
||||
public void SetMovement(Vector3 velocity, float maxVelocity = 3f)
|
||||
{
|
||||
_currentVelocity = velocity;
|
||||
_velocityPercentage = Mathf.Clamp01(velocity.magnitude / Mathf.Max(0.1f, maxVelocity));
|
||||
|
||||
bool nowMoving = _velocityPercentage > 0.05f;
|
||||
if (nowMoving && !_isMoving)
|
||||
{
|
||||
// Anticipation: Squash when starting to move
|
||||
TriggerAnticipation();
|
||||
}
|
||||
else if (!nowMoving && _isMoving)
|
||||
{
|
||||
// Overshoot: Rebound when stopping
|
||||
TriggerOvershoot();
|
||||
}
|
||||
|
||||
_isMoving = nowMoving;
|
||||
}
|
||||
|
||||
// Compatibility for older code
|
||||
public void SetMovement(float currentVelocity, float maxVelocity)
|
||||
{
|
||||
SetMovement(new Vector3(currentVelocity, 0, 0), maxVelocity);
|
||||
}
|
||||
|
||||
public void TriggerActionEffect()
|
||||
{
|
||||
StopAllCoroutines();
|
||||
StartCoroutine(ActionPulseRoutine(0.4f, 1.3f));
|
||||
}
|
||||
|
||||
private void TriggerAnticipation()
|
||||
{
|
||||
StartCoroutine(ActionPulseRoutine(0.15f, 0.8f)); // Squash
|
||||
}
|
||||
|
||||
private void TriggerOvershoot()
|
||||
{
|
||||
StartCoroutine(ActionPulseRoutine(0.2f, 1.15f)); // Slight stretch
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_spriteRenderer == null) return;
|
||||
|
||||
if (_isMoving)
|
||||
{
|
||||
AnimateMove();
|
||||
}
|
||||
else
|
||||
{
|
||||
AnimateIdle();
|
||||
}
|
||||
|
||||
// Smoothly apply transforms
|
||||
float lerpSpeed = 12f;
|
||||
var t = _spriteRenderer.transform;
|
||||
t.localPosition = Vector3.Lerp(t.localPosition, _targetLocalPos, Time.deltaTime * lerpSpeed);
|
||||
t.localRotation = Quaternion.Slerp(t.localRotation, _targetLocalRot, Time.deltaTime * lerpSpeed);
|
||||
t.localScale = Vector3.Lerp(t.localScale, _targetScale, Time.deltaTime * lerpSpeed);
|
||||
}
|
||||
|
||||
private void AnimateIdle()
|
||||
{
|
||||
// Idle "Breathing"
|
||||
float breathe = Mathf.Sin(Time.time * idleSpeed) * idleAmount;
|
||||
|
||||
_targetScale = new Vector3(_originalScale.x, _originalScale.y * (1f + breathe), _originalScale.z);
|
||||
_targetLocalPos = Vector3.zero;
|
||||
_targetLocalRot = Quaternion.identity;
|
||||
}
|
||||
|
||||
private void AnimateMove()
|
||||
{
|
||||
// Movement "Bopping" - Sin wave for vertical bounce
|
||||
float cycle = Time.time * moveBopSpeed;
|
||||
float bop = Mathf.Abs(Mathf.Sin(cycle)) * moveBopAmount * _velocityPercentage;
|
||||
|
||||
// Traditional Bop Tilt
|
||||
float bopTilt = Mathf.Sin(cycle) * moveTiltAmount * _velocityPercentage;
|
||||
|
||||
// Phase 19-E: Banking Turn Tilt
|
||||
// Lean into the direction of X velocity
|
||||
float bankingTilt = -(_currentVelocity.x / 3f) * bankingAmount;
|
||||
|
||||
_targetLocalPos = new Vector3(0, bop, 0);
|
||||
_targetLocalRot = Quaternion.Euler(0, 0, bopTilt + bankingTilt);
|
||||
|
||||
// Squash and stretch during the bop
|
||||
float stretch = 1f + bop;
|
||||
float squash = 1f / stretch;
|
||||
_targetScale = new Vector3(_originalScale.x * squash, _originalScale.y * stretch, _originalScale.z);
|
||||
}
|
||||
|
||||
private IEnumerator ActionPulseRoutine(float duration, float targetScaleY)
|
||||
{
|
||||
float elapsed = 0;
|
||||
Vector3 peakScale = new Vector3(_originalScale.x * (2f - targetScaleY), _originalScale.y * targetScaleY, _originalScale.z);
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
float progress = elapsed / duration;
|
||||
float sin = Mathf.Sin(progress * Mathf.PI);
|
||||
|
||||
// Temp override of targetScale for the pulse
|
||||
_spriteRenderer.transform.localScale = Vector3.Lerp(_originalScale, peakScale, sin);
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
unity-client/Assets/Scripts/Visual/AgentAnimator.cs.meta
Normal file
2
unity-client/Assets/Scripts/Visual/AgentAnimator.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b9e1cbf8a16c41ccb4b5f197e3ade72
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using TheIsland.Core;
|
||||
using TheIsland.Network;
|
||||
@@ -56,7 +57,7 @@ namespace TheIsland.Visual
|
||||
[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;
|
||||
[SerializeField] private Material customWaterMaterial; // Custom shader support
|
||||
#endregion
|
||||
|
||||
#region References
|
||||
@@ -74,6 +75,7 @@ namespace TheIsland.Visual
|
||||
private float _transitionProgress = 1f;
|
||||
private Color _targetSkyTop, _targetSkyBottom;
|
||||
private Color _currentSkyTop, _currentSkyBottom;
|
||||
private List<Transform> _palmTrees = new List<Transform>();
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
@@ -86,6 +88,7 @@ namespace TheIsland.Visual
|
||||
}
|
||||
_instance = this;
|
||||
|
||||
LoadEnvironmentTexture();
|
||||
_mainCamera = Camera.main;
|
||||
CreateEnvironment();
|
||||
}
|
||||
@@ -103,6 +106,15 @@ namespace TheIsland.Visual
|
||||
|
||||
// Set initial sky
|
||||
UpdateSkyColors();
|
||||
|
||||
// Phase 19-B: Cache palm trees for animation
|
||||
CachePalmTrees();
|
||||
|
||||
// Phase 19: Add Visual Effects Manager
|
||||
if (FindFirstObjectByType<VisualEffectsManager>() == null)
|
||||
{
|
||||
new GameObject("VisualEffectsManager").AddComponent<VisualEffectsManager>();
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
@@ -116,8 +128,56 @@ namespace TheIsland.Visual
|
||||
UpdateSkyMaterial();
|
||||
}
|
||||
|
||||
// Animate water
|
||||
AnimateWater();
|
||||
// Phase 19: Cinematic Lighting
|
||||
AnimateLighting();
|
||||
|
||||
// Animate environment (Water & Trees)
|
||||
AnimateEnvironment();
|
||||
AnimateClouds();
|
||||
}
|
||||
|
||||
private void AnimateLighting()
|
||||
{
|
||||
if (_mainLight == null) return;
|
||||
|
||||
// Simple 120s cycle for demonstration (30s per phase)
|
||||
float cycleDuration = 120f;
|
||||
float t = (Time.time % cycleDuration) / cycleDuration;
|
||||
|
||||
// t: 0=Dawn, 0.25=Noon, 0.5=Dusk, 0.75=Midnight
|
||||
float intensity = 1f;
|
||||
Color lightColor = Color.white;
|
||||
|
||||
if (t < 0.2f) // Dawn
|
||||
{
|
||||
float p = t / 0.2f;
|
||||
intensity = Mathf.Lerp(0.5f, 1.2f, p);
|
||||
lightColor = Color.Lerp(new Color(1f, 0.6f, 0.4f), Color.white, p);
|
||||
}
|
||||
else if (t < 0.5f) // Day
|
||||
{
|
||||
intensity = 1.2f;
|
||||
lightColor = Color.white;
|
||||
}
|
||||
else if (t < 0.7f) // Dusk
|
||||
{
|
||||
float p = (t - 0.5f) / 0.2f;
|
||||
intensity = Mathf.Lerp(1.2f, 0.4f, p);
|
||||
lightColor = Color.Lerp(Color.white, new Color(1f, 0.4f, 0.2f), p);
|
||||
}
|
||||
else // Night
|
||||
{
|
||||
float p = (t - 0.7f) / 0.3f;
|
||||
intensity = Mathf.Lerp(0.4f, 0.2f, p);
|
||||
lightColor = new Color(0.4f, 0.5f, 1f); // Moonlight
|
||||
}
|
||||
|
||||
_mainLight.intensity = intensity;
|
||||
_mainLight.color = lightColor;
|
||||
|
||||
// Rotate sun
|
||||
float sunAngle = t * 360f - 90f;
|
||||
_mainLight.transform.rotation = Quaternion.Euler(sunAngle, -30f, 0);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
@@ -140,6 +200,7 @@ namespace TheIsland.Visual
|
||||
CreateWater();
|
||||
CreateLighting();
|
||||
CreateDecorations();
|
||||
CreateClouds();
|
||||
}
|
||||
|
||||
private void CreateSky()
|
||||
@@ -172,45 +233,6 @@ namespace TheIsland.Visual
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -308,9 +330,17 @@ namespace TheIsland.Visual
|
||||
_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;
|
||||
if (customWaterMaterial != null)
|
||||
{
|
||||
_waterMaterial = customWaterMaterial;
|
||||
_waterPlane.GetComponent<Renderer>().material = _waterMaterial;
|
||||
}
|
||||
else
|
||||
{
|
||||
_waterMaterial = new Material(Shader.Find("Unlit/Transparent"));
|
||||
_waterMaterial.mainTexture = CreateWaterTexture();
|
||||
_waterPlane.GetComponent<Renderer>().material = _waterMaterial;
|
||||
}
|
||||
_waterPlane.GetComponent<Renderer>().sortingOrder = -40;
|
||||
|
||||
Destroy(_waterPlane.GetComponent<Collider>());
|
||||
@@ -328,11 +358,14 @@ namespace TheIsland.Visual
|
||||
for (int x = 0; x < size; x++)
|
||||
{
|
||||
float t = (float)y / size;
|
||||
Color baseColor = Color.Lerp(waterShallowColor, waterDeepColor, t);
|
||||
// Add some noise to the base color
|
||||
float n = Mathf.PerlinNoise(x * 0.05f, y * 0.05f) * 0.1f;
|
||||
Color baseColor = Color.Lerp(waterShallowColor, waterDeepColor, t + n);
|
||||
|
||||
// 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);
|
||||
// Add caustic-like highlights
|
||||
float wave1 = Mathf.Sin(x * 0.15f + y * 0.05f + Time.time * 0.2f) * 0.5f + 0.5f;
|
||||
float wave2 = Mathf.Cos(x * 0.08f - y * 0.12f + Time.time * 0.15f) * 0.5f + 0.5f;
|
||||
baseColor = Color.Lerp(baseColor, Color.white, (wave1 * wave2) * 0.15f);
|
||||
|
||||
tex.SetPixel(x, y, baseColor);
|
||||
}
|
||||
@@ -346,8 +379,11 @@ namespace TheIsland.Visual
|
||||
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);
|
||||
float offset = Time.time * waveSpeed * 0.05f;
|
||||
_waterMaterial.mainTextureOffset = new Vector2(offset, offset * 0.3f);
|
||||
|
||||
// Periodically update texture for dynamic caustic effect (expensive but looks premium)
|
||||
// Or just use the original UV scrolling if performance is an issue.
|
||||
}
|
||||
|
||||
private void CreateLighting()
|
||||
@@ -383,6 +419,8 @@ namespace TheIsland.Visual
|
||||
CreateRock(new Vector3(-5, 0, 4), 0.5f);
|
||||
CreateRock(new Vector3(6, 0, 5), 0.7f);
|
||||
CreateRock(new Vector3(-7, 0, 6), 0.4f);
|
||||
|
||||
CreateGroundDetails();
|
||||
}
|
||||
|
||||
private void CreatePalmTree(Vector3 position, float scale)
|
||||
@@ -399,11 +437,70 @@ namespace TheIsland.Visual
|
||||
var trunkRenderer = trunkSprite.AddComponent<SpriteRenderer>();
|
||||
trunkRenderer.sprite = CreateTreeSprite();
|
||||
trunkRenderer.sortingOrder = -20;
|
||||
trunkSprite.transform.localScale = new Vector3(scale * 0.5f, scale, 1);
|
||||
|
||||
// Phase 19-C: Add Billboard for 2.5D perspective
|
||||
trunkSprite.AddComponent<Billboard>();
|
||||
|
||||
// Phase 19-C: Normalize scale based on world units.
|
||||
// If the sprite is large, we want it to fit the intended 'scale' height.
|
||||
// A typical tree sprite at 100 PPU might be 10 units high.
|
||||
// We want it to be 'scale' units high (e.g. 3 units).
|
||||
float spriteHeightUnits = trunkRenderer.sprite.rect.height / trunkRenderer.sprite.pixelsPerUnit;
|
||||
float normScale = scale / spriteHeightUnits;
|
||||
trunkSprite.transform.localScale = new Vector3(normScale, normScale, 1);
|
||||
}
|
||||
|
||||
private Texture2D _envTexture;
|
||||
|
||||
private void LoadEnvironmentTexture()
|
||||
{
|
||||
string path = Application.dataPath + "/Sprites/Environment.png";
|
||||
if (System.IO.File.Exists(path))
|
||||
{
|
||||
byte[] data = System.IO.File.ReadAllBytes(path);
|
||||
Texture2D sourceTex = new Texture2D(2, 2);
|
||||
sourceTex.LoadImage(data);
|
||||
|
||||
// Phase 19-C: Robust transparency transcoding
|
||||
_envTexture = ProcessTransparency(sourceTex);
|
||||
}
|
||||
}
|
||||
|
||||
private Texture2D ProcessTransparency(Texture2D source)
|
||||
{
|
||||
if (source == null) return null;
|
||||
|
||||
// Create a new texture with Alpha channel
|
||||
Texture2D tex = new Texture2D(source.width, source.height, TextureFormat.RGBA32, false);
|
||||
Color[] pixels = source.GetPixels();
|
||||
|
||||
for (int i = 0; i < pixels.Length; i++)
|
||||
{
|
||||
Color p = pixels[i];
|
||||
// Chroma-key: If pixel is very close to white, make it transparent
|
||||
if (p.r > 0.9f && p.g > 0.9f && p.b > 0.9f)
|
||||
{
|
||||
pixels[i] = new Color(0, 0, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
pixels[i] = new Color(p.r, p.g, p.b, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
tex.SetPixels(pixels);
|
||||
tex.Apply();
|
||||
return tex;
|
||||
}
|
||||
|
||||
private Sprite CreateTreeSprite()
|
||||
{
|
||||
if (_envTexture != null)
|
||||
{
|
||||
// Slice palm tree (Assuming it's in the top-left quadrant of the collection)
|
||||
return Sprite.Create(_envTexture, new Rect(0, _envTexture.height / 2f, _envTexture.width / 2f, _envTexture.height / 2f), new Vector2(0.5f, 0f), 100f);
|
||||
}
|
||||
|
||||
int width = 64;
|
||||
int height = 128;
|
||||
Texture2D tex = new Texture2D(width, height);
|
||||
@@ -439,6 +536,40 @@ namespace TheIsland.Visual
|
||||
return Sprite.Create(tex, new Rect(0, 0, width, height), new Vector2(0.5f, 0));
|
||||
}
|
||||
|
||||
private void CachePalmTrees()
|
||||
{
|
||||
_palmTrees.Clear();
|
||||
foreach (Transform child in transform)
|
||||
{
|
||||
if (child.name == "PalmTree")
|
||||
{
|
||||
_palmTrees.Add(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AnimateEnvironment()
|
||||
{
|
||||
// Water animation
|
||||
if (_waterMaterial != null)
|
||||
{
|
||||
float offset = Time.time * waveSpeed * 0.1f;
|
||||
_waterMaterial.mainTextureOffset = new Vector2(offset, offset * 0.5f);
|
||||
}
|
||||
|
||||
// Tree swaying animation
|
||||
float weatherIntensity = (_currentWeather == "Stormy" || _currentWeather == "Rainy") ? 2.5f : 1.0f;
|
||||
float time = Time.time;
|
||||
|
||||
foreach (var tree in _palmTrees)
|
||||
{
|
||||
if (tree == null) continue;
|
||||
// Sway rotation with slight variation per tree position
|
||||
float sway = Mathf.Sin(time * 1.5f + tree.position.x * 0.5f) * 2.0f * weatherIntensity;
|
||||
tree.rotation = Quaternion.Euler(0, 0, sway);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPalmFronds(Color[] pixels, int width, int height, Color leaf, Color leafBright)
|
||||
{
|
||||
Vector2 center = new Vector2(width / 2, height * 0.65f);
|
||||
@@ -490,11 +621,24 @@ namespace TheIsland.Visual
|
||||
var rockRenderer = rockObj.AddComponent<SpriteRenderer>();
|
||||
rockRenderer.sprite = CreateRockSprite();
|
||||
rockRenderer.sortingOrder = -15;
|
||||
rockObj.transform.localScale = Vector3.one * scale;
|
||||
|
||||
// Phase 19-C: Add Billboard
|
||||
rockObj.AddComponent<Billboard>();
|
||||
|
||||
// Phase 19-C: Normalize scale
|
||||
float spriteWidthUnits = rockRenderer.sprite.rect.width / rockRenderer.sprite.pixelsPerUnit;
|
||||
float normScale = scale / spriteWidthUnits;
|
||||
rockObj.transform.localScale = Vector3.one * normScale;
|
||||
}
|
||||
|
||||
private Sprite CreateRockSprite()
|
||||
{
|
||||
if (_envTexture != null)
|
||||
{
|
||||
// Slice rock from Environment.png (Assuming bottom-right quadrant)
|
||||
return Sprite.Create(_envTexture, new Rect(_envTexture.width / 2f, 0, _envTexture.width / 2f, _envTexture.height / 2f), new Vector2(0.5f, 0.5f), 100f);
|
||||
}
|
||||
|
||||
int size = 32;
|
||||
Texture2D tex = new Texture2D(size, size);
|
||||
|
||||
@@ -540,7 +684,16 @@ namespace TheIsland.Visual
|
||||
private void HandleWeatherChange(WeatherChangeData data)
|
||||
{
|
||||
_currentWeather = data.new_weather;
|
||||
UpdateSkyColors();
|
||||
Debug.Log($"[EnvironmentManager] Weather changed to: {_currentWeather}");
|
||||
|
||||
// Notify VFX manager
|
||||
if (VisualEffectsManager.Instance != null)
|
||||
{
|
||||
VisualEffectsManager.Instance.SetWeather(_currentWeather);
|
||||
}
|
||||
|
||||
// Adjust lighting based on weather
|
||||
UpdateSkyColors(); // This will use the new weather in its logic
|
||||
}
|
||||
|
||||
private void HandleTick(TickData data)
|
||||
@@ -612,6 +765,164 @@ namespace TheIsland.Visual
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void CreateGroundDetails()
|
||||
{
|
||||
// Scatter shells
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
float x = Random.Range(-25f, 25f);
|
||||
float z = Random.Range(3f, 7f); // Near water line
|
||||
|
||||
var shell = new GameObject("Shell");
|
||||
shell.transform.SetParent(transform);
|
||||
shell.transform.position = new Vector3(x, -0.45f, z);
|
||||
// Lie flat
|
||||
shell.transform.rotation = Quaternion.Euler(90, Random.Range(0, 360), 0);
|
||||
|
||||
var renderer = shell.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = CreateShellSprite();
|
||||
renderer.sortingOrder = -39;
|
||||
shell.transform.localScale = Vector3.one * Random.Range(0.2f, 0.4f);
|
||||
}
|
||||
|
||||
// Scatter pebbles
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
float x = Random.Range(-25f, 25f);
|
||||
float z = Random.Range(-2f, 10f); // Wider range
|
||||
|
||||
var pebble = new GameObject("Pebble");
|
||||
pebble.transform.SetParent(transform);
|
||||
pebble.transform.position = new Vector3(x, -0.48f, z);
|
||||
pebble.transform.rotation = Quaternion.Euler(90, Random.Range(0, 360), 0);
|
||||
|
||||
var renderer = pebble.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = CreatePebbleSprite();
|
||||
renderer.sortingOrder = -39;
|
||||
renderer.color = new Color(0.7f, 0.7f, 0.7f);
|
||||
pebble.transform.localScale = Vector3.one * Random.Range(0.1f, 0.2f);
|
||||
}
|
||||
}
|
||||
|
||||
private Sprite CreateShellSprite()
|
||||
{
|
||||
int size = 32;
|
||||
Texture2D tex = new Texture2D(size, size);
|
||||
Color[] pixels = new Color[size*size];
|
||||
for(int i=0; i<pixels.Length; i++) pixels[i] = Color.clear;
|
||||
|
||||
Vector2 center = new Vector2(size/2, size/2);
|
||||
for(int y=0; y<size; y++){
|
||||
for(int x=0; x<size; x++){
|
||||
float dist = Vector2.Distance(new Vector2(x,y), center);
|
||||
if(dist < 12) {
|
||||
float angle = Mathf.Atan2(y-center.y, x-center.x);
|
||||
// Simple spiral or scallop shape
|
||||
float radius = 10 + Mathf.Sin(angle * 5) * 2;
|
||||
if(dist < radius)
|
||||
pixels[y*size+x] = new Color(1f, 0.95f, 0.85f);
|
||||
}
|
||||
}
|
||||
}
|
||||
tex.SetPixels(pixels);
|
||||
tex.Apply();
|
||||
return Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f));
|
||||
}
|
||||
|
||||
private Sprite CreatePebbleSprite()
|
||||
{
|
||||
int size = 16;
|
||||
Texture2D tex = new Texture2D(size, size);
|
||||
Color[] pixels = new Color[size*size];
|
||||
for(int i=0; i<pixels.Length; i++) pixels[i] = Color.clear;
|
||||
|
||||
Vector2 center = new Vector2(size/2, size/2);
|
||||
for(int y=0; y<size; y++){
|
||||
for(int x=0; x<size; x++){
|
||||
if(Vector2.Distance(new Vector2(x,y), center) < 5 + Random.Range(-1f, 1f)) {
|
||||
pixels[y*size+x] = Color.white;
|
||||
}
|
||||
}
|
||||
}
|
||||
tex.SetPixels(pixels);
|
||||
tex.Apply();
|
||||
return Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f));
|
||||
}
|
||||
|
||||
private void CreateClouds()
|
||||
{
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var cloud = new GameObject("Cloud");
|
||||
cloud.transform.SetParent(transform);
|
||||
|
||||
// Random position in sky
|
||||
float startX = Random.Range(-25f, 25f);
|
||||
float startY = Random.Range(3f, 8f);
|
||||
float depth = Random.Range(15f, 25f);
|
||||
cloud.transform.position = new Vector3(startX, startY, depth);
|
||||
|
||||
var renderer = cloud.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = CreateCloudSprite();
|
||||
renderer.sortingOrder = -90; // Behind everything but sky
|
||||
|
||||
// Random size and opacity
|
||||
float scale = Random.Range(3f, 6f);
|
||||
cloud.transform.localScale = new Vector3(scale * 1.5f, scale, 1f);
|
||||
renderer.color = new Color(1f, 1f, 1f, Random.Range(0.4f, 0.8f));
|
||||
}
|
||||
}
|
||||
|
||||
private Sprite CreateCloudSprite()
|
||||
{
|
||||
int size = 64;
|
||||
Texture2D tex = new Texture2D(size, size);
|
||||
Color[] pixels = new Color[size * size];
|
||||
|
||||
// Procedural fluffy cloud
|
||||
Vector2 center = new Vector2(size/2, size/2);
|
||||
for (int y = 0; y < size; y++)
|
||||
{
|
||||
for (int x = 0; x < size; x++)
|
||||
{
|
||||
float noise = Mathf.PerlinNoise(x * 0.15f, y * 0.15f); // Noise frequency
|
||||
float dist = Vector2.Distance(new Vector2(x, y), center) / (size * 0.4f);
|
||||
|
||||
// Soft circle with noise
|
||||
float density = Mathf.Clamp01(1f - dist);
|
||||
density *= (0.5f + noise * 0.5f);
|
||||
// Threshold for fluffiness
|
||||
density = Mathf.SmoothStep(0.2f, 0.8f, density);
|
||||
|
||||
pixels[y * size + x] = new Color(1, 1, 1, density * density);
|
||||
}
|
||||
}
|
||||
|
||||
tex.SetPixels(pixels);
|
||||
tex.Apply();
|
||||
return Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f));
|
||||
}
|
||||
|
||||
private void AnimateClouds()
|
||||
{
|
||||
// Move clouds slowly
|
||||
foreach (Transform child in transform)
|
||||
{
|
||||
if (child.name == "Cloud")
|
||||
{
|
||||
Vector3 pos = child.transform.position;
|
||||
// Wind speed depends on cloud distance for parallax
|
||||
float speed = 0.5f + (25f - pos.z) * 0.05f;
|
||||
pos.x += Time.deltaTime * speed;
|
||||
|
||||
// Wrap around
|
||||
if (pos.x > 30f) pos.x = -30f;
|
||||
|
||||
child.transform.position = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Public API
|
||||
/// <summary>
|
||||
/// Force update the environment to specific conditions.
|
||||
|
||||
317
unity-client/Assets/Scripts/Visual/VFXManager.cs
Normal file
317
unity-client/Assets/Scripts/Visual/VFXManager.cs
Normal file
@@ -0,0 +1,317 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace TheIsland.Visual
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton VFX Manager for handling particle effects.
|
||||
/// Creates procedural particle systems for gift effects.
|
||||
/// </summary>
|
||||
public class VFXManager : MonoBehaviour
|
||||
{
|
||||
#region Singleton
|
||||
private static VFXManager _instance;
|
||||
public static VFXManager Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = FindFirstObjectByType<VFXManager>();
|
||||
if (_instance == null)
|
||||
{
|
||||
var go = new GameObject("VFXManager");
|
||||
_instance = go.AddComponent<VFXManager>();
|
||||
}
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Settings
|
||||
[Header("Gold Rain Settings")]
|
||||
[SerializeField] private Color goldColor = new Color(1f, 0.84f, 0f); // Gold
|
||||
[SerializeField] private int goldParticleCount = 50;
|
||||
[SerializeField] private float goldDuration = 2f;
|
||||
|
||||
[Header("Heart Explosion Settings")]
|
||||
[SerializeField] private Color heartColor = new Color(1f, 0.2f, 0.3f); // Red/Pink
|
||||
[SerializeField] private int heartParticleCount = 30;
|
||||
[SerializeField] private float heartDuration = 1.5f;
|
||||
|
||||
[Header("General Settings")]
|
||||
[SerializeField] private float effectScale = 1f;
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
private void Awake()
|
||||
{
|
||||
if (_instance != null && _instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
_instance = this;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
/// <summary>
|
||||
/// Play gold coin rain effect at position.
|
||||
/// Used for Bits donations.
|
||||
/// </summary>
|
||||
public void PlayGoldRain(Vector3 position)
|
||||
{
|
||||
Debug.Log($"[VFXManager] Playing Gold Rain at {position}");
|
||||
var ps = CreateGoldRainSystem(position);
|
||||
ps.Play();
|
||||
Destroy(ps.gameObject, goldDuration + 0.5f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play heart explosion effect at position.
|
||||
/// Used for subscription/heart gifts.
|
||||
/// </summary>
|
||||
public void PlayHeartExplosion(Vector3 position)
|
||||
{
|
||||
Debug.Log($"[VFXManager] Playing Heart Explosion at {position}");
|
||||
var ps = CreateHeartExplosionSystem(position);
|
||||
ps.Play();
|
||||
Destroy(ps.gameObject, heartDuration + 0.5f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play an effect by type name.
|
||||
/// </summary>
|
||||
public void PlayEffect(string effectType, Vector3 position)
|
||||
{
|
||||
switch (effectType.ToLower())
|
||||
{
|
||||
case "bits":
|
||||
case "gold":
|
||||
case "goldrain":
|
||||
PlayGoldRain(position);
|
||||
break;
|
||||
case "heart":
|
||||
case "hearts":
|
||||
case "sub":
|
||||
case "subscription":
|
||||
PlayHeartExplosion(position);
|
||||
break;
|
||||
default:
|
||||
// Default to gold rain
|
||||
PlayGoldRain(position);
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Particle System Creation
|
||||
/// <summary>
|
||||
/// Create a procedural gold coin rain particle system.
|
||||
/// </summary>
|
||||
private ParticleSystem CreateGoldRainSystem(Vector3 position)
|
||||
{
|
||||
GameObject go = new GameObject("GoldRain_VFX");
|
||||
go.transform.position = position + Vector3.up * 3f; // Start above
|
||||
|
||||
ParticleSystem ps = go.AddComponent<ParticleSystem>();
|
||||
var main = ps.main;
|
||||
main.loop = false;
|
||||
main.duration = goldDuration;
|
||||
main.startLifetime = 1.5f;
|
||||
main.startSpeed = 2f;
|
||||
main.startSize = 0.15f * effectScale;
|
||||
main.startColor = goldColor;
|
||||
main.gravityModifier = 1f;
|
||||
main.maxParticles = goldParticleCount;
|
||||
main.simulationSpace = ParticleSystemSimulationSpace.World;
|
||||
|
||||
// Emission - burst at start
|
||||
var emission = ps.emission;
|
||||
emission.enabled = true;
|
||||
emission.rateOverTime = 0;
|
||||
emission.SetBursts(new ParticleSystem.Burst[]
|
||||
{
|
||||
new ParticleSystem.Burst(0f, goldParticleCount)
|
||||
});
|
||||
|
||||
// Shape - spread from point
|
||||
var shape = ps.shape;
|
||||
shape.enabled = true;
|
||||
shape.shapeType = ParticleSystemShapeType.Circle;
|
||||
shape.radius = 1f * effectScale;
|
||||
|
||||
// Size over lifetime - shrink slightly
|
||||
var sizeOverLifetime = ps.sizeOverLifetime;
|
||||
sizeOverLifetime.enabled = true;
|
||||
AnimationCurve sizeCurve = new AnimationCurve();
|
||||
sizeCurve.AddKey(0f, 1f);
|
||||
sizeCurve.AddKey(1f, 0.5f);
|
||||
sizeOverLifetime.size = new ParticleSystem.MinMaxCurve(1f, sizeCurve);
|
||||
|
||||
// Color over lifetime - fade out
|
||||
var colorOverLifetime = ps.colorOverLifetime;
|
||||
colorOverLifetime.enabled = true;
|
||||
Gradient gradient = new Gradient();
|
||||
gradient.SetKeys(
|
||||
new GradientColorKey[] {
|
||||
new GradientColorKey(goldColor, 0f),
|
||||
new GradientColorKey(goldColor, 0.7f)
|
||||
},
|
||||
new GradientAlphaKey[] {
|
||||
new GradientAlphaKey(1f, 0f),
|
||||
new GradientAlphaKey(1f, 0.5f),
|
||||
new GradientAlphaKey(0f, 1f)
|
||||
}
|
||||
);
|
||||
colorOverLifetime.color = gradient;
|
||||
|
||||
// Rotation - spin
|
||||
var rotationOverLifetime = ps.rotationOverLifetime;
|
||||
rotationOverLifetime.enabled = true;
|
||||
rotationOverLifetime.z = new ParticleSystem.MinMaxCurve(-180f, 180f);
|
||||
|
||||
// Renderer - use default sprite
|
||||
var renderer = go.GetComponent<ParticleSystemRenderer>();
|
||||
renderer.renderMode = ParticleSystemRenderMode.Billboard;
|
||||
renderer.material = CreateParticleMaterial(goldColor);
|
||||
|
||||
return ps;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a procedural heart explosion particle system.
|
||||
/// </summary>
|
||||
private ParticleSystem CreateHeartExplosionSystem(Vector3 position)
|
||||
{
|
||||
GameObject go = new GameObject("HeartExplosion_VFX");
|
||||
go.transform.position = position + Vector3.up * 1.5f;
|
||||
|
||||
ParticleSystem ps = go.AddComponent<ParticleSystem>();
|
||||
var main = ps.main;
|
||||
main.loop = false;
|
||||
main.duration = heartDuration;
|
||||
main.startLifetime = 1.2f;
|
||||
main.startSpeed = new ParticleSystem.MinMaxCurve(3f, 5f);
|
||||
main.startSize = 0.2f * effectScale;
|
||||
main.startColor = heartColor;
|
||||
main.gravityModifier = -0.3f; // Float up slightly
|
||||
main.maxParticles = heartParticleCount;
|
||||
main.simulationSpace = ParticleSystemSimulationSpace.World;
|
||||
|
||||
// Emission - burst at start
|
||||
var emission = ps.emission;
|
||||
emission.enabled = true;
|
||||
emission.rateOverTime = 0;
|
||||
emission.SetBursts(new ParticleSystem.Burst[]
|
||||
{
|
||||
new ParticleSystem.Burst(0f, heartParticleCount)
|
||||
});
|
||||
|
||||
// Shape - explode outwards from sphere
|
||||
var shape = ps.shape;
|
||||
shape.enabled = true;
|
||||
shape.shapeType = ParticleSystemShapeType.Sphere;
|
||||
shape.radius = 0.3f * effectScale;
|
||||
|
||||
// Size over lifetime - grow then shrink
|
||||
var sizeOverLifetime = ps.sizeOverLifetime;
|
||||
sizeOverLifetime.enabled = true;
|
||||
AnimationCurve sizeCurve = new AnimationCurve();
|
||||
sizeCurve.AddKey(0f, 0.5f);
|
||||
sizeCurve.AddKey(0.3f, 1.2f);
|
||||
sizeCurve.AddKey(1f, 0.2f);
|
||||
sizeOverLifetime.size = new ParticleSystem.MinMaxCurve(1f, sizeCurve);
|
||||
|
||||
// Color over lifetime - vibrant to fade
|
||||
var colorOverLifetime = ps.colorOverLifetime;
|
||||
colorOverLifetime.enabled = true;
|
||||
Gradient gradient = new Gradient();
|
||||
gradient.SetKeys(
|
||||
new GradientColorKey[] {
|
||||
new GradientColorKey(heartColor, 0f),
|
||||
new GradientColorKey(new Color(1f, 0.5f, 0.6f), 0.5f),
|
||||
new GradientColorKey(heartColor, 1f)
|
||||
},
|
||||
new GradientAlphaKey[] {
|
||||
new GradientAlphaKey(1f, 0f),
|
||||
new GradientAlphaKey(1f, 0.4f),
|
||||
new GradientAlphaKey(0f, 1f)
|
||||
}
|
||||
);
|
||||
colorOverLifetime.color = gradient;
|
||||
|
||||
// Rotation - gentle spin
|
||||
var rotationOverLifetime = ps.rotationOverLifetime;
|
||||
rotationOverLifetime.enabled = true;
|
||||
rotationOverLifetime.z = new ParticleSystem.MinMaxCurve(-90f, 90f);
|
||||
|
||||
// Renderer
|
||||
var renderer = go.GetComponent<ParticleSystemRenderer>();
|
||||
renderer.renderMode = ParticleSystemRenderMode.Billboard;
|
||||
renderer.material = CreateParticleMaterial(heartColor);
|
||||
|
||||
return ps;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a simple additive particle material.
|
||||
/// </summary>
|
||||
private Material CreateParticleMaterial(Color color)
|
||||
{
|
||||
// Use a built-in shader that works well for particles
|
||||
Shader shader = Shader.Find("Particles/Standard Unlit");
|
||||
if (shader == null)
|
||||
{
|
||||
shader = Shader.Find("Unlit/Color");
|
||||
}
|
||||
|
||||
Material mat = new Material(shader);
|
||||
mat.color = color;
|
||||
|
||||
// Enable additive blending for glow effect
|
||||
if (shader.name.Contains("Particles"))
|
||||
{
|
||||
mat.SetFloat("_Mode", 2); // Additive
|
||||
mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
|
||||
mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.One);
|
||||
}
|
||||
|
||||
// Use default particle texture
|
||||
Texture2D particleTex = CreateDefaultParticleTexture();
|
||||
mat.mainTexture = particleTex;
|
||||
|
||||
return mat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a simple circular particle texture procedurally.
|
||||
/// </summary>
|
||||
private Texture2D CreateDefaultParticleTexture()
|
||||
{
|
||||
int size = 32;
|
||||
Texture2D tex = new Texture2D(size, size, TextureFormat.RGBA32, false);
|
||||
Color[] pixels = new Color[size * size];
|
||||
|
||||
float center = size / 2f;
|
||||
float radius = size / 2f - 1;
|
||||
|
||||
for (int y = 0; y < size; y++)
|
||||
{
|
||||
for (int x = 0; x < size; x++)
|
||||
{
|
||||
float dist = Vector2.Distance(new Vector2(x, y), new Vector2(center, center));
|
||||
float alpha = Mathf.Clamp01(1f - (dist / radius));
|
||||
alpha = alpha * alpha; // Softer falloff
|
||||
pixels[y * size + x] = new Color(1f, 1f, 1f, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
tex.SetPixels(pixels);
|
||||
tex.Apply();
|
||||
return tex;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
2
unity-client/Assets/Scripts/Visual/VFXManager.cs.meta
Normal file
2
unity-client/Assets/Scripts/Visual/VFXManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c07bfc9fddc8347ea826abf2adc4d44c
|
||||
243
unity-client/Assets/Scripts/Visual/VisualEffectsManager.cs
Normal file
243
unity-client/Assets/Scripts/Visual/VisualEffectsManager.cs
Normal file
@@ -0,0 +1,243 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace TheIsland.Visual
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages environmental visual effects like fireflies, stars, and weather (Phase 19 & 20).
|
||||
/// </summary>
|
||||
public class VisualEffectsManager : MonoBehaviour
|
||||
{
|
||||
public static VisualEffectsManager Instance { get; private set; }
|
||||
|
||||
[Header("Fireflies")]
|
||||
[SerializeField] private int fireflyCount = 20;
|
||||
[SerializeField] private Color fireflyColor = new Color(1f, 1f, 0.5f, 0.8f);
|
||||
[SerializeField] private Vector2 islandBounds = new Vector2(10, 10);
|
||||
|
||||
[Header("Stars & Meteors")]
|
||||
[SerializeField] private int starCount = 50;
|
||||
[SerializeField] private Color starColor = Color.white;
|
||||
[SerializeField] private float meteorInterval = 15f;
|
||||
|
||||
[Header("Weather VFX")]
|
||||
[SerializeField] private int rainDensity = 120;
|
||||
private List<GameObject> _rainDrops = new List<GameObject>();
|
||||
private string _activeWeather = "Sunny";
|
||||
private Sprite _rainSprite;
|
||||
|
||||
private List<GameObject> _fireflies = new List<GameObject>();
|
||||
private List<GameObject> _stars = new List<GameObject>();
|
||||
private float _meteorTimer;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
_rainSprite = CreateRainSprite();
|
||||
CreateFireflies();
|
||||
CreateStars();
|
||||
CreateRain();
|
||||
}
|
||||
|
||||
public void SetWeather(string weather)
|
||||
{
|
||||
_activeWeather = weather;
|
||||
Debug.Log($"[VFXManager] Setting weather visuals for: {weather}");
|
||||
|
||||
// Toggle systems
|
||||
bool isRainy = weather == "Rainy" || weather == "Stormy";
|
||||
foreach (var drop in _rainDrops) drop.SetActive(isRainy);
|
||||
|
||||
if (weather == "Stormy") StartCoroutine(LightningLoop());
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
UpdateAtmosphere();
|
||||
UpdateRain();
|
||||
}
|
||||
|
||||
private void UpdateAtmosphere()
|
||||
{
|
||||
float t = (Time.time % 120f) / 120f;
|
||||
bool isNight = t > 0.65f || t < 0.15f;
|
||||
float starAlpha = isNight ? (t > 0.75f || t < 0.05f ? 1f : 0.5f) : 0f;
|
||||
|
||||
foreach (var star in _stars)
|
||||
{
|
||||
var sprite = star.GetComponent<SpriteRenderer>();
|
||||
sprite.color = new Color(starColor.r, starColor.g, starColor.b, starAlpha * 0.8f);
|
||||
}
|
||||
|
||||
if (isNight)
|
||||
{
|
||||
_meteorTimer += Time.deltaTime;
|
||||
if (_meteorTimer >= meteorInterval)
|
||||
{
|
||||
_meteorTimer = 0;
|
||||
if (Random.value < 0.3f) StartCoroutine(ShootMeteor());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateFireflies()
|
||||
{
|
||||
for (int i = 0; i < fireflyCount; i++)
|
||||
{
|
||||
var firefly = new GameObject($"Firefly_{i}");
|
||||
firefly.transform.SetParent(transform);
|
||||
float x = Random.Range(-islandBounds.x, islandBounds.x);
|
||||
float z = Random.Range(-islandBounds.y, islandBounds.y);
|
||||
firefly.transform.position = new Vector3(x, Random.Range(1f, 3f), z);
|
||||
|
||||
var sprite = firefly.AddComponent<SpriteRenderer>();
|
||||
sprite.sprite = CreateGlowSprite();
|
||||
sprite.color = fireflyColor;
|
||||
sprite.sortingOrder = 50;
|
||||
firefly.AddComponent<Billboard>();
|
||||
_fireflies.Add(firefly);
|
||||
StartCoroutine(AnimateFirefly(firefly));
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateStars()
|
||||
{
|
||||
for (int i = 0; i < starCount; i++)
|
||||
{
|
||||
var star = new GameObject($"Star_{i}");
|
||||
star.transform.SetParent(transform);
|
||||
float x = Random.Range(-25f, 25f);
|
||||
float y = Random.Range(10f, 15f);
|
||||
float z = Random.Range(-25f, 25f);
|
||||
star.transform.position = new Vector3(x, y, z);
|
||||
|
||||
var sprite = star.AddComponent<SpriteRenderer>();
|
||||
sprite.sprite = CreateGlowSprite();
|
||||
sprite.color = new Color(1, 1, 1, 0);
|
||||
sprite.sortingOrder = -50;
|
||||
star.AddComponent<Billboard>().ConfigureForUI();
|
||||
_stars.Add(star);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateRain()
|
||||
{
|
||||
for (int i = 0; i < rainDensity; i++)
|
||||
{
|
||||
var drop = new GameObject($"Rain_{i}");
|
||||
drop.transform.SetParent(transform);
|
||||
var sprite = drop.AddComponent<SpriteRenderer>();
|
||||
sprite.sprite = _rainSprite;
|
||||
sprite.color = new Color(0.8f, 0.9f, 1f, 0.6f);
|
||||
sprite.sortingOrder = 100;
|
||||
ResetRainDrop(drop);
|
||||
drop.SetActive(false);
|
||||
_rainDrops.Add(drop);
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetRainDrop(GameObject drop)
|
||||
{
|
||||
float x = Random.Range(-20f, 20f);
|
||||
float y = Random.Range(10f, 15f);
|
||||
float z = Random.Range(-20f, 20f);
|
||||
drop.transform.position = new Vector3(x, y, z);
|
||||
}
|
||||
|
||||
private void UpdateRain()
|
||||
{
|
||||
if (_activeWeather != "Rainy" && _activeWeather != "Stormy") return;
|
||||
float speed = _activeWeather == "Stormy" ? 40f : 20f;
|
||||
foreach (var drop in _rainDrops)
|
||||
{
|
||||
drop.transform.position += Vector3.down * speed * Time.deltaTime;
|
||||
drop.transform.position += Vector3.left * speed * 0.2f * Time.deltaTime;
|
||||
if (drop.transform.position.y < -2f) ResetRainDrop(drop);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator LightningLoop()
|
||||
{
|
||||
while (_activeWeather == "Stormy")
|
||||
{
|
||||
yield return new WaitForSeconds(Random.Range(3f, 10f));
|
||||
var flash = new GameObject("LightningFlash");
|
||||
var img = flash.AddComponent<SpriteRenderer>();
|
||||
img.sprite = CreateGlowSprite();
|
||||
img.color = new Color(1, 1, 1, 0.8f);
|
||||
flash.transform.position = new Vector3(0, 10, 0);
|
||||
flash.transform.localScale = new Vector3(100, 100, 1);
|
||||
yield return new WaitForSeconds(0.05f);
|
||||
img.color = new Color(1, 1, 1, 0.3f);
|
||||
yield return new WaitForSeconds(0.05f);
|
||||
Destroy(flash);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator ShootMeteor()
|
||||
{
|
||||
var meteor = new GameObject("Meteor");
|
||||
meteor.transform.SetParent(transform);
|
||||
Vector3 startPos = new Vector3(Random.Range(-20, 20), 15, Random.Range(-20, 20));
|
||||
Vector3 direction = new Vector3(Random.Range(-10, 10), -5, Random.Range(-10, 10)).normalized;
|
||||
meteor.transform.position = startPos;
|
||||
var sprite = meteor.AddComponent<SpriteRenderer>();
|
||||
sprite.sprite = CreateGlowSprite();
|
||||
sprite.color = Color.white;
|
||||
sprite.transform.localScale = new Vector3(0.5f, 0.1f, 1f);
|
||||
float duration = 1.0f;
|
||||
float elapsed = 0;
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
meteor.transform.position += direction * 30f * Time.deltaTime;
|
||||
sprite.color = new Color(1, 1, 1, 1f - (elapsed / duration));
|
||||
yield return null;
|
||||
}
|
||||
Destroy(meteor);
|
||||
}
|
||||
|
||||
private Sprite CreateRainSprite()
|
||||
{
|
||||
Texture2D tex = new Texture2D(2, 8);
|
||||
for (int y = 0; y < 8; y++)
|
||||
for (int x = 0; x < 2; x++) tex.SetPixel(x, y, Color.white);
|
||||
tex.Apply();
|
||||
return Sprite.Create(tex, new Rect(0, 0, 2, 8), new Vector2(0.5f, 0.5f));
|
||||
}
|
||||
|
||||
private Sprite CreateGlowSprite()
|
||||
{
|
||||
int size = 32;
|
||||
Texture2D tex = new Texture2D(size, size);
|
||||
tex.filterMode = FilterMode.Bilinear;
|
||||
for (int y = 0; y < size; y++)
|
||||
for (int x = 0; x < size; x++) {
|
||||
float dist = Vector2.Distance(new Vector2(x, y), new Vector2(size/2f, size/2f)) / (size/2f);
|
||||
float alpha = Mathf.Exp(-dist * 4f);
|
||||
tex.SetPixel(x, y, new Color(1, 1, 1, alpha));
|
||||
}
|
||||
tex.Apply();
|
||||
return Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f));
|
||||
}
|
||||
|
||||
private IEnumerator AnimateFirefly(GameObject firefly)
|
||||
{
|
||||
Vector3 startPos = firefly.transform.position;
|
||||
float seed = Random.value * 100f;
|
||||
while (true)
|
||||
{
|
||||
float t = Time.time + seed;
|
||||
float dx = (Mathf.PerlinNoise(t * 0.2f, 0) - 0.5f) * 2f;
|
||||
float dy = (Mathf.PerlinNoise(0, t * 0.2f) - 0.5f) * 1f;
|
||||
float dz = (Mathf.PerlinNoise(t * 0.1f, t * 0.1f) - 0.5f) * 2f;
|
||||
firefly.transform.position = startPos + new Vector3(dx, dy, dz);
|
||||
var sprite = firefly.GetComponent<SpriteRenderer>();
|
||||
float pulse = Mathf.PingPong(t, 1f) * 0.5f + 0.5f;
|
||||
sprite.color = new Color(fireflyColor.r, fireflyColor.g, fireflyColor.b, pulse * fireflyColor.a);
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 47dcba28f3d78421ca5bdf74a4365bed
|
||||
8
unity-client/Assets/Shaders.meta
Normal file
8
unity-client/Assets/Shaders.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 45d3ecb46bd4b4019847fcced069b50f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
81
unity-client/Assets/Shaders/CartoonWater.shader
Normal file
81
unity-client/Assets/Shaders/CartoonWater.shader
Normal file
@@ -0,0 +1,81 @@
|
||||
Shader "TheIsland/CartoonWater"
|
||||
{
|
||||
Properties
|
||||
{
|
||||
_MainColor ("Water Color", Color) = (0.2, 0.5, 0.9, 0.8)
|
||||
_FoamColor ("Foam Color", Color) = (1, 1, 1, 1)
|
||||
_WaveSpeed ("Wave Speed", Range(0, 5)) = 1.0
|
||||
_WaveHeight ("Wave Height", Range(0, 1)) = 0.1
|
||||
_FoamAmount ("Foam Amount", Range(0, 1)) = 0.1
|
||||
}
|
||||
SubShader
|
||||
{
|
||||
Tags { "RenderType"="Transparent" "Queue"="Transparent" }
|
||||
LOD 100
|
||||
Blend SrcAlpha OneMinusSrcAlpha
|
||||
ZWrite Off
|
||||
|
||||
Pass
|
||||
{
|
||||
CGPROGRAM
|
||||
#pragma vertex vert
|
||||
#pragma fragment frag
|
||||
#include "UnityCG.cginc"
|
||||
|
||||
struct appdata
|
||||
{
|
||||
float4 vertex : POSITION;
|
||||
float2 uv : TEXCOORD0;
|
||||
};
|
||||
|
||||
struct v2f
|
||||
{
|
||||
float2 uv : TEXCOORD0;
|
||||
float4 vertex : SV_POSITION;
|
||||
float3 worldPos : TEXCOORD1;
|
||||
};
|
||||
|
||||
fixed4 _MainColor;
|
||||
fixed4 _FoamColor;
|
||||
float _WaveSpeed;
|
||||
float _WaveHeight;
|
||||
float _FoamAmount;
|
||||
|
||||
v2f vert (appdata v)
|
||||
{
|
||||
v2f o;
|
||||
// Simple vertex displacement wave
|
||||
float wave = sin(_Time.y * _WaveSpeed + v.vertex.x * 2.0) * _WaveHeight;
|
||||
v.vertex.y += wave;
|
||||
|
||||
o.vertex = UnityObjectToClipPos(v.vertex);
|
||||
o.uv = v.uv;
|
||||
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
|
||||
return o;
|
||||
}
|
||||
|
||||
fixed4 frag (v2f i) : SV_Target
|
||||
{
|
||||
// Moving foam texture simulation using noise
|
||||
float2 uv1 = i.uv + float2(_Time.x * 0.1, _Time.x * 0.05);
|
||||
float noise = frac(sin(dot(uv1, float2(12.9898, 78.233))) * 43758.5453);
|
||||
|
||||
// 1. Horizon/Wave Foam (Top)
|
||||
float waveFoam = step(1.0 - _FoamAmount - (noise * 0.05), i.uv.y);
|
||||
|
||||
// 2. Shoreline Foam (Bottom)
|
||||
// Sine wave for "tide" effect
|
||||
float tide = sin(_Time.y * 1.5) * 0.05;
|
||||
float shoreThreshold = 0.05 + tide + (noise * 0.02);
|
||||
float shoreFoam = step(i.uv.y, shoreThreshold);
|
||||
|
||||
// Combine foam
|
||||
float totalFoam = max(waveFoam, shoreFoam);
|
||||
|
||||
fixed4 col = lerp(_MainColor, _FoamColor, totalFoam);
|
||||
return col;
|
||||
}
|
||||
ENDCG
|
||||
}
|
||||
}
|
||||
}
|
||||
9
unity-client/Assets/Shaders/CartoonWater.shader.meta
Normal file
9
unity-client/Assets/Shaders/CartoonWater.shader.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f82a9056a602b4fcd9b86f52d2c43c9a
|
||||
ShaderImporter:
|
||||
externalObjects: {}
|
||||
defaultTextures: []
|
||||
nonModifiableTextures: []
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
unity-client/Assets/Sprites/Characters.png
Normal file
BIN
unity-client/Assets/Sprites/Characters.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 357 KiB |
143
unity-client/Assets/Sprites/Characters.png.meta
Normal file
143
unity-client/Assets/Sprites/Characters.png.meta
Normal file
@@ -0,0 +1,143 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2976a69bb8c114901b263e613a7b6006
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: WebGL
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: iOS
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
unity-client/Assets/Sprites/Environment.png
Normal file
BIN
unity-client/Assets/Sprites/Environment.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 632 KiB |
143
unity-client/Assets/Sprites/Environment.png.meta
Normal file
143
unity-client/Assets/Sprites/Environment.png.meta
Normal file
@@ -0,0 +1,143 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ea1b9777c2a54dea9d8f4fb46a00d03
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: WebGL
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: iOS
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because one or more lines are too long
@@ -487,6 +487,57 @@ Transform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &2108464767
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 2108464769}
|
||||
- component: {fileID: 2108464768}
|
||||
m_Layer: 0
|
||||
m_Name: VFXManager
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &2108464768
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2108464767}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: c07bfc9fddc8347ea826abf2adc4d44c, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TheIsland.Visual.VFXManager
|
||||
goldColor: {r: 1, g: 0.84, b: 0, a: 1}
|
||||
goldParticleCount: 50
|
||||
goldDuration: 2
|
||||
heartColor: {r: 1, g: 0.2, b: 0.3, a: 1}
|
||||
heartParticleCount: 30
|
||||
heartDuration: 1.5
|
||||
effectScale: 1
|
||||
--- !u!4 &2108464769
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2108464767}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1660057539 &9223372036854775807
|
||||
SceneRoots:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -497,3 +548,4 @@ SceneRoots:
|
||||
- {fileID: 851065944}
|
||||
- {fileID: 1562380643}
|
||||
- {fileID: 318018868}
|
||||
- {fileID: 2108464769}
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"com.unity.modules.video": "1.0.0",
|
||||
"com.unity.modules.vr": "1.0.0",
|
||||
"com.unity.modules.wind": "1.0.0",
|
||||
"com.unity.modules.xr": "1.0.0"
|
||||
"com.unity.modules.xr": "1.0.0",
|
||||
"com.unity.postprocessing": "3.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,6 +276,15 @@
|
||||
"dependencies": {},
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.postprocessing": {
|
||||
"version": "3.4.0",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
"com.unity.modules.physics": "1.0.0"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.serialization": {
|
||||
"version": "3.1.3",
|
||||
"depth": 1,
|
||||
|
||||
@@ -682,7 +682,22 @@ PlayerSettings:
|
||||
webWasm2023: 0
|
||||
webEnableSubmoduleStrippingCompatibility: 0
|
||||
scriptingDefineSymbols:
|
||||
Standalone: APP_UI_EDITOR_ONLY
|
||||
Android: UNITY_POST_PROCESSING_STACK_V2
|
||||
EmbeddedLinux: UNITY_POST_PROCESSING_STACK_V2
|
||||
GameCoreScarlett: UNITY_POST_PROCESSING_STACK_V2
|
||||
GameCoreXboxOne: UNITY_POST_PROCESSING_STACK_V2
|
||||
Kepler: UNITY_POST_PROCESSING_STACK_V2
|
||||
LinuxHeadlessSimulation: UNITY_POST_PROCESSING_STACK_V2
|
||||
Nintendo Switch: UNITY_POST_PROCESSING_STACK_V2
|
||||
Nintendo Switch 2: UNITY_POST_PROCESSING_STACK_V2
|
||||
PS4: UNITY_POST_PROCESSING_STACK_V2
|
||||
PS5: UNITY_POST_PROCESSING_STACK_V2
|
||||
QNX: UNITY_POST_PROCESSING_STACK_V2
|
||||
Standalone: APP_UI_EDITOR_ONLY;UNITY_POST_PROCESSING_STACK_V2
|
||||
VisionOS: UNITY_POST_PROCESSING_STACK_V2
|
||||
WebGL: UNITY_POST_PROCESSING_STACK_V2
|
||||
XboxOne: UNITY_POST_PROCESSING_STACK_V2
|
||||
tvOS: UNITY_POST_PROCESSING_STACK_V2
|
||||
additionalCompilerArguments: {}
|
||||
platformArchitecture: {}
|
||||
scriptingBackend: {}
|
||||
|
||||
Reference in New Issue
Block a user