Compare commits

...

10 Commits

Author SHA1 Message Date
empty
f270a8b099 feat: Phase 19-D/E - social behavior and velocity-based animation
- Add soft-repulsion to prevent agent crowding
- Implement dynamic Z-sorting based on world position
- Add social orientation (agents face nearby agents)
- Use velocity-based animation instead of isMoving flag
- Track lastPosition for smooth velocity calculation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 00:30:46 +08:00
empty
0187c5ecbe feat: Phase 19-C/D - sprite loading, transparency, and animation
- Add runtime sprite loading from Characters.png and Environment.png
- Implement ProcessTransparency for chroma-key white background removal
- Add AgentAnimator for procedural idle/movement animations
- Add Billboard component support for 2.5D perspective
- Normalize sprite scales based on world units
- Fix SetMovement parameter mismatch

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 00:23:13 +08:00
empty
01abd3c2dc fix: resolve compilation errors and warnings
- Add missing MoveTowardsTarget() method in AgentVisual
- Remove unused _breathScale field
- Replace deprecated FindObjectOfType with FindFirstObjectByType
- Remove unused shaderCode variable

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 00:05:40 +08:00
empty
119afede55 fix: add missing System.Collections.Generic using directive
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 00:02:23 +08:00
empty
b0e6a3488c feat: Phase 19 - visual polish and cinematic lighting
Backend:
- Add weather transition processing logic

Unity Client:
- Add smooth UI bar transitions with lerp animations
- Implement cinematic lighting system (dawn/day/dusk/night cycle)
- Add VisualEffectsManager for advanced effects
- Support premium sprite loading for agents
- Add character and environment sprite sheets

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 23:58:02 +08:00
empty
91694ba802 docs: update README with new game systems and features
- Add memory_service.py to project structure
- Document new game systems: autonomous actions, sickness, crafting,
  resource scarcity, social roles, memory system, random events
- Add agent attributes table (immunity, social_role, action, location)
- Update Unity scripts documentation
- Add new event types (AGENT_ACTION, CRAFT, USE_ITEM, RANDOM_EVENT)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 23:33:26 +08:00
empty
8277778106 feat: implement survival, crafting, memory, and social systems
- Phase 13: Autonomous Agency - agents now have actions and locations
- Phase 15: Sickness mechanics with immunity and weather effects
- Phase 16: Crafting system (medicine from herbs)
- Phase 17-A: Resource scarcity with tree fruit regeneration
- Phase 17-B: Social roles (leader, follower, loner) with clique behavior
- Phase 17-C: Random events support
- Add AgentMemory model for long-term agent memory storage
- Add memory_service for managing agent memories
- Update Unity client models and event handlers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 23:28:38 +08:00
empty
432f178fc5 feat(unity): add beach ground details
- Add scattered seashells near water line
- Add pebbles across the beach
- Procedural sprite generation for shells and pebbles

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 22:07:39 +08:00
empty
20c82276fa feat(unity): enhance visual effects and animations
- Add cloud system with procedural sprites and parallax movement
- Add tree swaying animation for palm trees
- Improve agent breathing with squash & stretch animation
- Add jump animation routine for agent reactions
- Add custom CartoonWater shader support
- Add SetupVisuals editor tool and GlobalProfile asset
- Lower speech bubble alpha for glass effect

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 22:07:12 +08:00
empty
d1b02b4dfd feat: Phase 8 - VFX 和 AI 打赏反应系统
- Unity: 添加 VFXManager 实现金币雨和爱心爆炸特效
- Unity: NetworkManager 支持 GiftEffect 事件
- Unity: AgentVisual 支持自定义时长的 SpeechBubble
- Backend: LLMService 支持生成个性化感谢语
- Backend: Engine 统一处理礼物逻辑 (handle_gift)
- Backend: TwitchBot 接入新的礼物处理流程
2026-01-01 21:38:49 +08:00
39 changed files with 3178 additions and 857 deletions

View File

