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 数据模型
|
│ ├── models.py # SQLAlchemy 数据模型
|
||||||
│ ├── schemas.py # Pydantic 消息模式
|
│ ├── schemas.py # Pydantic 消息模式
|
||||||
│ ├── llm.py # LLM 集成 (对话生成)
|
│ ├── llm.py # LLM 集成 (对话生成)
|
||||||
|
│ ├── memory_service.py # Agent 记忆管理服务
|
||||||
│ ├── twitch_service.py # Twitch 聊天机器人
|
│ ├── twitch_service.py # Twitch 聊天机器人
|
||||||
│ └── database.py # 数据库配置
|
│ └── database.py # 数据库配置
|
||||||
├── frontend/ # Web 调试客户端
|
├── 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 连接管理、消息收发 |
|
| `NetworkManager.cs` | WebSocket 连接管理、消息收发 |
|
||||||
| `GameManager.cs` | 游戏状态管理、角色生成 |
|
| `GameManager.cs` | 游戏状态管理、角色生成、行动系统 |
|
||||||
| `UIManager.cs` | 主 UI 界面 (顶部状态栏、底部命令输入) |
|
| `UIManager.cs` | 主 UI 界面 (顶部状态栏、底部命令输入) |
|
||||||
| `EventLog.cs` | 事件日志面板 (显示游戏事件) |
|
| `EventLog.cs` | 事件日志面板 (显示游戏事件) |
|
||||||
| `AgentVisual.cs` | 角色视觉组件 (精灵、血条、对话框) |
|
| `AgentVisual.cs` | 角色视觉组件 (精灵、血条、对话框、状态图标) |
|
||||||
| `EnvironmentManager.cs` | 环境场景 (沙滩、海洋、天空) |
|
| `EnvironmentManager.cs` | 环境场景 (沙滩、海洋、天空) |
|
||||||
| `WeatherEffects.cs` | 天气粒子效果 (雨、雾、热浪) |
|
| `WeatherEffects.cs` | 天气粒子效果 (雨、雾、热浪) |
|
||||||
|
| `Models.cs` | 数据模型 (Agent、WorldState、事件数据) |
|
||||||
|
|
||||||
### 视觉特性
|
### 视觉特性
|
||||||
- 程序化生成的 2.5D 角色精灵
|
- 程序化生成的 2.5D 角色精灵
|
||||||
@@ -147,10 +167,17 @@ HEAL # 治疗反馈
|
|||||||
TALK # 对话反馈
|
TALK # 对话反馈
|
||||||
ENCOURAGE # 鼓励反馈
|
ENCOURAGE # 鼓励反馈
|
||||||
REVIVE # 复活反馈
|
REVIVE # 复活反馈
|
||||||
|
GIFT_EFFECT # Bits 打赏特效
|
||||||
|
|
||||||
# 社交系统
|
# 社交系统
|
||||||
SOCIAL_INTERACTION # 角色间社交
|
SOCIAL_INTERACTION # 角色间社交
|
||||||
AUTO_REVIVE # 自动复活 (休闲模式)
|
AUTO_REVIVE # 自动复活 (休闲模式)
|
||||||
|
|
||||||
|
# 自主行动系统 (Phase 13+)
|
||||||
|
AGENT_ACTION # 角色执行行动 (采集/休息/社交等)
|
||||||
|
CRAFT # 制作物品 (药品等)
|
||||||
|
USE_ITEM # 使用物品
|
||||||
|
RANDOM_EVENT # 随机事件 (风暴/宝藏/野兽等)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Twitch 直播集成
|
## Twitch 直播集成
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from .schemas import GameEvent, EventType
|
|||||||
from .database import init_db, get_db_session
|
from .database import init_db, get_db_session
|
||||||
from .models import User, Agent, WorldState, GameConfig, AgentRelationship
|
from .models import User, Agent, WorldState, GameConfig, AgentRelationship
|
||||||
from .llm import llm_service
|
from .llm import llm_service
|
||||||
|
from .memory_service import memory_service
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .server import ConnectionManager
|
from .server import ConnectionManager
|
||||||
@@ -214,6 +215,35 @@ class GameEngine:
|
|||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Day/Night cycle (Phase 2)
|
# 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]:
|
async def _advance_time(self) -> Optional[dict]:
|
||||||
"""Advance time and return phase change info if applicable."""
|
"""Advance time and return phase change info if applicable."""
|
||||||
with get_db_session() as db:
|
with get_db_session() as db:
|
||||||
@@ -228,9 +258,14 @@ class GameEngine:
|
|||||||
if world.current_tick_in_day >= TICKS_PER_DAY:
|
if world.current_tick_in_day >= TICKS_PER_DAY:
|
||||||
world.current_tick_in_day = 0
|
world.current_tick_in_day = 0
|
||||||
world.day_count += 1
|
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, {
|
await self._broadcast_event(EventType.DAY_CHANGE, {
|
||||||
"day": world.day_count,
|
"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
|
# Determine current phase
|
||||||
@@ -299,6 +334,55 @@ class GameEngine:
|
|||||||
else:
|
else:
|
||||||
agent.mood_state = "anxious"
|
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
|
# Survival mechanics
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -315,6 +399,45 @@ class GameEngine:
|
|||||||
alive_agents = db.query(Agent).filter(Agent.status == "Alive").all()
|
alive_agents = db.query(Agent).filter(Agent.status == "Alive").all()
|
||||||
|
|
||||||
for agent in alive_agents:
|
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
|
# Calculate energy decay with all modifiers
|
||||||
base_decay = BASE_ENERGY_DECAY_PER_TICK
|
base_decay = BASE_ENERGY_DECAY_PER_TICK
|
||||||
decay = base_decay * config.energy_decay_multiplier
|
decay = base_decay * config.energy_decay_multiplier
|
||||||
@@ -323,9 +446,9 @@ class GameEngine:
|
|||||||
|
|
||||||
agent.energy = max(0, agent.energy - int(decay))
|
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)
|
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)
|
agent.hp = min(100, agent.hp + hp_recovery)
|
||||||
|
|
||||||
# Starvation damage
|
# Starvation damage
|
||||||
@@ -337,6 +460,9 @@ class GameEngine:
|
|||||||
if agent.hp <= 0:
|
if agent.hp <= 0:
|
||||||
agent.status = "Dead"
|
agent.status = "Dead"
|
||||||
agent.death_tick = self._tick_count
|
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})
|
deaths.append({"name": agent.name, "personality": agent.personality})
|
||||||
logger.info(f"Agent {agent.name} has died!")
|
logger.info(f"Agent {agent.name} has died!")
|
||||||
|
|
||||||
@@ -347,6 +473,18 @@ class GameEngine:
|
|||||||
"message": f"{death['name']} ({death['personality']}) has died..."
|
"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:
|
async def _process_auto_revive(self) -> None:
|
||||||
"""Auto-revive dead agents in casual mode."""
|
"""Auto-revive dead agents in casual mode."""
|
||||||
config = self._get_config()
|
config = self._get_config()
|
||||||
@@ -497,6 +635,237 @@ class GameEngine:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in social dialogue: {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
|
# LLM-powered agent speech
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -648,12 +1017,19 @@ class GameEngine:
|
|||||||
|
|
||||||
user.gold -= HEAL_COST
|
user.gold -= HEAL_COST
|
||||||
old_hp = agent.hp
|
old_hp = agent.hp
|
||||||
|
was_sick = agent.is_sick
|
||||||
|
|
||||||
agent.hp = min(100, agent.hp + HEAL_HP_RESTORE)
|
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, {
|
await self._broadcast_event(EventType.HEAL, {
|
||||||
"user": username, "agent_name": agent.name,
|
"user": username, "agent_name": agent.name,
|
||||||
"hp_restored": agent.hp - old_hp, "agent_hp": agent.hp,
|
"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})
|
await self._broadcast_event(EventType.USER_UPDATE, {"user": username, "gold": user.gold})
|
||||||
|
|
||||||
@@ -848,6 +1224,77 @@ class GameEngine:
|
|||||||
await self._handle_reset(user)
|
await self._handle_reset(user)
|
||||||
return
|
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
|
# Game loop
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -888,9 +1335,19 @@ class GameEngine:
|
|||||||
# 5. Update moods (Phase 3)
|
# 5. Update moods (Phase 3)
|
||||||
await self._update_moods()
|
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()
|
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
|
# 7. Idle chat
|
||||||
with get_db_session() as db:
|
with get_db_session() as db:
|
||||||
alive_count = db.query(Agent).filter(Agent.status == "Alive").count()
|
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
|
# Use the existing process_comment method to handle commands
|
||||||
await self.process_comment(user, text)
|
await self.process_comment(user, text)
|
||||||
|
|
||||||
async def process_bits(self, user: str, amount: int) -> None:
|
async def handle_gift(self, user: str, amount: int, gift_type: str = "bits") -> None:
|
||||||
"""Process Twitch bits and convert to game gold."""
|
"""
|
||||||
# 1 Bit = 1 Gold conversion rate
|
Handle a gift/donation (bits, subscription, or test).
|
||||||
gold_added = amount
|
|
||||||
|
|
||||||
|
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:
|
with get_db_session() as db:
|
||||||
user_obj = self._get_or_create_user(db, user)
|
user_obj = self._get_or_create_user(db, user)
|
||||||
user_obj.gold += gold_added
|
user_obj.gold += gold_added
|
||||||
@@ -960,14 +1423,49 @@ class GameEngine:
|
|||||||
await self._broadcast_event(EventType.USER_UPDATE, {
|
await self._broadcast_event(EventType.USER_UPDATE, {
|
||||||
"user": user,
|
"user": user,
|
||||||
"gold": user_obj.gold,
|
"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
|
# Check for alive agents for reaction
|
||||||
await self._broadcast_event("bits_received", {
|
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,
|
"user": user,
|
||||||
"bits": amount,
|
"gift_type": gift_type,
|
||||||
"gold": gold_added
|
"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 bits: {user} -> {amount} bits -> {gold_added} gold")
|
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:
|
if TYPE_CHECKING:
|
||||||
from .models import Agent
|
from .models import Agent
|
||||||
|
from .memory_service import MemoryService
|
||||||
|
|
||||||
|
from .memory_service import memory_service
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -56,6 +59,24 @@ MOCK_REACTIONS = {
|
|||||||
"My stomach is eating itself...",
|
"My stomach is eating itself...",
|
||||||
"Is this how it ends? Starving on a beach?",
|
"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
|
# Default model configuration
|
||||||
@@ -182,11 +203,16 @@ class LLMService:
|
|||||||
return self._get_mock_response(event_type)
|
return self._get_mock_response(event_type)
|
||||||
|
|
||||||
try:
|
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 = (
|
system_prompt = (
|
||||||
f"You are {agent.name}. "
|
f"You are {agent.name}. "
|
||||||
f"Personality: {agent.personality}. "
|
f"Personality: {agent.personality}. "
|
||||||
f"Current Status: HP={agent.hp}, Energy={agent.energy}. "
|
f"Current Status: HP={agent.hp}, Energy={agent.energy}. "
|
||||||
f"You live on a survival island. "
|
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"React to the following event briefly (under 20 words). "
|
||||||
f"Respond in first person, as if speaking out loud."
|
f"Respond in first person, as if speaking out loud."
|
||||||
)
|
)
|
||||||
@@ -329,10 +355,15 @@ class LLMService:
|
|||||||
"calm and neutral" if agent_mood >= 40 else \
|
"calm and neutral" if agent_mood >= 40 else \
|
||||||
"a bit down" if agent_mood >= 20 else "anxious and worried"
|
"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 = (
|
system_prompt = (
|
||||||
f"You are {agent_name}, a survivor on a deserted island. "
|
f"You are {agent_name}, a survivor on a deserted island. "
|
||||||
f"Personality: {agent_personality}. "
|
f"Personality: {agent_personality}. "
|
||||||
f"Current mood: {mood_desc} (mood level: {agent_mood}/100). "
|
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"A viewer named {username} wants to chat with you. "
|
||||||
f"Respond naturally in character (under 30 words). "
|
f"Respond naturally in character (under 30 words). "
|
||||||
f"Be conversational and show your personality."
|
f"Be conversational and show your personality."
|
||||||
@@ -461,6 +492,92 @@ class LLMService:
|
|||||||
logger.error(f"LLM API error for social interaction: {e}")
|
logger.error(f"LLM API error for social interaction: {e}")
|
||||||
return f"{initiator_name}: ...\n{target_name}: ..."
|
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
|
# Global instance for easy import
|
||||||
llm_service = LLMService()
|
llm_service = LLMService()
|
||||||
|
|||||||
@@ -140,6 +140,13 @@ async def websocket_endpoint(websocket: WebSocket):
|
|||||||
text = message.payload.get("message", "")
|
text = message.payload.get("message", "")
|
||||||
await engine.process_comment(user, text)
|
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:
|
except WebSocketDisconnect:
|
||||||
manager.disconnect(websocket)
|
manager.disconnect(websocket)
|
||||||
except Exception as e:
|
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 attributes (Phase 5)
|
||||||
social_tendency = Column(String(20), default="neutral") # introvert, extrovert, neutral
|
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):
|
def __repr__(self):
|
||||||
return f"<Agent {self.name} ({self.personality}) HP={self.hp} Energy={self.energy} Mood={self.mood}>"
|
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,
|
"inventory": self.inventory,
|
||||||
"mood": self.mood,
|
"mood": self.mood,
|
||||||
"mood_state": self.mood_state,
|
"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 system (Phase 3)
|
||||||
weather_duration = Column(Integer, default=0) # Ticks since last weather change
|
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):
|
def __repr__(self):
|
||||||
return f"<WorldState Day={self.day_count} {self.time_of_day} Weather={self.weather}>"
|
return f"<WorldState Day={self.day_count} {self.time_of_day} Weather={self.weather}>"
|
||||||
|
|
||||||
@@ -104,7 +125,9 @@ class WorldState(Base):
|
|||||||
"weather": self.weather,
|
"weather": self.weather,
|
||||||
"resource_level": self.resource_level,
|
"resource_level": self.resource_level,
|
||||||
"current_tick_in_day": self.current_tick_in_day,
|
"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"
|
self.relationship_type = "friend"
|
||||||
else:
|
else:
|
||||||
self.relationship_type = "close_friend"
|
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
|
RELATIONSHIP_CHANGE = "relationship_change" # Relationship status changed
|
||||||
AUTO_REVIVE = "auto_revive" # Agent auto-revived (casual mode)
|
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):
|
class GameEvent(BaseModel):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Compatible with twitchio 2.x (IRC-based)
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import random
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from twitchio.ext import commands
|
from twitchio.ext import commands
|
||||||
@@ -83,20 +84,19 @@ class TwitchBot(commands.Bot):
|
|||||||
if hasattr(message, 'tags') and message.tags:
|
if hasattr(message, 'tags') and message.tags:
|
||||||
bits = message.tags.get('bits')
|
bits = message.tags.get('bits')
|
||||||
if bits:
|
if bits:
|
||||||
try:
|
await self._handle_bits(username, int(bits))
|
||||||
bits_amount = int(bits)
|
|
||||||
logger.info(f"Received {bits_amount} bits from {username}")
|
|
||||||
await self._game_engine.process_bits(username, bits_amount)
|
|
||||||
|
|
||||||
# Send special gift effect to Unity
|
async def _handle_bits(self, username: str, bits_amount: int):
|
||||||
await self._game_engine._broadcast_event("gift_effect", {
|
"""
|
||||||
"type": "gift_effect",
|
Handle bits donation.
|
||||||
"user": username,
|
Delegates to game engine's unified gift handling.
|
||||||
"value": bits_amount,
|
"""
|
||||||
"message": f"{username} cheered {bits_amount} bits!"
|
try:
|
||||||
})
|
logger.info(f"Received {bits_amount} bits from {username}")
|
||||||
except (ValueError, TypeError) as e:
|
await self._game_engine.handle_gift(username, bits_amount, "bits")
|
||||||
logger.error(f"Error parsing bits amount: {e}")
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error handling bits: {e}")
|
||||||
|
|
||||||
async def event_command_error(self, context, error):
|
async def event_command_error(self, context, error):
|
||||||
"""Called when a command error occurs."""
|
"""Called when a command error occurs."""
|
||||||
@@ -110,3 +110,4 @@ class TwitchBot(commands.Bot):
|
|||||||
logger.error(f"Twitch bot error: {error}")
|
logger.error(f"Twitch bot error: {error}")
|
||||||
if data:
|
if data:
|
||||||
logger.debug(f"Error data: {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.sizeDelta = size;
|
||||||
rect.anchoredPosition = Vector2.zero;
|
rect.anchoredPosition = Vector2.zero;
|
||||||
|
|
||||||
// Semi-transparent background
|
// Semi-transparent glass background
|
||||||
var bg = panel.AddComponent<Image>();
|
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;
|
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,
|
private TextMeshProUGUI CreateText(GameObject parent, string name, string text,
|
||||||
float fontSize, Color color, FontStyles style = FontStyles.Normal)
|
float fontSize, Color color, FontStyles style = FontStyles.Normal)
|
||||||
{
|
{
|
||||||
@@ -296,25 +330,11 @@ namespace TheIsland.UI
|
|||||||
{
|
{
|
||||||
_currentData = data;
|
_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)
|
if (_hpText != null)
|
||||||
{
|
{
|
||||||
_hpText.text = $"HP: {data.hp}";
|
_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)
|
if (_energyText != null)
|
||||||
{
|
{
|
||||||
_energyText.text = $"Energy: {data.energy}";
|
_energyText.text = $"Energy: {data.energy}";
|
||||||
|
|||||||
@@ -64,18 +64,33 @@ namespace TheIsland.Visual
|
|||||||
private Billboard _spriteBillboard;
|
private Billboard _spriteBillboard;
|
||||||
private Billboard _uiBillboard;
|
private Billboard _uiBillboard;
|
||||||
private Camera _mainCamera;
|
private Camera _mainCamera;
|
||||||
|
private AgentAnimator _animator;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region State
|
#region State
|
||||||
private int _agentId;
|
private int _agentId;
|
||||||
private AgentData _currentData;
|
private AgentData _currentData;
|
||||||
|
private string _moodState = "neutral";
|
||||||
private Coroutine _speechCoroutine;
|
private Coroutine _speechCoroutine;
|
||||||
|
|
||||||
// Animation state
|
// Animation state
|
||||||
private float _idleAnimTimer;
|
private float _idleAnimTimer;
|
||||||
private float _breathScale = 1f;
|
|
||||||
private Vector3 _originalSpriteScale;
|
private Vector3 _originalSpriteScale;
|
||||||
private float _bobOffset;
|
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
|
#endregion
|
||||||
|
|
||||||
#region Properties
|
#region Properties
|
||||||
@@ -88,33 +103,202 @@ namespace TheIsland.Visual
|
|||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
_mainCamera = Camera.main;
|
_mainCamera = Camera.main;
|
||||||
|
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||||
|
|
||||||
|
// Phase 19-B: Ensure AgentAnimator is present
|
||||||
|
_animator = GetComponent<AgentAnimator>();
|
||||||
|
if (_animator == null) _animator = gameObject.AddComponent<AgentAnimator>();
|
||||||
|
|
||||||
CreateVisuals();
|
CreateVisuals();
|
||||||
|
_lastPosition = transform.position;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (!IsAlive) return;
|
if (!IsAlive) return;
|
||||||
|
|
||||||
// Idle breathing animation
|
// Phase 19-D: Apply soft-repulsion to prevent crowding
|
||||||
_idleAnimTimer += Time.deltaTime;
|
Vector3 repulsion = CalculateRepulsion();
|
||||||
_breathScale = 1f + Mathf.Sin(_idleAnimTimer * 2f) * 0.02f;
|
|
||||||
|
|
||||||
// Gentle bobbing
|
// Handle Movement
|
||||||
_bobOffset = Mathf.Sin(_idleAnimTimer * 1.5f) * 0.05f;
|
if (_isMoving)
|
||||||
|
|
||||||
if (_spriteRenderer != null && _originalSpriteScale != Vector3.zero)
|
|
||||||
{
|
{
|
||||||
// Apply breathing scale
|
// Simple steering toward target
|
||||||
_spriteRenderer.transform.localScale = new Vector3(
|
Vector3 moveDir = (_targetPosition - transform.position).normalized;
|
||||||
_originalSpriteScale.x * _breathScale,
|
Vector3 finalVelocity = (moveDir * _moveSpeed) + repulsion;
|
||||||
_originalSpriteScale.y * _breathScale,
|
|
||||||
_originalSpriteScale.z
|
transform.position += finalVelocity * Time.deltaTime;
|
||||||
);
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
// Apply bobbing
|
|
||||||
var pos = _spriteRenderer.transform.localPosition;
|
var pos = _spriteRenderer.transform.localPosition;
|
||||||
pos.y = 1f + _bobOffset;
|
pos.y = startPos.y + height;
|
||||||
_spriteRenderer.transform.localPosition = pos;
|
_spriteRenderer.transform.localPosition = pos;
|
||||||
|
yield return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +322,10 @@ namespace TheIsland.Visual
|
|||||||
_currentData = data;
|
_currentData = data;
|
||||||
gameObject.name = $"Agent_{data.id}_{data.name}";
|
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);
|
ApplyAgentColor(data.id);
|
||||||
|
|
||||||
// Set UI text
|
// Set UI text
|
||||||
@@ -149,6 +336,85 @@ namespace TheIsland.Visual
|
|||||||
Debug.Log($"[AgentVisual] Initialized: {data.name}");
|
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)
|
private void ApplyAgentColor(int agentId)
|
||||||
{
|
{
|
||||||
// Generate unique color per agent
|
// 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++)
|
for (int x = (int)(center.x - smileWidth); x <= (int)(center.x + smileWidth); x++)
|
||||||
{
|
{
|
||||||
float t = (x - center.x + smileWidth) / (smileWidth * 2);
|
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)
|
if (x >= 0 && x < width && y >= 0 && y < height)
|
||||||
{
|
{
|
||||||
pixels[y * width + x] = mouthColor;
|
pixels[y * width + x] = mouthColor;
|
||||||
@@ -602,28 +888,23 @@ namespace TheIsland.Visual
|
|||||||
rect.anchoredPosition = Vector2.zero;
|
rect.anchoredPosition = Vector2.zero;
|
||||||
|
|
||||||
var bg = panel.AddComponent<Image>();
|
var bg = panel.AddComponent<Image>();
|
||||||
bg.sprite = CreateRoundedRectSprite(32, 32, 8);
|
bg.sprite = CreateRoundedRectSprite(32, 32, 12);
|
||||||
bg.type = Image.Type.Sliced;
|
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");
|
var borderObj = new GameObject("Border");
|
||||||
borderObj.transform.SetParent(panel.transform);
|
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>();
|
var borderRect = borderObj.AddComponent<RectTransform>();
|
||||||
borderRect.anchorMin = Vector2.zero;
|
borderRect.anchorMin = Vector2.zero;
|
||||||
borderRect.anchorMax = Vector2.one;
|
borderRect.anchorMax = Vector2.one;
|
||||||
borderRect.offsetMin = new Vector2(-2, -2);
|
borderRect.offsetMin = new Vector2(1, 1);
|
||||||
borderRect.offsetMax = new Vector2(2, 2);
|
borderRect.offsetMax = new Vector2(-1, -1);
|
||||||
borderRect.SetAsFirstSibling();
|
|
||||||
|
|
||||||
var borderImg = borderObj.AddComponent<Image>();
|
var borderImg = borderObj.AddComponent<Image>();
|
||||||
borderImg.sprite = CreateRoundedRectSprite(32, 32, 8);
|
borderImg.sprite = CreateRoundedRectSprite(32, 32, 12);
|
||||||
borderImg.type = Image.Type.Sliced;
|
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;
|
return panel;
|
||||||
}
|
}
|
||||||
@@ -818,36 +1099,30 @@ namespace TheIsland.Visual
|
|||||||
{
|
{
|
||||||
_currentData = data;
|
_currentData = data;
|
||||||
|
|
||||||
// Update HP bar
|
// Set targets for smooth lerping (Phase 19)
|
||||||
float hpPercent = data.hp / 100f;
|
_targetHpPercent = data.hp / 100f;
|
||||||
if (_hpBarFill != null)
|
_targetEnergyPercent = data.energy / 100f;
|
||||||
{
|
_targetMoodPercent = data.mood / 100f;
|
||||||
_hpBarFill.rectTransform.anchorMax = new Vector2(hpPercent, 1);
|
|
||||||
_hpBarFill.color = Color.Lerp(hpLowColor, hpHighColor, hpPercent);
|
|
||||||
}
|
|
||||||
if (_hpText != null)
|
if (_hpText != null)
|
||||||
{
|
{
|
||||||
_hpText.text = $"HP: {data.hp}";
|
_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)
|
if (_energyText != null)
|
||||||
{
|
{
|
||||||
_energyText.text = $"Energy: {data.energy}";
|
_energyText.text = $"Energy: {data.energy}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update Mood bar
|
// Check for mood change (Visual Expression)
|
||||||
float moodPercent = data.mood / 100f;
|
if (_moodState != data.mood_state)
|
||||||
if (_moodBarFill != null)
|
|
||||||
{
|
{
|
||||||
_moodBarFill.rectTransform.anchorMax = new Vector2(moodPercent, 1);
|
_moodState = data.mood_state;
|
||||||
_moodBarFill.color = GetMoodColor(data.mood_state);
|
// Only regenerate if using placeholder sprite
|
||||||
|
if (characterSprite == null && _spriteRenderer != null)
|
||||||
|
{
|
||||||
|
RegeneratePlaceholderSprite();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (_moodText != null)
|
if (_moodText != null)
|
||||||
{
|
{
|
||||||
@@ -912,19 +1187,56 @@ namespace TheIsland.Visual
|
|||||||
{
|
{
|
||||||
if (_deathOverlay != null) _deathOverlay.SetActive(false);
|
if (_deathOverlay != null) _deathOverlay.SetActive(false);
|
||||||
|
|
||||||
// Restore sprite color
|
// Restore sprite color based on state
|
||||||
if (_spriteRenderer != null)
|
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
|
#endregion
|
||||||
|
|
||||||
#region Speech
|
#region Speech
|
||||||
public void ShowSpeech(string text)
|
public void ShowSpeech(string text)
|
||||||
|
{
|
||||||
|
ShowSpeech(text, speechDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowSpeech(string text, float duration)
|
||||||
{
|
{
|
||||||
if (_speechBubble == null || !IsAlive) return;
|
if (_speechBubble == null || !IsAlive) return;
|
||||||
|
|
||||||
|
_speechBubble.DisplayDuration = duration;
|
||||||
_speechBubble.Setup(text);
|
_speechBubble.Setup(text);
|
||||||
Debug.Log($"[AgentVisual] {_currentData?.name} says: \"{text}\"");
|
Debug.Log($"[AgentVisual] {_currentData?.name} says: \"{text}\"");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ namespace TheIsland.UI
|
|||||||
private List<GameObject> _entries = new List<GameObject>();
|
private List<GameObject> _entries = new List<GameObject>();
|
||||||
private bool _visible = true;
|
private bool _visible = true;
|
||||||
private int _unread = 0;
|
private int _unread = 0;
|
||||||
private bool _ready = false;
|
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
@@ -83,7 +82,6 @@ namespace TheIsland.UI
|
|||||||
if (NetworkManager.Instance != null)
|
if (NetworkManager.Instance != null)
|
||||||
{
|
{
|
||||||
SubscribeEvents();
|
SubscribeEvents();
|
||||||
_ready = true;
|
|
||||||
AddLog("事件日志已就绪", Color.yellow);
|
AddLog("事件日志已就绪", Color.yellow);
|
||||||
Debug.Log("[EventLog] 初始化完成");
|
Debug.Log("[EventLog] 初始化完成");
|
||||||
}
|
}
|
||||||
@@ -189,7 +187,7 @@ namespace TheIsland.UI
|
|||||||
panelRect.offsetMax = new Vector2(360, -80);
|
panelRect.offsetMax = new Vector2(360, -80);
|
||||||
|
|
||||||
var panelImg = _panel.AddComponent<Image>();
|
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");
|
var header = new GameObject("Header");
|
||||||
@@ -201,7 +199,7 @@ namespace TheIsland.UI
|
|||||||
headerRect.sizeDelta = new Vector2(0, 28);
|
headerRect.sizeDelta = new Vector2(0, 28);
|
||||||
headerRect.anchoredPosition = Vector2.zero;
|
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");
|
var titleObj = new GameObject("Title");
|
||||||
titleObj.transform.SetParent(header.transform, false);
|
titleObj.transform.SetParent(header.transform, false);
|
||||||
@@ -296,8 +294,8 @@ namespace TheIsland.UI
|
|||||||
entry.transform.SetParent(_content, false);
|
entry.transform.SetParent(_content, false);
|
||||||
|
|
||||||
entry.AddComponent<Image>().color = _entries.Count % 2 == 0
|
entry.AddComponent<Image>().color = _entries.Count % 2 == 0
|
||||||
? new Color(0.08f, 0.1f, 0.13f, 0.9f)
|
? new Color(0f, 0f, 0f, 0.2f)
|
||||||
: new Color(0.06f, 0.08f, 0.11f, 0.9f);
|
: new Color(0f, 0f, 0f, 0.1f);
|
||||||
|
|
||||||
var le = entry.AddComponent<LayoutElement>();
|
var le = entry.AddComponent<LayoutElement>();
|
||||||
le.minHeight = 36;
|
le.minHeight = 36;
|
||||||
|
|||||||
@@ -152,6 +152,9 @@ namespace TheIsland.Core
|
|||||||
network.OnTalk += HandleTalk;
|
network.OnTalk += HandleTalk;
|
||||||
network.OnRevive += HandleRevive;
|
network.OnRevive += HandleRevive;
|
||||||
network.OnSocialInteraction += HandleSocialInteraction;
|
network.OnSocialInteraction += HandleSocialInteraction;
|
||||||
|
network.OnGiftEffect += HandleGiftEffect; // Phase 8
|
||||||
|
network.OnAgentAction += HandleAgentAction; // Phase 13
|
||||||
|
network.OnRandomEvent += HandleRandomEvent; // Phase 17-C
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UnsubscribeFromNetworkEvents()
|
private void UnsubscribeFromNetworkEvents()
|
||||||
@@ -178,6 +181,8 @@ namespace TheIsland.Core
|
|||||||
network.OnTalk -= HandleTalk;
|
network.OnTalk -= HandleTalk;
|
||||||
network.OnRevive -= HandleRevive;
|
network.OnRevive -= HandleRevive;
|
||||||
network.OnSocialInteraction -= HandleSocialInteraction;
|
network.OnSocialInteraction -= HandleSocialInteraction;
|
||||||
|
network.OnGiftEffect -= HandleGiftEffect; // Phase 8
|
||||||
|
network.OnRandomEvent -= HandleRandomEvent; // Phase 17-C
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -320,6 +325,7 @@ namespace TheIsland.Core
|
|||||||
if (_agentVisuals.TryGetValue(data.agent_id, out AgentVisual agentVisual))
|
if (_agentVisuals.TryGetValue(data.agent_id, out AgentVisual agentVisual))
|
||||||
{
|
{
|
||||||
agentVisual.ShowSpeech(data.text);
|
agentVisual.ShowSpeech(data.text);
|
||||||
|
agentVisual.DoJump(); // Add jump effect
|
||||||
}
|
}
|
||||||
// Check AgentUI (programmatic UI system)
|
// Check AgentUI (programmatic UI system)
|
||||||
else if (_agentUIs.TryGetValue(data.agent_id, out AgentUI agentUI))
|
else if (_agentUIs.TryGetValue(data.agent_id, out AgentUI agentUI))
|
||||||
@@ -485,6 +491,140 @@ namespace TheIsland.Core
|
|||||||
initiatorUI.ShowSpeech(data.dialogue);
|
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
|
#endregion
|
||||||
|
|
||||||
#region Agent Management
|
#region Agent Management
|
||||||
|
|||||||
@@ -48,6 +48,17 @@ namespace TheIsland.Models
|
|||||||
public string mood_state; // "happy", "neutral", "sad", "anxious"
|
public string mood_state; // "happy", "neutral", "sad", "anxious"
|
||||||
public string social_tendency; // "introvert", "extrovert", "neutral"
|
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";
|
public bool IsAlive => status == "Alive";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +149,10 @@ namespace TheIsland.Models
|
|||||||
public int resource_level;
|
public int resource_level;
|
||||||
public int current_tick_in_day;
|
public int current_tick_in_day;
|
||||||
public string time_of_day; // "dawn", "day", "dusk", "night"
|
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>
|
/// <summary>
|
||||||
@@ -240,6 +255,21 @@ namespace TheIsland.Models
|
|||||||
public string dialogue;
|
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>
|
/// <summary>
|
||||||
/// Client message structure for sending to server.
|
/// Client message structure for sending to server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -293,5 +323,67 @@ namespace TheIsland.Models
|
|||||||
public const string SOCIAL_INTERACTION = "social_interaction";
|
public const string SOCIAL_INTERACTION = "social_interaction";
|
||||||
public const string RELATIONSHIP_CHANGE = "relationship_change";
|
public const string RELATIONSHIP_CHANGE = "relationship_change";
|
||||||
public const string AUTO_REVIVE = "auto_revive";
|
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<ReviveEventData> OnRevive;
|
||||||
public event Action<SocialInteractionData> OnSocialInteraction;
|
public event Action<SocialInteractionData> OnSocialInteraction;
|
||||||
public event Action<WorldStateData> OnWorldUpdate;
|
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
|
#endregion
|
||||||
|
|
||||||
#region Private Fields
|
#region Private Fields
|
||||||
@@ -343,11 +348,37 @@ namespace TheIsland.Network
|
|||||||
OnSocialInteraction?.Invoke(socialData);
|
OnSocialInteraction?.Invoke(socialData);
|
||||||
break;
|
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:
|
case EventTypes.COMMENT:
|
||||||
// Comments can be logged but typically not displayed in 3D
|
// Comments can be logged but typically not displayed in 3D
|
||||||
Debug.Log($"[Chat] {json}");
|
Debug.Log($"[Chat] {json}");
|
||||||
break;
|
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:
|
default:
|
||||||
Debug.Log($"[NetworkManager] Unhandled event type: {baseMessage.event_type}");
|
Debug.Log($"[NetworkManager] Unhandled event type: {baseMessage.event_type}");
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ namespace TheIsland.UI
|
|||||||
topBar.offsetMax = new Vector2(-10, -10);
|
topBar.offsetMax = new Vector2(-10, -10);
|
||||||
|
|
||||||
var topBarImg = topBar.gameObject.AddComponent<Image>();
|
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)
|
// Connection Status (Left)
|
||||||
_connectionStatus = CreateText(topBar, "ConnectionStatus", "● Disconnected",
|
_connectionStatus = CreateText(topBar, "ConnectionStatus", "● Disconnected",
|
||||||
@@ -205,7 +205,7 @@ namespace TheIsland.UI
|
|||||||
bottomBar.offsetMax = new Vector2(-10, 70);
|
bottomBar.offsetMax = new Vector2(-10, 70);
|
||||||
|
|
||||||
var bottomBarImg = bottomBar.gameObject.AddComponent<Image>();
|
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
|
// Command Input
|
||||||
var inputObj = new GameObject("CommandInput");
|
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 UnityEngine;
|
||||||
using TheIsland.Core;
|
using TheIsland.Core;
|
||||||
using TheIsland.Network;
|
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 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 Color waterDeepColor = new Color(0.1f, 0.4f, 0.6f, 0.9f);
|
||||||
[SerializeField] private float waveSpeed = 0.5f;
|
[SerializeField] private float waveSpeed = 0.5f;
|
||||||
[SerializeField] private float waveAmplitude = 0.1f;
|
[SerializeField] private Material customWaterMaterial; // Custom shader support
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region References
|
#region References
|
||||||
@@ -74,6 +75,7 @@ namespace TheIsland.Visual
|
|||||||
private float _transitionProgress = 1f;
|
private float _transitionProgress = 1f;
|
||||||
private Color _targetSkyTop, _targetSkyBottom;
|
private Color _targetSkyTop, _targetSkyBottom;
|
||||||
private Color _currentSkyTop, _currentSkyBottom;
|
private Color _currentSkyTop, _currentSkyBottom;
|
||||||
|
private List<Transform> _palmTrees = new List<Transform>();
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Unity Lifecycle
|
#region Unity Lifecycle
|
||||||
@@ -86,6 +88,7 @@ namespace TheIsland.Visual
|
|||||||
}
|
}
|
||||||
_instance = this;
|
_instance = this;
|
||||||
|
|
||||||
|
LoadEnvironmentTexture();
|
||||||
_mainCamera = Camera.main;
|
_mainCamera = Camera.main;
|
||||||
CreateEnvironment();
|
CreateEnvironment();
|
||||||
}
|
}
|
||||||
@@ -103,6 +106,15 @@ namespace TheIsland.Visual
|
|||||||
|
|
||||||
// Set initial sky
|
// Set initial sky
|
||||||
UpdateSkyColors();
|
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()
|
private void Update()
|
||||||
@@ -116,8 +128,56 @@ namespace TheIsland.Visual
|
|||||||
UpdateSkyMaterial();
|
UpdateSkyMaterial();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Animate water
|
// Phase 19: Cinematic Lighting
|
||||||
AnimateWater();
|
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()
|
private void OnDestroy()
|
||||||
@@ -140,6 +200,7 @@ namespace TheIsland.Visual
|
|||||||
CreateWater();
|
CreateWater();
|
||||||
CreateLighting();
|
CreateLighting();
|
||||||
CreateDecorations();
|
CreateDecorations();
|
||||||
|
CreateClouds();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateSky()
|
private void CreateSky()
|
||||||
@@ -172,45 +233,6 @@ namespace TheIsland.Visual
|
|||||||
|
|
||||||
private Material CreateGradientMaterial()
|
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
|
// Since we can't create shaders at runtime easily, use a texture-based approach
|
||||||
return CreateGradientTextureMaterial();
|
return CreateGradientTextureMaterial();
|
||||||
}
|
}
|
||||||
@@ -308,9 +330,17 @@ namespace TheIsland.Visual
|
|||||||
_waterPlane.transform.localScale = new Vector3(60, 15, 1);
|
_waterPlane.transform.localScale = new Vector3(60, 15, 1);
|
||||||
|
|
||||||
// Create water material
|
// Create water material
|
||||||
|
if (customWaterMaterial != null)
|
||||||
|
{
|
||||||
|
_waterMaterial = customWaterMaterial;
|
||||||
|
_waterPlane.GetComponent<Renderer>().material = _waterMaterial;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
_waterMaterial = new Material(Shader.Find("Unlit/Transparent"));
|
_waterMaterial = new Material(Shader.Find("Unlit/Transparent"));
|
||||||
_waterMaterial.mainTexture = CreateWaterTexture();
|
_waterMaterial.mainTexture = CreateWaterTexture();
|
||||||
_waterPlane.GetComponent<Renderer>().material = _waterMaterial;
|
_waterPlane.GetComponent<Renderer>().material = _waterMaterial;
|
||||||
|
}
|
||||||
_waterPlane.GetComponent<Renderer>().sortingOrder = -40;
|
_waterPlane.GetComponent<Renderer>().sortingOrder = -40;
|
||||||
|
|
||||||
Destroy(_waterPlane.GetComponent<Collider>());
|
Destroy(_waterPlane.GetComponent<Collider>());
|
||||||
@@ -328,11 +358,14 @@ namespace TheIsland.Visual
|
|||||||
for (int x = 0; x < size; x++)
|
for (int x = 0; x < size; x++)
|
||||||
{
|
{
|
||||||
float t = (float)y / size;
|
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
|
// Add caustic-like highlights
|
||||||
float wave = Mathf.Sin(x * 0.2f + y * 0.1f) * 0.5f + 0.5f;
|
float wave1 = Mathf.Sin(x * 0.15f + y * 0.05f + Time.time * 0.2f) * 0.5f + 0.5f;
|
||||||
baseColor = Color.Lerp(baseColor, Color.white, wave * 0.1f);
|
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);
|
tex.SetPixel(x, y, baseColor);
|
||||||
}
|
}
|
||||||
@@ -346,8 +379,11 @@ namespace TheIsland.Visual
|
|||||||
if (_waterMaterial == null) return;
|
if (_waterMaterial == null) return;
|
||||||
|
|
||||||
// Simple UV scrolling for wave effect
|
// Simple UV scrolling for wave effect
|
||||||
float offset = Time.time * waveSpeed * 0.1f;
|
float offset = Time.time * waveSpeed * 0.05f;
|
||||||
_waterMaterial.mainTextureOffset = new Vector2(offset, offset * 0.5f);
|
_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()
|
private void CreateLighting()
|
||||||
@@ -383,6 +419,8 @@ namespace TheIsland.Visual
|
|||||||
CreateRock(new Vector3(-5, 0, 4), 0.5f);
|
CreateRock(new Vector3(-5, 0, 4), 0.5f);
|
||||||
CreateRock(new Vector3(6, 0, 5), 0.7f);
|
CreateRock(new Vector3(6, 0, 5), 0.7f);
|
||||||
CreateRock(new Vector3(-7, 0, 6), 0.4f);
|
CreateRock(new Vector3(-7, 0, 6), 0.4f);
|
||||||
|
|
||||||
|
CreateGroundDetails();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreatePalmTree(Vector3 position, float scale)
|
private void CreatePalmTree(Vector3 position, float scale)
|
||||||
@@ -399,11 +437,70 @@ namespace TheIsland.Visual
|
|||||||
var trunkRenderer = trunkSprite.AddComponent<SpriteRenderer>();
|
var trunkRenderer = trunkSprite.AddComponent<SpriteRenderer>();
|
||||||
trunkRenderer.sprite = CreateTreeSprite();
|
trunkRenderer.sprite = CreateTreeSprite();
|
||||||
trunkRenderer.sortingOrder = -20;
|
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()
|
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 width = 64;
|
||||||
int height = 128;
|
int height = 128;
|
||||||
Texture2D tex = new Texture2D(width, height);
|
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));
|
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)
|
private void DrawPalmFronds(Color[] pixels, int width, int height, Color leaf, Color leafBright)
|
||||||
{
|
{
|
||||||
Vector2 center = new Vector2(width / 2, height * 0.65f);
|
Vector2 center = new Vector2(width / 2, height * 0.65f);
|
||||||
@@ -490,11 +621,24 @@ namespace TheIsland.Visual
|
|||||||
var rockRenderer = rockObj.AddComponent<SpriteRenderer>();
|
var rockRenderer = rockObj.AddComponent<SpriteRenderer>();
|
||||||
rockRenderer.sprite = CreateRockSprite();
|
rockRenderer.sprite = CreateRockSprite();
|
||||||
rockRenderer.sortingOrder = -15;
|
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()
|
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;
|
int size = 32;
|
||||||
Texture2D tex = new Texture2D(size, size);
|
Texture2D tex = new Texture2D(size, size);
|
||||||
|
|
||||||
@@ -540,7 +684,16 @@ namespace TheIsland.Visual
|
|||||||
private void HandleWeatherChange(WeatherChangeData data)
|
private void HandleWeatherChange(WeatherChangeData data)
|
||||||
{
|
{
|
||||||
_currentWeather = data.new_weather;
|
_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)
|
private void HandleTick(TickData data)
|
||||||
@@ -612,6 +765,164 @@ namespace TheIsland.Visual
|
|||||||
}
|
}
|
||||||
#endregion
|
#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
|
#region Public API
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Force update the environment to specific conditions.
|
/// 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_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 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
|
--- !u!1660057539 &9223372036854775807
|
||||||
SceneRoots:
|
SceneRoots:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -497,3 +548,4 @@ SceneRoots:
|
|||||||
- {fileID: 851065944}
|
- {fileID: 851065944}
|
||||||
- {fileID: 1562380643}
|
- {fileID: 1562380643}
|
||||||
- {fileID: 318018868}
|
- {fileID: 318018868}
|
||||||
|
- {fileID: 2108464769}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
"com.unity.modules.video": "1.0.0",
|
"com.unity.modules.video": "1.0.0",
|
||||||
"com.unity.modules.vr": "1.0.0",
|
"com.unity.modules.vr": "1.0.0",
|
||||||
"com.unity.modules.wind": "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": {},
|
"dependencies": {},
|
||||||
"url": "https://packages.unity.com"
|
"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": {
|
"com.unity.serialization": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
|
|||||||
@@ -682,7 +682,22 @@ PlayerSettings:
|
|||||||
webWasm2023: 0
|
webWasm2023: 0
|
||||||
webEnableSubmoduleStrippingCompatibility: 0
|
webEnableSubmoduleStrippingCompatibility: 0
|
||||||
scriptingDefineSymbols:
|
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: {}
|
additionalCompilerArguments: {}
|
||||||
platformArchitecture: {}
|
platformArchitecture: {}
|
||||||
scriptingBackend: {}
|
scriptingBackend: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user