@@ -14,6 +14,7 @@ the-island/
│ ├── models.py # SQLAlchemy 数据模型
│ ├── schemas.py # Pydantic 消息模式
│ ├── llm.py # LLM 集成 (对话生成)
│ ├── memory_service.py # Agent 记忆管理服务
│ ├── twitch_service.py # Twitch 聊天机器人
│ └── database.py # 数据库配置
├── frontend/ # Web 调试客户端
@@ -35,6 +36,13 @@ the-island/
- **天气系统**: 晴天、多云、雨天、暴风雨、炎热、雾天
- **社交系统**: 角色间自主社交互动
- **休闲模式**: 自动复活、降低难度
- **自主行动**: 角色会自动进行采集、休息、社交等行为
- **疾病机制**: 恶劣天气和低免疫力可能导致生病
- **制作系统**: 使用草药制作药品治愈疾病
- **资源稀缺**: 树木果实有限,每日再生
- **社交角色**: 领导者、追随者、独行者动态关系
- **记忆系统**: Agent 会记住重要的互动和事件
- **随机事件**: 风暴破坏、发现宝藏、野兽袭击等
### 玩家命令
| 命令 | 格式 | 金币消耗 | 效果 |
@@ -54,6 +62,17 @@ the-island/
每个角色有独特性格,会根据性格做出不同反应和社交行为。
#### 角色属性
| 属性 | 说明 |
|------|------|
| HP | 生命值,归零则死亡 |
| 能量 | 行动力,过低会影响行动 |
| 心情 | 情绪状态,影响社交和决策 |
| 免疫力 | 抵抗疾病的能力 (0-100) |
| 社交角色 | leader/follower/loner/neutral |
| 当前行动 | Idle/Gather/Sleep/Socialize 等 |
| 位置 | tree_left/tree_right/campfire/herb_patch 等 |
## 技术栈
### 后端
@@ -97,12 +116,13 @@ uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
| 脚本 | 功能 |
|------|------|
| `NetworkManager.cs` | WebSocket 连接管理、消息收发 |
| `GameManager.cs` | 游戏状态管理、角色生成 |
| `GameManager.cs` | 游戏状态管理、角色生成、行动系统 |
| `UIManager.cs` | 主 UI 界面 (顶部状态栏、底部命令输入) |
| `EventLog.cs` | 事件日志面板 (显示游戏事件) |
| `AgentVisual.cs` | 角色视觉组件 (精灵、血条、对话框) |
| `AgentVisual.cs` | 角色视觉组件 (精灵、血条、对话框、状态图标) |
| `EnvironmentManager.cs` | 环境场景 (沙滩、海洋、天空) |
| `WeatherEffects.cs` | 天气粒子效果 (雨、雾、热浪) |
| `Models.cs` | 数据模型 (Agent、WorldState、事件数据) |
### 视觉特性
- 程序化生成的 2.5D 角色精灵
@@ -147,10 +167,17 @@ HEAL # 治疗反馈
TALK # 对话反馈
ENCOURAGE # 鼓励反馈
REVIVE # 复活反馈
GIFT_EFFECT # Bits 打赏特效
# 社交系统
SOCIAL_INTERACTION # 角色间社交
AUTO_REVIVE # 自动复活 (休闲模式)
# 自主行动系统 (Phase 13+)
AGENT_ACTION # 角色执行行动 (采集/休息/社交等)
CRAFT # 制作物品 (药品等)
USE_ITEM # 使用物品
RANDOM_EVENT # 随机事件 (风暴/宝藏/野兽等)
```
## Twitch 直播集成

View File

@@ -14,6 +14,7 @@ from .schemas import GameEvent, EventType
from .database import init_db, get_db_session
from .models import User, Agent, WorldState, GameConfig, AgentRelationship
from .llm import llm_service
from .memory_service import memory_service
if TYPE_CHECKING:
from .server import ConnectionManager
@@ -214,6 +215,35 @@ class GameEngine:
# =========================================================================
# Day/Night cycle (Phase 2)
# =========================================================================
async def _process_weather(self) -> None:
"""Process weather transitions."""
with get_db_session() as db:
world = db.query(WorldState).first()
if not world:
return
world.weather_duration += 1
# Should we transition?
if world.weather_duration >= random.randint(WEATHER_MIN_DURATION, WEATHER_MAX_DURATION):
old_weather = world.weather
transitions = WEATHER_TRANSITIONS.get(old_weather, {"Sunny": 1.0})
# Biased random choice
choices = list(transitions.keys())
weights = list(transitions.values())
new_weather = random.choices(choices, weights=weights, k=1)[0]
if new_weather != old_weather:
world.weather = new_weather
world.weather_duration = 0
await self._broadcast_event(EventType.WEATHER_CHANGE, {
"old_weather": old_weather,
"new_weather": new_weather,
"message": f"The weather is changing to {new_weather}."
})
async def _advance_time(self) -> Optional[dict]:
"""Advance time and return phase change info if applicable."""
with get_db_session() as db:
@@ -228,9 +258,14 @@ class GameEngine:
if world.current_tick_in_day >= TICKS_PER_DAY:
world.current_tick_in_day = 0
world.day_count += 1
# Phase 17-A: Regenerate resources
world.tree_left_fruit = min(5, world.tree_left_fruit + 2)
world.tree_right_fruit = min(5, world.tree_right_fruit + 2)
await self._broadcast_event(EventType.DAY_CHANGE, {
"day": world.day_count,
"message": f"Day {world.day_count} begins!"
"message": f"Day {world.day_count} begins! Trees have new fruit."
})
# Determine current phase
@@ -299,6 +334,55 @@ class GameEngine:
else:
agent.mood_state = "anxious"
# =========================================================================
# Relationship 2.0 (Phase 17-B)
# =========================================================================
async def _assign_social_roles(self) -> None:
"""Assign social roles based on personality and social tendency."""
with get_db_session() as db:
agents = db.query(Agent).filter(Agent.status == "Alive").all()
for agent in agents:
if agent.social_role != "neutral":
continue # Already assigned
# Role assignment based on personality and tendency
if agent.social_tendency == "extrovert" and agent.mood > 60:
agent.social_role = "leader"
elif agent.social_tendency == "introvert" and agent.mood < 40:
agent.social_role = "loner"
elif random.random() < 0.3:
agent.social_role = "follower"
# Otherwise stays neutral
async def _process_clique_behavior(self) -> None:
"""Leaders influence followers' actions."""
# Run occasionally
if self._tick_count % 10 != 0:
return
with get_db_session() as db:
leaders = db.query(Agent).filter(
Agent.status == "Alive",
Agent.social_role == "leader"
).all()
followers = db.query(Agent).filter(
Agent.status == "Alive",
Agent.social_role == "follower"
).all()
for leader in leaders:
# Followers near leader copy their action
for follower in followers:
if follower.current_action == "Idle" and leader.current_action not in ["Idle", None]:
follower.current_action = leader.current_action
follower.location = leader.location
await self._broadcast_event(EventType.COMMENT, {
"user": "System",
"message": f"{follower.name} follows {leader.name}'s lead!"
})
# =========================================================================
# Survival mechanics
# =========================================================================
@@ -315,6 +399,45 @@ class GameEngine:
alive_agents = db.query(Agent).filter(Agent.status == "Alive").all()
for agent in alive_agents:
# --- Sickness Mechanics (Phase 15) ---
# 1. Contracting Sickness
if not agent.is_sick:
sickness_chance = 0.01 # Base 1% per tick (every 5s)
# Weather impact
current_weather = world.weather if world else "Sunny"
if current_weather == "Rainy":
sickness_chance += 0.05
elif current_weather == "Stormy":
sickness_chance += 0.10
# Immunity impact (Higher immunity = lower chance)
# Immunity 50 -> -2.5%, Immunity 100 -> -5%
sickness_chance -= (agent.immunity / 2000.0)
if random.random() < sickness_chance:
agent.is_sick = True
agent.mood -= 20
logger.info(f"Agent {agent.name} has fallen sick!")
# We could broadcast a specific event, but AGENTS_UPDATE will handle visual state
# Just log it or maybe a system message?
await self._broadcast_event(EventType.COMMENT, {
"user": "System",
"message": f"{agent.name} is looking pale... (Sick)"
})
# 2. Sickness Effects
if agent.is_sick:
# Decay HP and Energy faster
agent.hp = max(0, agent.hp - 2)
agent.energy = max(0, agent.energy - 2)
# Lower mood over time
if self._tick_count % 5 == 0:
agent.mood = max(0, agent.mood - 1)
# --- End Sickness ---
# Calculate energy decay with all modifiers
base_decay = BASE_ENERGY_DECAY_PER_TICK
decay = base_decay * config.energy_decay_multiplier
@@ -323,9 +446,9 @@ class GameEngine:
agent.energy = max(0, agent.energy - int(decay))
# HP recovery during day phases
# HP recovery during day phases (Only if NOT sick)
hp_recovery = phase_mod.get("hp_recovery", 0)
if hp_recovery > 0 and agent.energy > 20:
if hp_recovery > 0 and agent.energy > 20 and not agent.is_sick:
agent.hp = min(100, agent.hp + hp_recovery)
# Starvation damage
@@ -337,6 +460,9 @@ class GameEngine:
if agent.hp <= 0:
agent.status = "Dead"
agent.death_tick = self._tick_count
if agent.is_sick:
# Clear sickness on death
agent.is_sick = False
deaths.append({"name": agent.name, "personality": agent.personality})
logger.info(f"Agent {agent.name} has died!")
@@ -346,6 +472,18 @@ class GameEngine:
"agent_name": death["name"],
"message": f"{death['name']} ({death['personality']}) has died..."
})
# Phase 14: Alive agents remember the death
with get_db_session() as db:
witnesses = db.query(Agent).filter(Agent.status == "Alive").all()
for witness in witnesses:
await memory_service.add_memory(
agent_id=witness.id,
description=f"{death['name']} died. It was a sad day.",
importance=8,
related_entity_name=death["name"],
memory_type="event"
)
async def _process_auto_revive(self) -> None:
"""Auto-revive dead agents in casual mode."""
@@ -497,6 +635,237 @@ class GameEngine:
except Exception as e:
logger.error(f"Error in social dialogue: {e}")
# =========================================================================
# Autonomous Agency (Phase 13)
# =========================================================================
async def _process_activity_tick(self) -> None:
"""Decide and execute autonomous agent actions."""
# Only process activity every few ticks to avoid chaotic movement
if self._tick_count % 3 != 0:
return
with get_db_session() as db:
world = db.query(WorldState).first()
if not world:
return
agents = db.query(Agent).filter(Agent.status == "Alive").all()
for agent in agents:
new_action = agent.current_action
new_location = agent.location
target_name = None
should_update = False
# 1. Critical Needs (Override everything)
if world.time_of_day == "night":
if agent.current_action != "Sleep":
new_action = "Sleep"
new_location = "campfire"
should_update = True
elif agent.energy < 30:
if agent.current_action != "Gather":
new_action = "Gather"
new_location = random.choice(["tree_left", "tree_right"])
should_update = True
# 1.5. Sickness Handling (Phase 16)
elif agent.is_sick:
inv = self._get_inventory(agent)
if inv.get("medicine", 0) > 0:
# Use medicine immediately
await self._use_medicine(agent)
new_action = "Use Medicine"
new_location = agent.location
should_update = True
elif inv.get("herb", 0) >= 3:
# Craft medicine
await self._craft_medicine(agent)
new_action = "Craft Medicine"
new_location = agent.location
should_update = True
elif agent.current_action != "Gather Herb":
# Go gather herbs
new_action = "Gather Herb"
new_location = "herb_patch"
should_update = True
# 2. Mood / Social Needs
elif agent.mood < 40 and agent.current_action not in ["Sleep", "Gather", "Socialize", "Gather Herb"]:
new_action = "Socialize"
potential_friends = [a for a in agents if a.id != agent.id]
if potential_friends:
friend = random.choice(potential_friends)
new_location = "agent"
target_name = friend.name
should_update = True
# 3. Boredom / Wandering
elif agent.current_action == "Idle" or agent.current_action is None:
if random.random() < 0.3:
new_action = "Wander"
new_location = "nearby"
should_update = True
# 4. Finish Tasks (Simulation)
elif agent.current_action == "Gather" and agent.energy >= 90:
new_action = "Idle"
new_location = "center"
should_update = True
elif agent.current_action == "Gather" and agent.location in ["tree_left", "tree_right"]:
# Phase 17-A: Consume fruit when gathering
fruit_available = await self._consume_fruit(world, agent.location)
if fruit_available:
agent.energy = min(100, agent.energy + 30)
new_action = "Idle"
new_location = "center"
should_update = True
else:
# No fruit! Try other tree or express frustration
other_tree = "tree_right" if agent.location == "tree_left" else "tree_left"
other_fruit = world.tree_right_fruit if agent.location == "tree_left" else world.tree_left_fruit
if other_fruit > 0:
new_action = "Gather"
new_location = other_tree
should_update = True
else:
# All trees empty!
new_action = "Hungry"
new_location = "center"
should_update = True
await self._broadcast_event(EventType.COMMENT, {
"user": "System",
"message": f"{agent.name} can't find any fruit! The trees are empty..."
})
elif agent.current_action == "Sleep" and world.time_of_day != "night":
new_action = "Wake Up"
new_location = "center"
should_update = True
elif agent.current_action == "Gather Herb":
# Simulate herb gathering (add herbs)
await self._gather_herb(agent)
new_action = "Idle"
new_location = "center"
should_update = True
# Execute Update
if should_update:
agent.current_action = new_action
agent.location = new_location
# Generate simple thought/bark
dialogue = self._get_action_bark(agent, new_action, target_name)
await self._broadcast_event(EventType.AGENT_ACTION, {
"agent_id": agent.id,
"agent_name": agent.name,
"action_type": new_action,
"location": new_location,
"target_name": target_name,
"dialogue": dialogue
})
def _get_action_bark(self, agent: Agent, action: str, target: str = None) -> str:
"""Get a simple bark text for an action."""
if action == "Sleep":
return random.choice(["Yawn... sleepy...", "Time to rest.", "Zzz..."])
elif action == "Gather":
return random.choice(["Hungry!", "Need food.", "Looking for coconuts..."])
elif action == "Gather Herb":
return random.choice(["I need herbs...", "Looking for medicine plants.", "Feeling sick..."])
elif action == "Craft Medicine":
return random.choice(["Let me make some medicine.", "Mixing herbs...", "Almost done!"])
elif action == "Use Medicine":
return random.choice(["Ahh, much better!", "Medicine tastes awful but works!", "Feeling cured!"])
elif action == "Socialize":
return f"Looking for {target}..." if target else "Need a friend."
elif action == "Wander":
return random.choice(["Hmm...", "Nice weather.", "Taking a walk."])
elif action == "Wake Up":
return "Good morning!"
return ""
# =========================================================================
# Inventory & Crafting (Phase 16)
# =========================================================================
def _get_inventory(self, agent: Agent) -> dict:
"""Parse agent inventory JSON."""
import json
try:
return json.loads(agent.inventory) if agent.inventory else {}
except json.JSONDecodeError:
return {}
def _set_inventory(self, agent: Agent, inv: dict) -> None:
"""Set agent inventory from dict."""
import json
agent.inventory = json.dumps(inv)
async def _consume_fruit(self, world: WorldState, location: str) -> bool:
"""Consume fruit from a tree. Returns True if successful."""
if location == "tree_left":
if world.tree_left_fruit > 0:
world.tree_left_fruit -= 1
logger.info(f"Fruit consumed from tree_left. Remaining: {world.tree_left_fruit}")
return True
elif location == "tree_right":
if world.tree_right_fruit > 0:
world.tree_right_fruit -= 1
logger.info(f"Fruit consumed from tree_right. Remaining: {world.tree_right_fruit}")
return True
return False
async def _gather_herb(self, agent: Agent) -> None:
"""Agent gathers herbs."""
inv = self._get_inventory(agent)
herbs_found = random.randint(1, 2)
inv["herb"] = inv.get("herb", 0) + herbs_found
self._set_inventory(agent, inv)
await self._broadcast_event(EventType.AGENT_ACTION, {
"agent_id": agent.id,
"agent_name": agent.name,
"action_type": "Gather Herb",
"location": "herb_patch",
"dialogue": f"Found {herbs_found} herbs!"
})
logger.info(f"Agent {agent.name} gathered {herbs_found} herbs. Total: {inv['herb']}")
async def _craft_medicine(self, agent: Agent) -> None:
"""Agent crafts medicine from herbs."""
inv = self._get_inventory(agent)
if inv.get("herb", 0) >= 3:
inv["herb"] -= 3
inv["medicine"] = inv.get("medicine", 0) + 1
self._set_inventory(agent, inv)
await self._broadcast_event(EventType.CRAFT, {
"agent_id": agent.id,
"agent_name": agent.name,
"item": "medicine",
"ingredients": {"herb": 3}
})
logger.info(f"Agent {agent.name} crafted medicine. Inventory: {inv}")
async def _use_medicine(self, agent: Agent) -> None:
"""Agent uses medicine to cure sickness."""
inv = self._get_inventory(agent)
if inv.get("medicine", 0) > 0:
inv["medicine"] -= 1
self._set_inventory(agent, inv)
agent.is_sick = False
agent.hp = min(100, agent.hp + 20)
agent.mood = min(100, agent.mood + 10)
await self._broadcast_event(EventType.USE_ITEM, {
"agent_id": agent.id,
"agent_name": agent.name,
"item": "medicine",
"effect": "cured sickness"
})
logger.info(f"Agent {agent.name} used medicine and is cured!")
# =========================================================================
# LLM-powered agent speech
# =========================================================================
@@ -648,12 +1017,19 @@ class GameEngine:
user.gold -= HEAL_COST
old_hp = agent.hp
was_sick = agent.is_sick
agent.hp = min(100, agent.hp + HEAL_HP_RESTORE)
agent.is_sick = False # Cure sickness
msg = f"{username} healed {agent.name}!"
if was_sick:
msg = f"{username} cured {agent.name}'s sickness!"
await self._broadcast_event(EventType.HEAL, {
"user": username, "agent_name": agent.name,
"hp_restored": agent.hp - old_hp, "agent_hp": agent.hp,
"user_gold": user.gold, "message": f"{username} healed {agent.name}!"
"user_gold": user.gold, "message": msg
})
await self._broadcast_event(EventType.USER_UPDATE, {"user": username, "gold": user.gold})
@@ -848,6 +1224,77 @@ class GameEngine:
await self._handle_reset(user)
return
# =========================================================================
# Random Events (Phase 17-C)
# =========================================================================
RANDOM_EVENTS = {
"storm_damage": {"weight": 30, "description": "A sudden storm damages the island!"},
"treasure_found": {"weight": 25, "description": "Someone found a buried treasure!"},
"beast_attack": {"weight": 20, "description": "A wild beast attacks the camp!"},
"rumor_spread": {"weight": 25, "description": "A rumor starts spreading..."},
}
async def _process_random_events(self) -> None:
"""Process random events (10% chance per day at dawn)."""
# Only trigger at dawn (once per day)
if self._tick_count % 100 != 1: # Roughly once every ~100 ticks
return
if random.random() > 0.10: # 10% chance
return
# Pick random event
events = list(self.RANDOM_EVENTS.keys())
weights = [self.RANDOM_EVENTS[e]["weight"] for e in events]
event_type = random.choices(events, weights=weights)[0]
with get_db_session() as db:
world = db.query(WorldState).first()
agents = db.query(Agent).filter(Agent.status == "Alive").all()
if not agents:
return
event_data = {"event_type": event_type, "message": ""}
if event_type == "storm_damage":
# All agents lose HP and resources depleted
for agent in agents:
agent.hp = max(0, agent.hp - 15)
if world:
world.tree_left_fruit = max(0, world.tree_left_fruit - 2)
world.tree_right_fruit = max(0, world.tree_right_fruit - 2)
event_data["message"] = "A violent storm hits! Everyone is injured and fruit trees are damaged."
elif event_type == "treasure_found":
# Random agent finds treasure (bonus herbs/medicine)
lucky = random.choice(agents)
inv = self._get_inventory(lucky)
inv["medicine"] = inv.get("medicine", 0) + 2
inv["herb"] = inv.get("herb", 0) + 3
self._set_inventory(lucky, inv)
event_data["message"] = f"{lucky.name} found a buried treasure with medicine and herbs!"
event_data["agent_name"] = lucky.name
elif event_type == "beast_attack":
# Random agent gets attacked
victim = random.choice(agents)
victim.hp = max(0, victim.hp - 25)
victim.mood = max(0, victim.mood - 20)
event_data["message"] = f"A wild beast attacked {victim.name}!"
event_data["agent_name"] = victim.name
elif event_type == "rumor_spread":
# Random relationship impact
if len(agents) >= 2:
a1, a2 = random.sample(list(agents), 2)
a1.mood = max(0, a1.mood - 10)
a2.mood = max(0, a2.mood - 10)
event_data["message"] = f"A rumor about {a1.name} and {a2.name} is spreading..."
await self._broadcast_event(EventType.RANDOM_EVENT, event_data)
logger.info(f"Random event triggered: {event_type}")
# =========================================================================
# Game loop
# =========================================================================
@@ -888,9 +1335,19 @@ class GameEngine:
# 5. Update moods (Phase 3)
await self._update_moods()
# 6. Social interactions (Phase 5)
# 6. Autonomous Activity (Phase 13)
await self._process_activity_tick()
# 7. Social interactions (Phase 5)
await self._process_social_tick()
# 8. Random Events (Phase 17-C)
await self._process_random_events()
# 9. Clique Behavior (Phase 17-B)
await self._assign_social_roles()
await self._process_clique_behavior()
# 7. Idle chat
with get_db_session() as db:
alive_count = db.query(Agent).filter(Agent.status == "Alive").count()
@@ -948,11 +1405,17 @@ class GameEngine:
# Use the existing process_comment method to handle commands
await self.process_comment(user, text)
async def process_bits(self, user: str, amount: int) -> None:
"""Process Twitch bits and convert to game gold."""
# 1 Bit = 1 Gold conversion rate
gold_added = amount
async def handle_gift(self, user: str, amount: int, gift_type: str = "bits") -> None:
"""
Handle a gift/donation (bits, subscription, or test).
Args:
user: Name of the donor
amount: Value of the gift
gift_type: Type of gift (bits, test, etc.)
"""
# 1. Add gold to user
gold_added = amount
with get_db_session() as db:
user_obj = self._get_or_create_user(db, user)
user_obj.gold += gold_added
@@ -960,14 +1423,49 @@ class GameEngine:
await self._broadcast_event(EventType.USER_UPDATE, {
"user": user,
"gold": user_obj.gold,
"message": f"{user} received {gold_added} gold from {amount} bits!"
"message": f"{user} received {gold_added} gold!"
})
# Also broadcast a special bits event for UI effects
await self._broadcast_event("bits_received", {
"user": user,
"bits": amount,
"gold": gold_added
})
logger.info(f"Processed bits: {user} -> {amount} bits -> {gold_added} gold")
# Check for alive agents for reaction
alive_agents = db.query(Agent).filter(Agent.status == "Alive").all()
agent = random.choice(alive_agents) if alive_agents else None
# Extract data immediately to avoid DetachedInstanceError after session closes
agent_name = agent.name if agent else "Survivor"
agent_personality = agent.personality if agent else "friendly"
# 2. Generate AI gratitude
gratitude = await llm_service.generate_gratitude(
user=user,
amount=amount,
agent_name=agent_name,
agent_personality=agent_personality,
gift_name=gift_type
)
# 3. Store Memory (Phase 14)
if agent:
memory_text = f"User {user} gave me {amount} {gift_type}. I felt grateful."
await memory_service.add_memory(
agent_id=agent.id,
description=memory_text,
importance=random.randint(6, 9), # Gifts are important
related_entity_name=user,
memory_type="gift"
)
# 4. Broadcast gift effect to Unity
await self._broadcast_event("gift_effect", {
"user": user,
"gift_type": gift_type,
"value": amount,
"message": f"{user} sent {amount} {gift_type}!",
"agent_name": agent_name if agent else None,
"gratitude": gratitude,
"duration": 8.0
})
logger.info(f"Processed gift: {user} -> {amount} {gift_type} (Gratitude: {gratitude})")
async def process_bits(self, user: str, amount: int) -> None:
"""Deprecated: Use handle_gift instead."""
await self.handle_gift(user, amount, "bits")

View File

@@ -27,6 +27,9 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .models import Agent
from .memory_service import MemoryService
from .memory_service import memory_service
logger = logging.getLogger(__name__)
@@ -56,6 +59,24 @@ MOCK_REACTIONS = {
"My stomach is eating itself...",
"Is this how it ends? Starving on a beach?",
],
"gratitude_arrogant": [
"Finally! A worthy tribute! {user}, you understand greatness!",
"About time someone recognized my value! Thanks, {user}!",
"Hmph, {user} knows quality when they see it. Much appreciated!",
"A gift for ME? Well, obviously. Thank you, {user}!",
],
"gratitude_humble": [
"Oh my gosh, {user}! You're too kind! Thank you so much!",
"Wow, {user}, I don't deserve this! You're amazing!",
"*tears up* {user}... this means the world to me!",
"Thank you, thank you {user}! You're the best!",
],
"gratitude_neutral": [
"Hey, thanks {user}! That's really generous of you!",
"Wow, {user}! Thank you so much for the support!",
"Appreciate it, {user}! You're awesome!",
"{user}, you're a legend! Thank you!",
],
}
# Default model configuration
@@ -182,11 +203,16 @@ class LLMService:
return self._get_mock_response(event_type)
try:
# Retrieve relevant memories
memories = await memory_service.get_relevant_memories(agent.id, event_description)
memory_context = "\n".join(memories) if memories else "No relevant memories."
system_prompt = (
f"You are {agent.name}. "
f"Personality: {agent.personality}. "
f"Current Status: HP={agent.hp}, Energy={agent.energy}. "
f"You live on a survival island. "
f"Relevant Memories:\n{memory_context}\n"
f"React to the following event briefly (under 20 words). "
f"Respond in first person, as if speaking out loud."
)
@@ -329,10 +355,15 @@ class LLMService:
"calm and neutral" if agent_mood >= 40 else \
"a bit down" if agent_mood >= 20 else "anxious and worried"
# Retrieve relevant memories
memories = await memory_service.get_relevant_memories(agent.id, topic)
memory_context = "\n".join(memories) if memories else "No relevant memories."
system_prompt = (
f"You are {agent_name}, a survivor on a deserted island. "
f"Personality: {agent_personality}. "
f"Current mood: {mood_desc} (mood level: {agent_mood}/100). "
f"Relevant Memories:\n{memory_context}\n"
f"A viewer named {username} wants to chat with you. "
f"Respond naturally in character (under 30 words). "
f"Be conversational and show your personality."
@@ -461,6 +492,92 @@ class LLMService:
logger.error(f"LLM API error for social interaction: {e}")
return f"{initiator_name}: ...\n{target_name}: ..."
async def generate_gratitude(
self,
user: str,
amount: int,
agent_name: str = "Survivor",
agent_personality: str = "friendly",
gift_name: str = "bits"
) -> str:
"""
Generate a special gratitude response for donations/gifts.
Args:
user: Name of the user who gave the gift
amount: Amount of the gift
agent_name: Name of the agent (optional)
agent_personality: Personality of the agent (optional)
gift_name: Type of gift (bits, subscription, etc.)
Returns:
An excited, grateful response from the agent
"""
personality = agent_personality.lower() if agent_personality else "friendly"
if self._mock_mode:
if "arrogant" in personality or "proud" in personality:
responses = MOCK_REACTIONS.get("gratitude_arrogant", [])
elif "humble" in personality or "shy" in personality or "kind" in personality:
responses = MOCK_REACTIONS.get("gratitude_humble", [])
else:
responses = MOCK_REACTIONS.get("gratitude_neutral", [])
if responses:
return random.choice(responses).format(user=user, amount=amount)
return f"Thank you so much, {user}! You're amazing!"
try:
# Customize tone based on personality
if "arrogant" in personality or "proud" in personality:
tone_instruction = (
"You are somewhat arrogant but still grateful. "
"React with confident excitement, like 'Finally, a worthy tribute!' "
"but still thank them."
)
elif "humble" in personality or "shy" in personality:
tone_instruction = (
"You are humble and easily moved. "
"React with overwhelming gratitude, maybe even get teary-eyed."
)
else:
tone_instruction = (
"React with genuine excitement and gratitude."
)
system_prompt = (
f"You are {agent_name}, a survivor on a deserted island. "
f"Personality: {personality if personality else 'friendly'}. "
f"A wealthy patron named {user} just gave you {amount} {gift_name}! "
f"{tone_instruction} "
f"Respond with extreme excitement and gratitude (max 15 words). "
f"Keep it fun and energetic!"
)
kwargs = {
"model": self._model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"{user} just gave you {amount} {gift_name}! React!"}
],
"max_tokens": 40,
"temperature": 0.95,
}
if self._api_base:
kwargs["api_base"] = self._api_base
if self._api_key and not self._api_key_header:
kwargs["api_key"] = self._api_key
if self._extra_headers:
kwargs["extra_headers"] = self._extra_headers
response = await self._acompletion(**kwargs)
return response.choices[0].message.content.strip()
except Exception as e:
logger.error(f"LLM API error for gratitude: {e}")
return f"Wow, thank you so much {user}! You're amazing!"
# Global instance for easy import
llm_service = LLMService()

View File

@@ -140,6 +140,13 @@ async def websocket_endpoint(websocket: WebSocket):
text = message.payload.get("message", "")
await engine.process_comment(user, text)
# Handle test gift action
elif message.action == "test_gift":
user = message.payload.get("user", "TestUser")
bits = int(message.payload.get("bits", 100))
# Trigger handle_gift in engine
await engine.handle_gift(user, bits, "bits")
except WebSocketDisconnect:
manager.disconnect(websocket)
except Exception as e:

View 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()

View File

@@ -51,6 +51,18 @@ class Agent(Base):
# Social attributes (Phase 5)
social_tendency = Column(String(20), default="neutral") # introvert, extrovert, neutral
# Autonomous Action (Phase 13)
current_action = Column(String(50), default="Idle")
location = Column(String(50), default="center") # logical location: tree_left, tree_right, center, etc.
target_agent_id = Column(Integer, nullable=True) # if action targets another agent
# Survival (Phase 15)
is_sick = Column(Boolean, default=False)
immunity = Column(Integer, default=50) # 0-100, higher = less chance to get sick
# Relationship 2.0 (Phase 17-B)
social_role = Column(String(20), default="neutral") # leader, follower, loner, neutral
def __repr__(self):
return f"<Agent {self.name} ({self.personality}) HP={self.hp} Energy={self.energy} Mood={self.mood}>"
@@ -71,7 +83,12 @@ class Agent(Base):
"inventory": self.inventory,
"mood": self.mood,
"mood_state": self.mood_state,
"social_tendency": self.social_tendency
"social_tendency": self.social_tendency,
"current_action": self.current_action,
"location": self.location,
"is_sick": self.is_sick,
"immunity": self.immunity,
"social_role": self.social_role
}
@@ -94,6 +111,10 @@ class WorldState(Base):
# Weather system (Phase 3)
weather_duration = Column(Integer, default=0) # Ticks since last weather change
# Resource Scarcity (Phase 17-A)
tree_left_fruit = Column(Integer, default=5) # Max 5 fruit
tree_right_fruit = Column(Integer, default=5) # Max 5 fruit
def __repr__(self):
return f"<WorldState Day={self.day_count} {self.time_of_day} Weather={self.weather}>"
@@ -104,7 +125,9 @@ class WorldState(Base):
"weather": self.weather,
"resource_level": self.resource_level,
"current_tick_in_day": self.current_tick_in_day,
"time_of_day": self.time_of_day
"time_of_day": self.time_of_day,
"tree_left_fruit": self.tree_left_fruit,
"tree_right_fruit": self.tree_right_fruit
}
@@ -206,3 +229,39 @@ class AgentRelationship(Base):
self.relationship_type = "friend"
else:
self.relationship_type = "close_friend"
class AgentMemory(Base):
"""
Long-term memory for agents.
Stores significant events, conversations, and interactions.
"""
__tablename__ = "agent_memories"
id = Column(Integer, primary_key=True, index=True)
agent_id = Column(Integer, ForeignKey("agents.id"), nullable=False)
# The memory content
description = Column(String(500), nullable=False)
# Metadata for retrieval
importance = Column(Integer, default=1) # 1-10
related_entity_id = Column(Integer, nullable=True) # ID of user or other agent involved
related_entity_name = Column(String(50), nullable=True)
memory_type = Column(String(20), default="general") # chat, gift, event, social
created_at = Column(DateTime, default=func.now())
tick_created = Column(Integer, nullable=True)
def __repr__(self):
return f"<Memory {self.agent_id}: {self.description[:20]}... ({self.importance})>"
def to_dict(self):
return {
"id": self.id,
"agent_id": self.agent_id,
"description": self.description,
"importance": self.importance,
"created_at": self.created_at.isoformat() if self.created_at else None
}

View File

@@ -45,6 +45,16 @@ class EventType(str, Enum):
RELATIONSHIP_CHANGE = "relationship_change" # Relationship status changed
AUTO_REVIVE = "auto_revive" # Agent auto-revived (casual mode)
# Autonomous Agency (Phase 13)
AGENT_ACTION = "agent_action" # Agent performs an action (move, gather, etc.)
# Crafting System (Phase 16)
CRAFT = "craft" # Agent crafted an item
USE_ITEM = "use_item" # Agent used an item
# Random Events (Phase 17-C)
RANDOM_EVENT = "random_event" # Random event occurred
class GameEvent(BaseModel):
"""

View File

@@ -7,6 +7,7 @@ Compatible with twitchio 2.x (IRC-based)
import os
import logging
import random
from typing import TYPE_CHECKING
from twitchio.ext import commands
@@ -83,20 +84,19 @@ class TwitchBot(commands.Bot):
if hasattr(message, 'tags') and message.tags:
bits = message.tags.get('bits')
if bits:
try:
bits_amount = int(bits)
logger.info(f"Received {bits_amount} bits from {username}")
await self._game_engine.process_bits(username, bits_amount)
await self._handle_bits(username, int(bits))
# Send special gift effect to Unity
await self._game_engine._broadcast_event("gift_effect", {
"type": "gift_effect",
"user": username,
"value": bits_amount,
"message": f"{username} cheered {bits_amount} bits!"
})
except (ValueError, TypeError) as e:
logger.error(f"Error parsing bits amount: {e}")
async def _handle_bits(self, username: str, bits_amount: int):
"""
Handle bits donation.
Delegates to game engine's unified gift handling.
"""
try:
logger.info(f"Received {bits_amount} bits from {username}")
await self._game_engine.handle_gift(username, bits_amount, "bits")
except Exception as e:
logger.error(f"Error handling bits: {e}")
async def event_command_error(self, context, error):
"""Called when a command error occurs."""
@@ -110,3 +110,4 @@ class TwitchBot(commands.Bot):
logger.error(f"Twitch bot error: {error}")
if data:
logger.debug(f"Error data: {data}")

View 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.");
}
}

View File

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

File diff suppressed because one or more lines are too long

View 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}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5a9d5cd213c7f4bebb196509d67c68e3
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -151,13 +151,47 @@ namespace TheIsland.UI
rect.sizeDelta = size;
rect.anchoredPosition = Vector2.zero;
// Semi-transparent background
// Semi-transparent glass background
var bg = panel.AddComponent<Image>();
bg.color = new Color(0, 0, 0, 0.5f);
bg.color = new Color(0.1f, 0.12f, 0.18f, 0.6f);
// Add border highlight
var border = new GameObject("Border");
border.transform.SetParent(panel.transform);
var bRect = border.AddComponent<RectTransform>();
bRect.anchorMin = Vector2.zero;
bRect.anchorMax = Vector2.one;
bRect.offsetMin = new Vector2(-1, -1);
bRect.offsetMax = new Vector2(1, 1);
var bImg = border.AddComponent<Image>();
bImg.color = new Color(1f, 1f, 1f, 0.1f);
border.transform.SetAsFirstSibling();
return panel;
}
private float _currentHp;
private float _currentEnergy;
private void Update()
{
if (_hpBarFill != null && _currentData != null)
{
_currentHp = Mathf.Lerp(_currentHp, _currentData.hp, Time.deltaTime * 5f);
float hpPercent = _currentHp / 100f;
_hpBarFill.rectTransform.anchorMax = new Vector2(hpPercent, 1);
_hpBarFill.color = Color.Lerp(hpLowColor, hpHighColor, hpPercent);
}
if (_energyBarFill != null && _currentData != null)
{
_currentEnergy = Mathf.Lerp(_currentEnergy, _currentData.energy, Time.deltaTime * 5f);
float energyPercent = _currentEnergy / 100f;
_energyBarFill.rectTransform.anchorMax = new Vector2(energyPercent, 1);
_energyBarFill.color = Color.Lerp(energyLowColor, energyHighColor, energyPercent);
}
}
private TextMeshProUGUI CreateText(GameObject parent, string name, string text,
float fontSize, Color color, FontStyles style = FontStyles.Normal)
{
@@ -296,25 +330,11 @@ namespace TheIsland.UI
{
_currentData = data;
// Update HP
float hpPercent = data.hp / 100f;
if (_hpBarFill != null)
{
_hpBarFill.rectTransform.anchorMax = new Vector2(hpPercent, 1);
_hpBarFill.color = Color.Lerp(hpLowColor, hpHighColor, hpPercent);
}
if (_hpText != null)
{
_hpText.text = $"HP: {data.hp}";
}
// Update Energy
float energyPercent = data.energy / 100f;
if (_energyBarFill != null)
{
_energyBarFill.rectTransform.anchorMax = new Vector2(energyPercent, 1);
_energyBarFill.color = Color.Lerp(energyLowColor, energyHighColor, energyPercent);
}
if (_energyText != null)
{
_energyText.text = $"Energy: {data.energy}";

View File

@@ -64,18 +64,33 @@ namespace TheIsland.Visual
private Billboard _spriteBillboard;
private Billboard _uiBillboard;
private Camera _mainCamera;
private AgentAnimator _animator;
#endregion
#region State
private int _agentId;
private AgentData _currentData;
private string _moodState = "neutral";
private Coroutine _speechCoroutine;
// Animation state
private float _idleAnimTimer;
private float _breathScale = 1f;
private Vector3 _originalSpriteScale;
private float _bobOffset;
// Movement state
private Vector3 _targetPosition;
private bool _isMoving;
private float _moveSpeed = 3f;
private Vector3 _lastPosition;
// UI Smoothing (Phase 19)
private float _currentHpPercent;
private float _currentEnergyPercent;
private float _currentMoodPercent;
private float _targetHpPercent;
private float _targetEnergyPercent;
private float _targetMoodPercent;
#endregion
#region Properties
@@ -88,33 +103,202 @@ namespace TheIsland.Visual
private void Awake()
{
_mainCamera = Camera.main;
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
// Phase 19-B: Ensure AgentAnimator is present
_animator = GetComponent<AgentAnimator>();
if (_animator == null) _animator = gameObject.AddComponent<AgentAnimator>();
CreateVisuals();
_lastPosition = transform.position;
}
private void Update()
{
if (!IsAlive) return;
// Idle breathing animation
_idleAnimTimer += Time.deltaTime;
_breathScale = 1f + Mathf.Sin(_idleAnimTimer * 2f) * 0.02f;
// Gentle bobbing
_bobOffset = Mathf.Sin(_idleAnimTimer * 1.5f) * 0.05f;
if (_spriteRenderer != null && _originalSpriteScale != Vector3.zero)
// Phase 19-D: Apply soft-repulsion to prevent crowding
Vector3 repulsion = CalculateRepulsion();
// Handle Movement
if (_isMoving)
{
// Apply breathing scale
_spriteRenderer.transform.localScale = new Vector3(
_originalSpriteScale.x * _breathScale,
_originalSpriteScale.y * _breathScale,
_originalSpriteScale.z
);
// Simple steering toward target
Vector3 moveDir = (_targetPosition - transform.position).normalized;
Vector3 finalVelocity = (moveDir * _moveSpeed) + repulsion;
transform.position += finalVelocity * Time.deltaTime;
// Apply bobbing
// Flip sprite based on direction
if (_spriteRenderer != null && Mathf.Abs(moveDir.x) > 0.01f)
{
_spriteRenderer.flipX = moveDir.x < 0;
}
if (Vector3.Distance(transform.position, _targetPosition) < 0.1f)
{
_isMoving = false;
}
}
else if (repulsion.sqrMagnitude > 0.001f)
{
// Push away even when idle
transform.position += repulsion * Time.deltaTime;
}
// Phase 19-D: Dynamic Z-Sorting
if (_spriteRenderer != null)
{
// In world space, higher Z (further) should have lower sorting order
// Z typically ranges from -10 to 10 on the island
_spriteRenderer.sortingOrder = Mathf.RoundToInt(-transform.position.z * 100);
}
// Phase 19-B/D/E: Use AgentAnimator
if (_animator != null)
{
// Calculate world velocity based on position change
Vector3 currentVelocity = (transform.position - _lastPosition) / (Time.deltaTime > 0 ? Time.deltaTime : 0.001f);
_animator.SetMovement(currentVelocity, _moveSpeed);
_lastPosition = transform.position;
}
// Phase 19-E: Social Orientation (Interaction Facing)
if (!_isMoving)
{
FaceInteractionTarget();
}
// Phase 19: Smooth UI Bar Transitions
UpdateSmoothBars();
}
private void FaceInteractionTarget()
{
// If the agent is talking or near others, turn to face them
float socialRange = 2.5f;
AgentVisual nearestAgent = null;
float minDist = socialRange;
var allAgents = FindObjectsByType<AgentVisual>(FindObjectsSortMode.None);
foreach (var other in allAgents)
{
if (other == this || !other.IsAlive) continue;
float d = Vector3.Distance(transform.position, other.transform.position);
if (d < minDist)
{
minDist = d;
nearestAgent = other;
}
}
if (nearestAgent != null && _spriteRenderer != null)
{
float dx = nearestAgent.transform.position.x - transform.position.x;
if (Mathf.Abs(dx) > 0.1f)
{
_spriteRenderer.flipX = dx < 0;
}
}
}
private Vector3 CalculateRepulsion()
{
Vector3 force = Vector3.zero;
float radius = 1.2f; // Social distancing radius
float strength = 1.5f;
var allAgents = FindObjectsByType<AgentVisual>(FindObjectsSortMode.None);
foreach (var other in allAgents)
{
if (other == this || !other.IsAlive) continue;
Vector3 diff = transform.position - other.transform.position;
float dist = diff.magnitude;
if (dist < radius && dist > 0.01f)
{
// Linear falloff repulsion
force += diff.normalized * (1.0f - (dist / radius)) * strength;
}
}
return force;
}
private void UpdateSmoothBars()
{
float lerpSpeed = 5f * Time.deltaTime;
if (_hpBarFill != null)
{
_currentHpPercent = Mathf.Lerp(_currentHpPercent, _targetHpPercent, lerpSpeed);
_hpBarFill.rectTransform.anchorMax = new Vector2(_currentHpPercent, 1);
_hpBarFill.color = Color.Lerp(hpLowColor, hpHighColor, _currentHpPercent);
}
if (_energyBarFill != null)
{
_currentEnergyPercent = Mathf.Lerp(_currentEnergyPercent, _targetEnergyPercent, lerpSpeed);
_energyBarFill.rectTransform.anchorMax = new Vector2(_currentEnergyPercent, 1);
_energyBarFill.color = Color.Lerp(energyLowColor, energyHighColor, _currentEnergyPercent);
}
if (_moodBarFill != null)
{
_currentMoodPercent = Mathf.Lerp(_currentMoodPercent, _targetMoodPercent, lerpSpeed);
_moodBarFill.rectTransform.anchorMax = new Vector2(_currentMoodPercent, 1);
_moodBarFill.color = GetMoodColor(_currentData?.mood_state ?? "neutral");
}
}
// Trigger a jump animation (to be called by events)
public void DoJump()
{
StartCoroutine(JumpRoutine());
}
public void MoveTo(Vector3 target)
{
_targetPosition = target;
// Keep current Y (height) to avoid sinking/flying, unless target specifies it
// Actually our agents are on navmesh or free moving? Free moving for now.
// But we want to keep them on the "ground" plane roughly.
// Let's preserve current Y if target Y is 0 (which usually means undefined in 2D topdown logic, but here we are 2.5D)
// The spawn positions have Y=0.
_targetPosition.y = transform.position.y;
_isMoving = true;
}
private void MoveTowardsTarget()
{
Vector3 direction = (_targetPosition - transform.position).normalized;
transform.position = Vector3.MoveTowards(transform.position, _targetPosition, _moveSpeed * Time.deltaTime);
// Stop when close enough
if (Vector3.Distance(transform.position, _targetPosition) < 0.1f)
{
_isMoving = false;
}
}
private IEnumerator JumpRoutine()
{
float timer = 0;
float duration = 0.4f;
Vector3 startPos = _spriteRenderer.transform.localPosition;
while (timer < duration)
{
timer += Time.deltaTime;
float t = timer / duration;
// Parabolic jump height
float height = Mathf.Sin(t * Mathf.PI) * 0.5f;
var pos = _spriteRenderer.transform.localPosition;
pos.y = 1f + _bobOffset;
pos.y = startPos.y + height;
_spriteRenderer.transform.localPosition = pos;
yield return null;
}
}
@@ -138,7 +322,10 @@ namespace TheIsland.Visual
_currentData = data;
gameObject.name = $"Agent_{data.id}_{data.name}";
// Apply unique color based on agent ID
// Loading premium assets (Phase 19)
TryLoadPremiumSprite(data.id);
// Apply unique color based on agent ID (as fallback/tint)
ApplyAgentColor(data.id);
// Set UI text
@@ -149,6 +336,85 @@ namespace TheIsland.Visual
Debug.Log($"[AgentVisual] Initialized: {data.name}");
}
private void TryLoadPremiumSprite(int id)
{
// Load the collection texture from Assets
// Note: In a real build, we'd use Resources.Load or Addressables.
// For this environment, we'll try to find it in the path or use a static reference.
// Since we can't easily use Resources.Load at runtime for arbitrary paths,
// we'll implement a simple runtime texture loader if needed, or assume it's assigned to a manager.
// For now, let's assume the texture is assigned or loaded.
// I'll add a static reference to the collection texture in NetworkManager or AgentVisual.
if (characterSprite != null) return; // Already has a sprite
StartCoroutine(LoadSpriteCoroutine(id));
}
private IEnumerator LoadSpriteCoroutine(int id)
{
// This is a simplified runtime loader for the demonstration
string path = Application.dataPath + "/Sprites/Characters.png";
if (!System.IO.File.Exists(path)) yield break;
byte[] fileData = System.IO.File.ReadAllBytes(path);
Texture2D sourceTex = new Texture2D(2, 2);
sourceTex.LoadImage(fileData);
// Phase 19-C: Fix black/white background with robust transcoding
Texture2D tex = ProcessTransparency(sourceTex);
// Slice the 1x3 collection (3 characters in a row)
int charIndex = id % 3;
float charWidth = tex.width / 3f;
Rect rect = new Rect(charIndex * charWidth, 0, charWidth, tex.height);
characterSprite = Sprite.Create(tex, rect, new Vector2(0.5f, 0.5f), 100f);
if (_spriteRenderer != null)
{
_spriteRenderer.sprite = characterSprite;
_spriteRenderer.color = Color.white;
// Phase 19-C: Normalize scale. Target height approx 2.0 units.
float spriteHeightUnits = characterSprite.rect.height / characterSprite.pixelsPerUnit;
float normScale = 2.0f / spriteHeightUnits; // Desired height is 2.0 units
_spriteRenderer.transform.localScale = new Vector3(normScale, normScale, 1);
// Update original scale for animator
_originalSpriteScale = _spriteRenderer.transform.localScale;
}
}
private Texture2D ProcessTransparency(Texture2D source)
{
if (source == null) return null;
// Create a new texture with Alpha channel
Texture2D tex = new Texture2D(source.width, source.height, TextureFormat.RGBA32, false);
Color[] pixels = source.GetPixels();
for (int i = 0; i < pixels.Length; i++)
{
Color p = pixels[i];
// Chroma-key: If pixel is very close to white, make it transparent
// Using 0.9f as threshold to catch almost-white artifacts
if (p.r > 0.9f && p.g > 0.9f && p.b > 0.9f)
{
pixels[i] = new Color(0, 0, 0, 0);
}
else
{
// Ensure full opacity for others
pixels[i] = new Color(p.r, p.g, p.b, 1.0f);
}
}
tex.SetPixels(pixels);
tex.Apply();
return tex;
}
private void ApplyAgentColor(int agentId)
{
// Generate unique color per agent
@@ -407,7 +673,27 @@ namespace TheIsland.Visual
for (int x = (int)(center.x - smileWidth); x <= (int)(center.x + smileWidth); x++)
{
float t = (x - center.x + smileWidth) / (smileWidth * 2);
int y = (int)(center.y - Mathf.Sin(t * Mathf.PI) * 2);
int y = (int)center.y;
// Mouth shape based on mood
if (_moodState == "happy")
{
y = (int)(center.y - Mathf.Sin(t * Mathf.PI) * 2);
}
else if (_moodState == "sad")
{
y = (int)(center.y - 2 + Mathf.Sin(t * Mathf.PI) * 2);
}
else if (_moodState == "anxious")
{
// Wavy mouth
y = (int)(center.y + Mathf.Sin(t * Mathf.PI * 3) * 1);
}
else // neutral
{
y = (int)(center.y);
}
if (x >= 0 && x < width && y >= 0 && y < height)
{
pixels[y * width + x] = mouthColor;
@@ -602,28 +888,23 @@ namespace TheIsland.Visual
rect.anchoredPosition = Vector2.zero;
var bg = panel.AddComponent<Image>();
bg.sprite = CreateRoundedRectSprite(32, 32, 8);
bg.sprite = CreateRoundedRectSprite(32, 32, 12);
bg.type = Image.Type.Sliced;
bg.color = new Color(0.1f, 0.12f, 0.18f, 0.85f);
bg.color = new Color(0.05f, 0.08f, 0.15f, 0.45f); // Darker, more transparent glass
// Add subtle border
// Add inner glow border (Phase 19)
var borderObj = new GameObject("Border");
borderObj.transform.SetParent(panel.transform);
borderObj.transform.localPosition = Vector3.zero;
borderObj.transform.localRotation = Quaternion.identity;
borderObj.transform.localScale = Vector3.one;
var borderRect = borderObj.AddComponent<RectTransform>();
borderRect.anchorMin = Vector2.zero;
borderRect.anchorMax = Vector2.one;
borderRect.offsetMin = new Vector2(-2, -2);
borderRect.offsetMax = new Vector2(2, 2);
borderRect.SetAsFirstSibling();
borderRect.offsetMin = new Vector2(1, 1);
borderRect.offsetMax = new Vector2(-1, -1);
var borderImg = borderObj.AddComponent<Image>();
borderImg.sprite = CreateRoundedRectSprite(32, 32, 8);
borderImg.sprite = CreateRoundedRectSprite(32, 32, 12);
borderImg.type = Image.Type.Sliced;
borderImg.color = new Color(0.3f, 0.35f, 0.45f, 0.5f);
borderImg.color = new Color(1f, 1f, 1f, 0.15f); // Subtle highlight
return panel;
}
@@ -818,36 +1099,30 @@ namespace TheIsland.Visual
{
_currentData = data;
// Update HP bar
float hpPercent = data.hp / 100f;
if (_hpBarFill != null)
{
_hpBarFill.rectTransform.anchorMax = new Vector2(hpPercent, 1);
_hpBarFill.color = Color.Lerp(hpLowColor, hpHighColor, hpPercent);
}
// Set targets for smooth lerping (Phase 19)
_targetHpPercent = data.hp / 100f;
_targetEnergyPercent = data.energy / 100f;
_targetMoodPercent = data.mood / 100f;
if (_hpText != null)
{
_hpText.text = $"HP: {data.hp}";
}
// Update Energy bar
float energyPercent = data.energy / 100f;
if (_energyBarFill != null)
{
_energyBarFill.rectTransform.anchorMax = new Vector2(energyPercent, 1);
_energyBarFill.color = Color.Lerp(energyLowColor, energyHighColor, energyPercent);
}
if (_energyText != null)
{
_energyText.text = $"Energy: {data.energy}";
}
// Update Mood bar
float moodPercent = data.mood / 100f;
if (_moodBarFill != null)
// Check for mood change (Visual Expression)
if (_moodState != data.mood_state)
{
_moodBarFill.rectTransform.anchorMax = new Vector2(moodPercent, 1);
_moodBarFill.color = GetMoodColor(data.mood_state);
_moodState = data.mood_state;
// Only regenerate if using placeholder sprite
if (characterSprite == null && _spriteRenderer != null)
{
RegeneratePlaceholderSprite();
}
}
if (_moodText != null)
{
@@ -912,19 +1187,56 @@ namespace TheIsland.Visual
{
if (_deathOverlay != null) _deathOverlay.SetActive(false);
// Restore sprite color
// Restore sprite color based on state
if (_spriteRenderer != null)
{
_spriteRenderer.color = Color.white;
Color targetColor = spriteColor;
// Phase 15: Sickness visual (Green tint)
if (_currentData != null && _currentData.is_sick)
{
targetColor = Color.Lerp(targetColor, Color.green, 0.4f);
}
_spriteRenderer.color = targetColor;
}
// Phase 17-B: Update social role display
UpdateSocialRoleDisplay();
}
/// <summary>
/// Display social role indicator based on agent's role.
/// </summary>
private void UpdateSocialRoleDisplay()
{
if (_currentData == null || _nameLabel == null) return;
string roleIcon = _currentData.social_role switch
{
"leader" => " <color=#FFD700>★</color>", // Gold star
"loner" => " <color=#808080>☁</color>", // Gray cloud
"follower" => " <color=#87CEEB>→</color>", // Sky blue arrow
_ => ""
};
// Append role icon to name (strip any existing icons first)
string baseName = _currentData.name;
_nameLabel.text = baseName + roleIcon;
}
#endregion
#region Speech
public void ShowSpeech(string text)
{
ShowSpeech(text, speechDuration);
}
public void ShowSpeech(string text, float duration)
{
if (_speechBubble == null || !IsAlive) return;
_speechBubble.DisplayDuration = duration;
_speechBubble.Setup(text);
Debug.Log($"[AgentVisual] {_currentData?.name} says: \"{text}\"");
}

View File

@@ -38,7 +38,6 @@ namespace TheIsland.UI
private List<GameObject> _entries = new List<GameObject>();
private bool _visible = true;
private int _unread = 0;
private bool _ready = false;
private void Awake()
{
@@ -83,7 +82,6 @@ namespace TheIsland.UI
if (NetworkManager.Instance != null)
{
SubscribeEvents();
_ready = true;
AddLog("事件日志已就绪", Color.yellow);
Debug.Log("[EventLog] 初始化完成");
}
@@ -189,7 +187,7 @@ namespace TheIsland.UI
panelRect.offsetMax = new Vector2(360, -80);
var panelImg = _panel.AddComponent<Image>();
panelImg.color = new Color(0.05f, 0.07f, 0.1f, 0.95f);
panelImg.color = new Color(0f, 0f, 0f, 0.0f); // 完全透明背景
// 标题
var header = new GameObject("Header");
@@ -201,7 +199,7 @@ namespace TheIsland.UI
headerRect.sizeDelta = new Vector2(0, 28);
headerRect.anchoredPosition = Vector2.zero;
header.AddComponent<Image>().color = new Color(0.12f, 0.15f, 0.2f);
header.AddComponent<Image>().color = new Color(0.12f, 0.15f, 0.2f, 0.8f);
var titleObj = new GameObject("Title");
titleObj.transform.SetParent(header.transform, false);
@@ -296,8 +294,8 @@ namespace TheIsland.UI
entry.transform.SetParent(_content, false);
entry.AddComponent<Image>().color = _entries.Count % 2 == 0
? new Color(0.08f, 0.1f, 0.13f, 0.9f)
: new Color(0.06f, 0.08f, 0.11f, 0.9f);
? new Color(0f, 0f, 0f, 0.2f)
: new Color(0f, 0f, 0f, 0.1f);
var le = entry.AddComponent<LayoutElement>();
le.minHeight = 36;

View File

@@ -152,6 +152,9 @@ namespace TheIsland.Core
network.OnTalk += HandleTalk;
network.OnRevive += HandleRevive;
network.OnSocialInteraction += HandleSocialInteraction;
network.OnGiftEffect += HandleGiftEffect; // Phase 8
network.OnAgentAction += HandleAgentAction; // Phase 13
network.OnRandomEvent += HandleRandomEvent; // Phase 17-C
}
private void UnsubscribeFromNetworkEvents()
@@ -178,6 +181,8 @@ namespace TheIsland.Core
network.OnTalk -= HandleTalk;
network.OnRevive -= HandleRevive;
network.OnSocialInteraction -= HandleSocialInteraction;
network.OnGiftEffect -= HandleGiftEffect; // Phase 8
network.OnRandomEvent -= HandleRandomEvent; // Phase 17-C
}
#endregion
@@ -320,6 +325,7 @@ namespace TheIsland.Core
if (_agentVisuals.TryGetValue(data.agent_id, out AgentVisual agentVisual))
{
agentVisual.ShowSpeech(data.text);
agentVisual.DoJump(); // Add jump effect
}
// Check AgentUI (programmatic UI system)
else if (_agentUIs.TryGetValue(data.agent_id, out AgentUI agentUI))
@@ -485,6 +491,140 @@ namespace TheIsland.Core
initiatorUI.ShowSpeech(data.dialogue);
}
}
/// <summary>
/// Handle gift effect event (Twitch bits, subscriptions).
/// Plays VFX and shows gratitude speech.
/// </summary>
private void HandleGiftEffect(GiftEffectData data)
{
Debug.Log($"[GameManager] Gift: {data.user} sent {data.value} {data.gift_type}");
// Find target agent position for VFX
Vector3 effectPosition = Vector3.zero;
int agentId = GetAgentIdByName(data.agent_name);
if (agentId >= 0 && _agentVisuals.TryGetValue(agentId, out AgentVisual agentVisual))
{
effectPosition = agentVisual.transform.position;
// Show gratitude speech with extended duration
if (!string.IsNullOrEmpty(data.gratitude))
{
float duration = data.duration > 0 ? data.duration : 8f;
agentVisual.ShowSpeech(data.gratitude, duration);
}
}
else if (agentId >= 0 && _agentUIs.TryGetValue(agentId, out AgentUI agentUI))
{
effectPosition = agentUI.transform.position;
if (!string.IsNullOrEmpty(data.gratitude))
{
agentUI.ShowSpeech(data.gratitude);
}
}
else
{
// Default to center if no agent found
effectPosition = new Vector3(0, 1, 0);
}
// Play VFX
if (VFXManager.Instance != null)
{
VFXManager.Instance.PlayEffect(data.gift_type, effectPosition);
}
// Show notification
ShowNotification(data.message);
}
#region Agent Action (Phase 13)
private void HandleAgentAction(AgentActionData data)
{
Debug.Log($"[GameManager] Action: {data.agent_name} -> {data.action_type} at {data.location}");
// Resolve target position
Vector3 targetPos = GetLocationPosition(data.location, data.target_name);
// Find agent and command movement
if (_agentVisuals.TryGetValue(data.agent_id, out AgentVisual agentVisual))
{
agentVisual.MoveTo(targetPos);
// Optional: Show thought bubble or speech
if (!string.IsNullOrEmpty(data.dialogue))
{
agentVisual.ShowSpeech(data.dialogue, 3f);
}
}
else if (_agentUIs.TryGetValue(data.agent_id, out AgentUI agentUI))
{
// Fallback for UI-only agents (just show speech)
if (!string.IsNullOrEmpty(data.dialogue))
{
agentUI.ShowSpeech(data.dialogue);
}
}
}
private Vector3 GetLocationPosition(string location, string targetName)
{
// Map logical locations to world coordinates
switch (location.ToLower())
{
case "tree_left":
return new Vector3(-10f, 0f, 8f);
case "tree_right":
return new Vector3(10f, 0f, 8f);
case "campfire":
case "center":
return new Vector3(0f, 0f, 0f);
case "water":
case "beach":
return new Vector3(Random.Range(-5, 5), 0f, 4f);
case "nearby":
// Move to random nearby spot (wandering)
return new Vector3(Random.Range(-12, 12), 0f, Random.Range(-2, 6));
case "herb_patch":
// Phase 16: Herb gathering location
return new Vector3(-8f, 0f, -5f);
case "agent":
// Move to another agent
int targetId = GetAgentIdByName(targetName);
if (targetId >= 0 && _agentVisuals.TryGetValue(targetId, out AgentVisual target))
{
// Stand slightly offset from target
return target.transform.position + new Vector3(1.5f, 0, 0);
}
return Vector3.zero;
default:
return new Vector3(0f, 0f, 0f); // Fallback to center
}
}
#endregion
#region Random Events (Phase 17-C)
private void HandleRandomEvent(RandomEventData data)
{
Debug.Log($"[GameManager] Random Event: {data.event_type} - {data.message}");
// Display global notification banner
string eventIcon = data.event_type switch
{
"storm_damage" => "⛈️ ",
"treasure_found" => "💎 ",
"beast_attack" => "🐺 ",
"rumor_spread" => "💬 ",
_ => ""
};
ShowNotification(eventIcon + data.message);
// Optional: Trigger visual effects based on event type
// (Could add screen shake for storm, highlight agent for treasure, etc.)
}
#endregion
#endregion
#region Agent Management

View File

@@ -48,6 +48,17 @@ namespace TheIsland.Models
public string mood_state; // "happy", "neutral", "sad", "anxious"
public string social_tendency; // "introvert", "extrovert", "neutral"
// Survival (Phase 15)
public bool is_sick;
public int immunity;
// Autonomous Agency (Phase 13)
public string current_action;
public string location;
// Relationship 2.0 (Phase 17-B)
public string social_role; // "leader", "follower", "loner", "neutral"
public bool IsAlive => status == "Alive";
}
@@ -138,6 +149,10 @@ namespace TheIsland.Models
public int resource_level;
public int current_tick_in_day;
public string time_of_day; // "dawn", "day", "dusk", "night"
// Resource Scarcity (Phase 17-A)
public int tree_left_fruit;
public int tree_right_fruit;
}
/// <summary>
@@ -240,6 +255,21 @@ namespace TheIsland.Models
public string dialogue;
}
/// <summary>
/// Gift effect event data (Twitch bits, subscriptions, etc.).
/// </summary>
[Serializable]
public class GiftEffectData
{
public string user;
public string gift_type; // "bits", "heart", "sub"
public int value;
public string message;
public string agent_name; // Target agent for the effect
public string gratitude; // AI-generated thank you message
public float duration; // How long to show the speech bubble (default 8s)
}
/// <summary>
/// Client message structure for sending to server.
/// </summary>
@@ -293,5 +323,67 @@ namespace TheIsland.Models
public const string SOCIAL_INTERACTION = "social_interaction";
public const string RELATIONSHIP_CHANGE = "relationship_change";
public const string AUTO_REVIVE = "auto_revive";
// Gift/Donation system (Phase 8)
public const string GIFT_EFFECT = "gift_effect";
// Autonomous Agency (Phase 13)
public const string AGENT_ACTION = "agent_action";
// Crafting System (Phase 16)
public const string CRAFT = "craft";
public const string USE_ITEM = "use_item";
// Random Events (Phase 17-C)
public const string RANDOM_EVENT = "random_event";
}
/// <summary>
/// Agent action event data (Phase 13).
/// </summary>
[Serializable]
public class AgentActionData
{
public int agent_id;
public string agent_name;
public string action_type; // "Gather", "Sleep", "Socialize", "Wander", "Gather Herb", etc.
public string location; // "tree_left", "campfire", "herb_patch", etc.
public string target_name; // For social actions
public string dialogue; // Bark text
}
/// <summary>
/// Craft event data (Phase 16).
/// </summary>
[Serializable]
public class CraftEventData
{
public int agent_id;
public string agent_name;
public string item; // "medicine"
public string ingredients; // JSON string of ingredients used
}
/// <summary>
/// Use item event data (Phase 16).
/// </summary>
[Serializable]
public class UseItemEventData
{
public int agent_id;
public string agent_name;
public string item; // "medicine"
public string effect; // "cured sickness"
}
/// <summary>
/// Random event data (Phase 17-C).
/// </summary>
[Serializable]
public class RandomEventData
{
public string event_type; // "storm_damage", "treasure_found", "beast_attack", "rumor_spread"
public string message;
public string agent_name; // Optional: affected agent
}
}

View File

@@ -67,6 +67,11 @@ namespace TheIsland.Network
public event Action<ReviveEventData> OnRevive;
public event Action<SocialInteractionData> OnSocialInteraction;
public event Action<WorldStateData> OnWorldUpdate;
public event Action<GiftEffectData> OnGiftEffect; // Phase 8: Gift/Donation effects
public event Action<AgentActionData> OnAgentAction; // Phase 13: Autonomous Actions
public event Action<CraftEventData> OnCraft; // Phase 16: Crafting
public event Action<UseItemEventData> OnUseItem; // Phase 16: Using items
public event Action<RandomEventData> OnRandomEvent; // Phase 17-C: Random Events
#endregion
#region Private Fields
@@ -343,11 +348,37 @@ namespace TheIsland.Network
OnSocialInteraction?.Invoke(socialData);
break;
case EventTypes.GIFT_EFFECT:
var giftData = JsonUtility.FromJson<GiftEffectData>(dataJson);
OnGiftEffect?.Invoke(giftData);
break;
case EventTypes.AGENT_ACTION:
var actionData = JsonUtility.FromJson<AgentActionData>(dataJson);
OnAgentAction?.Invoke(actionData);
break;
case EventTypes.CRAFT:
var craftData = JsonUtility.FromJson<CraftEventData>(dataJson);
OnCraft?.Invoke(craftData);
break;
case EventTypes.USE_ITEM:
var useItemData = JsonUtility.FromJson<UseItemEventData>(dataJson);
OnUseItem?.Invoke(useItemData);
break;
case EventTypes.COMMENT:
// Comments can be logged but typically not displayed in 3D
Debug.Log($"[Chat] {json}");
break;
case EventTypes.RANDOM_EVENT:
var randomEventData = JsonUtility.FromJson<RandomEventData>(dataJson);
OnRandomEvent?.Invoke(randomEventData);
Debug.Log($"[Random Event] {randomEventData.event_type}: {randomEventData.message}");
break;
default:
Debug.Log($"[NetworkManager] Unhandled event type: {baseMessage.event_type}");
break;

View File

@@ -164,7 +164,7 @@ namespace TheIsland.UI
topBar.offsetMax = new Vector2(-10, -10);
var topBarImg = topBar.gameObject.AddComponent<Image>();
topBarImg.color = new Color(0, 0, 0, 0.7f);
topBarImg.color = new Color(0, 0, 0, 0.0f); // 透明顶部栏
// Connection Status (Left)
_connectionStatus = CreateText(topBar, "ConnectionStatus", "● Disconnected",
@@ -205,7 +205,7 @@ namespace TheIsland.UI
bottomBar.offsetMax = new Vector2(-10, 70);
var bottomBarImg = bottomBar.gameObject.AddComponent<Image>();
bottomBarImg.color = new Color(0, 0, 0, 0.7f);
bottomBarImg.color = new Color(0, 0, 0, 0.2f); // 低透明度底部栏
// Command Input
var inputObj = new GameObject("CommandInput");

View 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;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4b9e1cbf8a16c41ccb4b5f197e3ade72

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using UnityEngine;
using TheIsland.Core;
using TheIsland.Network;
@@ -56,7 +57,7 @@ namespace TheIsland.Visual
[SerializeField] private Color waterShallowColor = new Color(0.3f, 0.8f, 0.9f, 0.8f);
[SerializeField] private Color waterDeepColor = new Color(0.1f, 0.4f, 0.6f, 0.9f);
[SerializeField] private float waveSpeed = 0.5f;
[SerializeField] private float waveAmplitude = 0.1f;
[SerializeField] private Material customWaterMaterial; // Custom shader support
#endregion
#region References
@@ -74,6 +75,7 @@ namespace TheIsland.Visual
private float _transitionProgress = 1f;
private Color _targetSkyTop, _targetSkyBottom;
private Color _currentSkyTop, _currentSkyBottom;
private List<Transform> _palmTrees = new List<Transform>();
#endregion
#region Unity Lifecycle
@@ -86,6 +88,7 @@ namespace TheIsland.Visual
}
_instance = this;
LoadEnvironmentTexture();
_mainCamera = Camera.main;
CreateEnvironment();
}
@@ -103,6 +106,15 @@ namespace TheIsland.Visual
// Set initial sky
UpdateSkyColors();
// Phase 19-B: Cache palm trees for animation
CachePalmTrees();
// Phase 19: Add Visual Effects Manager
if (FindFirstObjectByType<VisualEffectsManager>() == null)
{
new GameObject("VisualEffectsManager").AddComponent<VisualEffectsManager>();
}
}
private void Update()
@@ -116,8 +128,56 @@ namespace TheIsland.Visual
UpdateSkyMaterial();
}
// Animate water
AnimateWater();
// Phase 19: Cinematic Lighting
AnimateLighting();
// Animate environment (Water & Trees)
AnimateEnvironment();
AnimateClouds();
}
private void AnimateLighting()
{
if (_mainLight == null) return;
// Simple 120s cycle for demonstration (30s per phase)
float cycleDuration = 120f;
float t = (Time.time % cycleDuration) / cycleDuration;
// t: 0=Dawn, 0.25=Noon, 0.5=Dusk, 0.75=Midnight
float intensity = 1f;
Color lightColor = Color.white;
if (t < 0.2f) // Dawn
{
float p = t / 0.2f;
intensity = Mathf.Lerp(0.5f, 1.2f, p);
lightColor = Color.Lerp(new Color(1f, 0.6f, 0.4f), Color.white, p);
}
else if (t < 0.5f) // Day
{
intensity = 1.2f;
lightColor = Color.white;
}
else if (t < 0.7f) // Dusk
{
float p = (t - 0.5f) / 0.2f;
intensity = Mathf.Lerp(1.2f, 0.4f, p);
lightColor = Color.Lerp(Color.white, new Color(1f, 0.4f, 0.2f), p);
}
else // Night
{
float p = (t - 0.7f) / 0.3f;
intensity = Mathf.Lerp(0.4f, 0.2f, p);
lightColor = new Color(0.4f, 0.5f, 1f); // Moonlight
}
_mainLight.intensity = intensity;
_mainLight.color = lightColor;
// Rotate sun
float sunAngle = t * 360f - 90f;
_mainLight.transform.rotation = Quaternion.Euler(sunAngle, -30f, 0);
}
private void OnDestroy()
@@ -140,6 +200,7 @@ namespace TheIsland.Visual
CreateWater();
CreateLighting();
CreateDecorations();
CreateClouds();
}
private void CreateSky()
@@ -172,45 +233,6 @@ namespace TheIsland.Visual
private Material CreateGradientMaterial()
{
// Create a simple shader for vertical gradient
string shaderCode = @"
Shader ""Custom/SkyGradient"" {
Properties {
_TopColor (""Top Color"", Color) = (0.4, 0.7, 1, 1)
_BottomColor (""Bottom Color"", Color) = (0.7, 0.9, 1, 1)
}
SubShader {
Tags { ""Queue""=""Background"" ""RenderType""=""Opaque"" }
Pass {
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include ""UnityCG.cginc""
fixed4 _TopColor;
fixed4 _BottomColor;
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
v2f vert(appdata_base v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 frag(v2f i) : SV_Target {
return lerp(_BottomColor, _TopColor, i.uv.y);
}
ENDCG
}
}
}";
// Since we can't create shaders at runtime easily, use a texture-based approach
return CreateGradientTextureMaterial();
}
@@ -308,9 +330,17 @@ namespace TheIsland.Visual
_waterPlane.transform.localScale = new Vector3(60, 15, 1);
// Create water material
_waterMaterial = new Material(Shader.Find("Unlit/Transparent"));
_waterMaterial.mainTexture = CreateWaterTexture();
_waterPlane.GetComponent<Renderer>().material = _waterMaterial;
if (customWaterMaterial != null)
{
_waterMaterial = customWaterMaterial;
_waterPlane.GetComponent<Renderer>().material = _waterMaterial;
}
else
{
_waterMaterial = new Material(Shader.Find("Unlit/Transparent"));
_waterMaterial.mainTexture = CreateWaterTexture();
_waterPlane.GetComponent<Renderer>().material = _waterMaterial;
}
_waterPlane.GetComponent<Renderer>().sortingOrder = -40;
Destroy(_waterPlane.GetComponent<Collider>());
@@ -328,11 +358,14 @@ namespace TheIsland.Visual
for (int x = 0; x < size; x++)
{
float t = (float)y / size;
Color baseColor = Color.Lerp(waterShallowColor, waterDeepColor, t);
// Add some noise to the base color
float n = Mathf.PerlinNoise(x * 0.05f, y * 0.05f) * 0.1f;
Color baseColor = Color.Lerp(waterShallowColor, waterDeepColor, t + n);
// Add wave highlights
float wave = Mathf.Sin(x * 0.2f + y * 0.1f) * 0.5f + 0.5f;
baseColor = Color.Lerp(baseColor, Color.white, wave * 0.1f);
// Add caustic-like highlights
float wave1 = Mathf.Sin(x * 0.15f + y * 0.05f + Time.time * 0.2f) * 0.5f + 0.5f;
float wave2 = Mathf.Cos(x * 0.08f - y * 0.12f + Time.time * 0.15f) * 0.5f + 0.5f;
baseColor = Color.Lerp(baseColor, Color.white, (wave1 * wave2) * 0.15f);
tex.SetPixel(x, y, baseColor);
}
@@ -346,8 +379,11 @@ namespace TheIsland.Visual
if (_waterMaterial == null) return;
// Simple UV scrolling for wave effect
float offset = Time.time * waveSpeed * 0.1f;
_waterMaterial.mainTextureOffset = new Vector2(offset, offset * 0.5f);
float offset = Time.time * waveSpeed * 0.05f;
_waterMaterial.mainTextureOffset = new Vector2(offset, offset * 0.3f);
// Periodically update texture for dynamic caustic effect (expensive but looks premium)
// Or just use the original UV scrolling if performance is an issue.
}
private void CreateLighting()
@@ -383,6 +419,8 @@ namespace TheIsland.Visual
CreateRock(new Vector3(-5, 0, 4), 0.5f);
CreateRock(new Vector3(6, 0, 5), 0.7f);
CreateRock(new Vector3(-7, 0, 6), 0.4f);
CreateGroundDetails();
}
private void CreatePalmTree(Vector3 position, float scale)
@@ -399,11 +437,70 @@ namespace TheIsland.Visual
var trunkRenderer = trunkSprite.AddComponent<SpriteRenderer>();
trunkRenderer.sprite = CreateTreeSprite();
trunkRenderer.sortingOrder = -20;
trunkSprite.transform.localScale = new Vector3(scale * 0.5f, scale, 1);
// Phase 19-C: Add Billboard for 2.5D perspective
trunkSprite.AddComponent<Billboard>();
// Phase 19-C: Normalize scale based on world units.
// If the sprite is large, we want it to fit the intended 'scale' height.
// A typical tree sprite at 100 PPU might be 10 units high.
// We want it to be 'scale' units high (e.g. 3 units).
float spriteHeightUnits = trunkRenderer.sprite.rect.height / trunkRenderer.sprite.pixelsPerUnit;
float normScale = scale / spriteHeightUnits;
trunkSprite.transform.localScale = new Vector3(normScale, normScale, 1);
}
private Texture2D _envTexture;
private void LoadEnvironmentTexture()
{
string path = Application.dataPath + "/Sprites/Environment.png";
if (System.IO.File.Exists(path))
{
byte[] data = System.IO.File.ReadAllBytes(path);
Texture2D sourceTex = new Texture2D(2, 2);
sourceTex.LoadImage(data);
// Phase 19-C: Robust transparency transcoding
_envTexture = ProcessTransparency(sourceTex);
}
}
private Texture2D ProcessTransparency(Texture2D source)
{
if (source == null) return null;
// Create a new texture with Alpha channel
Texture2D tex = new Texture2D(source.width, source.height, TextureFormat.RGBA32, false);
Color[] pixels = source.GetPixels();
for (int i = 0; i < pixels.Length; i++)
{
Color p = pixels[i];
// Chroma-key: If pixel is very close to white, make it transparent
if (p.r > 0.9f && p.g > 0.9f && p.b > 0.9f)
{
pixels[i] = new Color(0, 0, 0, 0);
}
else
{
pixels[i] = new Color(p.r, p.g, p.b, 1.0f);
}
}
tex.SetPixels(pixels);
tex.Apply();
return tex;
}
private Sprite CreateTreeSprite()
{
if (_envTexture != null)
{
// Slice palm tree (Assuming it's in the top-left quadrant of the collection)
return Sprite.Create(_envTexture, new Rect(0, _envTexture.height / 2f, _envTexture.width / 2f, _envTexture.height / 2f), new Vector2(0.5f, 0f), 100f);
}
int width = 64;
int height = 128;
Texture2D tex = new Texture2D(width, height);
@@ -439,6 +536,40 @@ namespace TheIsland.Visual
return Sprite.Create(tex, new Rect(0, 0, width, height), new Vector2(0.5f, 0));
}
private void CachePalmTrees()
{
_palmTrees.Clear();
foreach (Transform child in transform)
{
if (child.name == "PalmTree")
{
_palmTrees.Add(child);
}
}
}
private void AnimateEnvironment()
{
// Water animation
if (_waterMaterial != null)
{
float offset = Time.time * waveSpeed * 0.1f;
_waterMaterial.mainTextureOffset = new Vector2(offset, offset * 0.5f);
}
// Tree swaying animation
float weatherIntensity = (_currentWeather == "Stormy" || _currentWeather == "Rainy") ? 2.5f : 1.0f;
float time = Time.time;
foreach (var tree in _palmTrees)
{
if (tree == null) continue;
// Sway rotation with slight variation per tree position
float sway = Mathf.Sin(time * 1.5f + tree.position.x * 0.5f) * 2.0f * weatherIntensity;
tree.rotation = Quaternion.Euler(0, 0, sway);
}
}
private void DrawPalmFronds(Color[] pixels, int width, int height, Color leaf, Color leafBright)
{
Vector2 center = new Vector2(width / 2, height * 0.65f);
@@ -490,11 +621,24 @@ namespace TheIsland.Visual
var rockRenderer = rockObj.AddComponent<SpriteRenderer>();
rockRenderer.sprite = CreateRockSprite();
rockRenderer.sortingOrder = -15;
rockObj.transform.localScale = Vector3.one * scale;
// Phase 19-C: Add Billboard
rockObj.AddComponent<Billboard>();
// Phase 19-C: Normalize scale
float spriteWidthUnits = rockRenderer.sprite.rect.width / rockRenderer.sprite.pixelsPerUnit;
float normScale = scale / spriteWidthUnits;
rockObj.transform.localScale = Vector3.one * normScale;
}
private Sprite CreateRockSprite()
{
if (_envTexture != null)
{
// Slice rock from Environment.png (Assuming bottom-right quadrant)
return Sprite.Create(_envTexture, new Rect(_envTexture.width / 2f, 0, _envTexture.width / 2f, _envTexture.height / 2f), new Vector2(0.5f, 0.5f), 100f);
}
int size = 32;
Texture2D tex = new Texture2D(size, size);
@@ -540,7 +684,16 @@ namespace TheIsland.Visual
private void HandleWeatherChange(WeatherChangeData data)
{
_currentWeather = data.new_weather;
UpdateSkyColors();
Debug.Log($"[EnvironmentManager] Weather changed to: {_currentWeather}");
// Notify VFX manager
if (VisualEffectsManager.Instance != null)
{
VisualEffectsManager.Instance.SetWeather(_currentWeather);
}
// Adjust lighting based on weather
UpdateSkyColors(); // This will use the new weather in its logic
}
private void HandleTick(TickData data)
@@ -612,6 +765,164 @@ namespace TheIsland.Visual
}
#endregion
private void CreateGroundDetails()
{
// Scatter shells
for (int i = 0; i < 20; i++)
{
float x = Random.Range(-25f, 25f);
float z = Random.Range(3f, 7f); // Near water line
var shell = new GameObject("Shell");
shell.transform.SetParent(transform);
shell.transform.position = new Vector3(x, -0.45f, z);
// Lie flat
shell.transform.rotation = Quaternion.Euler(90, Random.Range(0, 360), 0);
var renderer = shell.AddComponent<SpriteRenderer>();
renderer.sprite = CreateShellSprite();
renderer.sortingOrder = -39;
shell.transform.localScale = Vector3.one * Random.Range(0.2f, 0.4f);
}
// Scatter pebbles
for (int i = 0; i < 30; i++)
{
float x = Random.Range(-25f, 25f);
float z = Random.Range(-2f, 10f); // Wider range
var pebble = new GameObject("Pebble");
pebble.transform.SetParent(transform);
pebble.transform.position = new Vector3(x, -0.48f, z);
pebble.transform.rotation = Quaternion.Euler(90, Random.Range(0, 360), 0);
var renderer = pebble.AddComponent<SpriteRenderer>();
renderer.sprite = CreatePebbleSprite();
renderer.sortingOrder = -39;
renderer.color = new Color(0.7f, 0.7f, 0.7f);
pebble.transform.localScale = Vector3.one * Random.Range(0.1f, 0.2f);
}
}
private Sprite CreateShellSprite()
{
int size = 32;
Texture2D tex = new Texture2D(size, size);
Color[] pixels = new Color[size*size];
for(int i=0; i<pixels.Length; i++) pixels[i] = Color.clear;
Vector2 center = new Vector2(size/2, size/2);
for(int y=0; y<size; y++){
for(int x=0; x<size; x++){
float dist = Vector2.Distance(new Vector2(x,y), center);
if(dist < 12) {
float angle = Mathf.Atan2(y-center.y, x-center.x);
// Simple spiral or scallop shape
float radius = 10 + Mathf.Sin(angle * 5) * 2;
if(dist < radius)
pixels[y*size+x] = new Color(1f, 0.95f, 0.85f);
}
}
}
tex.SetPixels(pixels);
tex.Apply();
return Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f));
}
private Sprite CreatePebbleSprite()
{
int size = 16;
Texture2D tex = new Texture2D(size, size);
Color[] pixels = new Color[size*size];
for(int i=0; i<pixels.Length; i++) pixels[i] = Color.clear;
Vector2 center = new Vector2(size/2, size/2);
for(int y=0; y<size; y++){
for(int x=0; x<size; x++){
if(Vector2.Distance(new Vector2(x,y), center) < 5 + Random.Range(-1f, 1f)) {
pixels[y*size+x] = Color.white;
}
}
}
tex.SetPixels(pixels);
tex.Apply();
return Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f));
}
private void CreateClouds()
{
for (int i = 0; i < 5; i++)
{
var cloud = new GameObject("Cloud");
cloud.transform.SetParent(transform);
// Random position in sky
float startX = Random.Range(-25f, 25f);
float startY = Random.Range(3f, 8f);
float depth = Random.Range(15f, 25f);
cloud.transform.position = new Vector3(startX, startY, depth);
var renderer = cloud.AddComponent<SpriteRenderer>();
renderer.sprite = CreateCloudSprite();
renderer.sortingOrder = -90; // Behind everything but sky
// Random size and opacity
float scale = Random.Range(3f, 6f);
cloud.transform.localScale = new Vector3(scale * 1.5f, scale, 1f);
renderer.color = new Color(1f, 1f, 1f, Random.Range(0.4f, 0.8f));
}
}
private Sprite CreateCloudSprite()
{
int size = 64;
Texture2D tex = new Texture2D(size, size);
Color[] pixels = new Color[size * size];
// Procedural fluffy cloud
Vector2 center = new Vector2(size/2, size/2);
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
float noise = Mathf.PerlinNoise(x * 0.15f, y * 0.15f); // Noise frequency
float dist = Vector2.Distance(new Vector2(x, y), center) / (size * 0.4f);
// Soft circle with noise
float density = Mathf.Clamp01(1f - dist);
density *= (0.5f + noise * 0.5f);
// Threshold for fluffiness
density = Mathf.SmoothStep(0.2f, 0.8f, density);
pixels[y * size + x] = new Color(1, 1, 1, density * density);
}
}
tex.SetPixels(pixels);
tex.Apply();
return Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f));
}
private void AnimateClouds()
{
// Move clouds slowly
foreach (Transform child in transform)
{
if (child.name == "Cloud")
{
Vector3 pos = child.transform.position;
// Wind speed depends on cloud distance for parallax
float speed = 0.5f + (25f - pos.z) * 0.05f;
pos.x += Time.deltaTime * speed;
// Wrap around
if (pos.x > 30f) pos.x = -30f;
child.transform.position = pos;
}
}
}
#region Public API
/// <summary>
/// Force update the environment to specific conditions.

View 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
}
}

View File

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

View 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;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 47dcba28f3d78421ca5bdf74a4365bed

View File

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

View 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
}
}
}

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: f82a9056a602b4fcd9b86f52d2c43c9a
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

View 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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 KiB

View 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:

View File

@@ -487,6 +487,57 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &2108464767
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2108464769}
- component: {fileID: 2108464768}
m_Layer: 0
m_Name: VFXManager
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &2108464768
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2108464767}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c07bfc9fddc8347ea826abf2adc4d44c, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TheIsland.Visual.VFXManager
goldColor: {r: 1, g: 0.84, b: 0, a: 1}
goldParticleCount: 50
goldDuration: 2
heartColor: {r: 1, g: 0.2, b: 0.3, a: 1}
heartParticleCount: 30
heartDuration: 1.5
effectScale: 1
--- !u!4 &2108464769
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2108464767}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
@@ -497,3 +548,4 @@ SceneRoots:
- {fileID: 851065944}
- {fileID: 1562380643}
- {fileID: 318018868}
- {fileID: 2108464769}

View File

@@ -43,6 +43,7 @@
"com.unity.modules.video": "1.0.0",
"com.unity.modules.vr": "1.0.0",
"com.unity.modules.wind": "1.0.0",
"com.unity.modules.xr": "1.0.0"
"com.unity.modules.xr": "1.0.0",
"com.unity.postprocessing": "3.4.0"
}
}
}

View File

@@ -276,6 +276,15 @@
"dependencies": {},
"url": "https://packages.unity.com"
},
"com.unity.postprocessing": {
"version": "3.4.0",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.modules.physics": "1.0.0"
},
"url": "https://packages.unity.com"
},
"com.unity.serialization": {
"version": "3.1.3",
"depth": 1,

View File

@@ -682,7 +682,22 @@ PlayerSettings:
webWasm2023: 0
webEnableSubmoduleStrippingCompatibility: 0
scriptingDefineSymbols:
Standalone: APP_UI_EDITOR_ONLY
Android: UNITY_POST_PROCESSING_STACK_V2
EmbeddedLinux: UNITY_POST_PROCESSING_STACK_V2
GameCoreScarlett: UNITY_POST_PROCESSING_STACK_V2
GameCoreXboxOne: UNITY_POST_PROCESSING_STACK_V2
Kepler: UNITY_POST_PROCESSING_STACK_V2
LinuxHeadlessSimulation: UNITY_POST_PROCESSING_STACK_V2
Nintendo Switch: UNITY_POST_PROCESSING_STACK_V2
Nintendo Switch 2: UNITY_POST_PROCESSING_STACK_V2
PS4: UNITY_POST_PROCESSING_STACK_V2
PS5: UNITY_POST_PROCESSING_STACK_V2
QNX: UNITY_POST_PROCESSING_STACK_V2
Standalone: APP_UI_EDITOR_ONLY;UNITY_POST_PROCESSING_STACK_V2
VisionOS: UNITY_POST_PROCESSING_STACK_V2
WebGL: UNITY_POST_PROCESSING_STACK_V2
XboxOne: UNITY_POST_PROCESSING_STACK_V2
tvOS: UNITY_POST_PROCESSING_STACK_V2
additionalCompilerArguments: {}
platformArchitecture: {}
scriptingBackend: {}