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>
This commit is contained in:
@@ -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
|
||||
@@ -228,9 +229,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 +305,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 +370,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 +417,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 +431,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 +443,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 +606,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 +988,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 +1195,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 +1306,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()
|
||||
@@ -984,8 +1412,19 @@ class GameEngine:
|
||||
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"
|
||||
)
|
||||
|
||||
# 3. Broadcast gift effect to Unity
|
||||
# 4. Broadcast gift effect to Unity
|
||||
await self._broadcast_event("gift_effect", {
|
||||
"user": user,
|
||||
"gift_type": gift_type,
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -200,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."
|
||||
)
|
||||
@@ -347,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."
|
||||
|
||||
74
backend/app/memory_service.py
Normal file
74
backend/app/memory_service.py
Normal file
@@ -0,0 +1,74 @@
|
||||
|
||||
import logging
|
||||
import random
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from .database import get_db_session
|
||||
from .models import Agent, AgentMemory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MemoryService:
|
||||
"""
|
||||
Manages long-term memories for agents.
|
||||
Responsible for:
|
||||
1. Storing new memories.
|
||||
2. Retrieving relevant memories for context.
|
||||
3. Pruning/Summarizing old memories (future).
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
async def add_memory(self, agent_id: int, description: str, importance: int = 1,
|
||||
related_entity_id: int = None, related_entity_name: str = None,
|
||||
memory_type: str = "general") -> AgentMemory:
|
||||
"""
|
||||
Record a new memory for an agent.
|
||||
"""
|
||||
with get_db_session() as db:
|
||||
memory = AgentMemory(
|
||||
agent_id=agent_id,
|
||||
description=description,
|
||||
importance=importance,
|
||||
related_entity_id=related_entity_id,
|
||||
related_entity_name=related_entity_name,
|
||||
memory_type=memory_type
|
||||
)
|
||||
db.add(memory)
|
||||
db.commit() # Ensure ID is generated
|
||||
db.refresh(memory)
|
||||
logger.info(f"Agent {agent_id} remembered: {description} (Imp: {importance})")
|
||||
return memory
|
||||
|
||||
async def get_relevant_memories(self, agent_id: int, context: str, limit: int = 3) -> List[str]:
|
||||
"""
|
||||
Retrieve memories relevant to the current context.
|
||||
For MVP, we just return the most recent high-importance memories
|
||||
and any memories related to the entities in context.
|
||||
"""
|
||||
memories = []
|
||||
with get_db_session() as db:
|
||||
# 1. Get recent important memories (Short-term / working memory)
|
||||
recent_memories = db.query(AgentMemory).filter(
|
||||
AgentMemory.agent_id == agent_id,
|
||||
AgentMemory.importance >= 5
|
||||
).order_by(AgentMemory.created_at.desc()).limit(limit).all()
|
||||
|
||||
# 2. Get entity-specific memories (e.g. if talking to "User1")
|
||||
# Simple keyword matching for now (Vector DB is Phase 14+)
|
||||
entity_memories = []
|
||||
if context:
|
||||
# Naive search for names in context
|
||||
# In real prod, use embeddings.
|
||||
search_term = f"%{context}%" # Very naive
|
||||
# Let's just fallback to recent for MVP to ensure stability
|
||||
|
||||
for mem in recent_memories:
|
||||
memories.append(f"- {mem.description}")
|
||||
|
||||
return memories
|
||||
|
||||
# Global instance
|
||||
memory_service = MemoryService()
|
||||
@@ -51,6 +51,18 @@ class Agent(Base):
|
||||
# Social attributes (Phase 5)
|
||||
social_tendency = Column(String(20), default="neutral") # introvert, extrovert, neutral
|
||||
|
||||
# Autonomous Action (Phase 13)
|
||||
current_action = Column(String(50), default="Idle")
|
||||
location = Column(String(50), default="center") # logical location: tree_left, tree_right, center, etc.
|
||||
target_agent_id = Column(Integer, nullable=True) # if action targets another agent
|
||||
|
||||
# Survival (Phase 15)
|
||||
is_sick = Column(Boolean, default=False)
|
||||
immunity = Column(Integer, default=50) # 0-100, higher = less chance to get sick
|
||||
|
||||
# Relationship 2.0 (Phase 17-B)
|
||||
social_role = Column(String(20), default="neutral") # leader, follower, loner, neutral
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Agent {self.name} ({self.personality}) HP={self.hp} Energy={self.energy} Mood={self.mood}>"
|
||||
|
||||
@@ -71,7 +83,12 @@ class Agent(Base):
|
||||
"inventory": self.inventory,
|
||||
"mood": self.mood,
|
||||
"mood_state": self.mood_state,
|
||||
"social_tendency": self.social_tendency
|
||||
"social_tendency": self.social_tendency,
|
||||
"current_action": self.current_action,
|
||||
"location": self.location,
|
||||
"is_sick": self.is_sick,
|
||||
"immunity": self.immunity,
|
||||
"social_role": self.social_role
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +111,10 @@ class WorldState(Base):
|
||||
# Weather system (Phase 3)
|
||||
weather_duration = Column(Integer, default=0) # Ticks since last weather change
|
||||
|
||||
# Resource Scarcity (Phase 17-A)
|
||||
tree_left_fruit = Column(Integer, default=5) # Max 5 fruit
|
||||
tree_right_fruit = Column(Integer, default=5) # Max 5 fruit
|
||||
|
||||
def __repr__(self):
|
||||
return f"<WorldState Day={self.day_count} {self.time_of_day} Weather={self.weather}>"
|
||||
|
||||
@@ -104,7 +125,9 @@ class WorldState(Base):
|
||||
"weather": self.weather,
|
||||
"resource_level": self.resource_level,
|
||||
"current_tick_in_day": self.current_tick_in_day,
|
||||
"time_of_day": self.time_of_day
|
||||
"time_of_day": self.time_of_day,
|
||||
"tree_left_fruit": self.tree_left_fruit,
|
||||
"tree_right_fruit": self.tree_right_fruit
|
||||
}
|
||||
|
||||
|
||||
@@ -206,3 +229,39 @@ class AgentRelationship(Base):
|
||||
self.relationship_type = "friend"
|
||||
else:
|
||||
self.relationship_type = "close_friend"
|
||||
|
||||
|
||||
class AgentMemory(Base):
|
||||
"""
|
||||
Long-term memory for agents.
|
||||
Stores significant events, conversations, and interactions.
|
||||
"""
|
||||
__tablename__ = "agent_memories"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
agent_id = Column(Integer, ForeignKey("agents.id"), nullable=False)
|
||||
|
||||
# The memory content
|
||||
description = Column(String(500), nullable=False)
|
||||
|
||||
# Metadata for retrieval
|
||||
importance = Column(Integer, default=1) # 1-10
|
||||
related_entity_id = Column(Integer, nullable=True) # ID of user or other agent involved
|
||||
related_entity_name = Column(String(50), nullable=True)
|
||||
memory_type = Column(String(20), default="general") # chat, gift, event, social
|
||||
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
tick_created = Column(Integer, nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Memory {self.agent_id}: {self.description[:20]}... ({self.importance})>"
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"agent_id": self.agent_id,
|
||||
"description": self.description,
|
||||
"importance": self.importance,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,16 @@ class EventType(str, Enum):
|
||||
RELATIONSHIP_CHANGE = "relationship_change" # Relationship status changed
|
||||
AUTO_REVIVE = "auto_revive" # Agent auto-revived (casual mode)
|
||||
|
||||
# Autonomous Agency (Phase 13)
|
||||
AGENT_ACTION = "agent_action" # Agent performs an action (move, gather, etc.)
|
||||
|
||||
# Crafting System (Phase 16)
|
||||
CRAFT = "craft" # Agent crafted an item
|
||||
USE_ITEM = "use_item" # Agent used an item
|
||||
|
||||
# Random Events (Phase 17-C)
|
||||
RANDOM_EVENT = "random_event" # Random event occurred
|
||||
|
||||
|
||||
class GameEvent(BaseModel):
|
||||
"""
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -69,6 +69,7 @@ namespace TheIsland.Visual
|
||||
#region State
|
||||
private int _agentId;
|
||||
private AgentData _currentData;
|
||||
private string _moodState = "neutral";
|
||||
private Coroutine _speechCoroutine;
|
||||
|
||||
// Animation state
|
||||
@@ -76,6 +77,11 @@ namespace TheIsland.Visual
|
||||
private float _breathScale = 1f;
|
||||
private Vector3 _originalSpriteScale;
|
||||
private float _bobOffset;
|
||||
|
||||
// Movement state
|
||||
private Vector3 _targetPosition;
|
||||
private bool _isMoving;
|
||||
private float _moveSpeed = 3f;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
@@ -95,6 +101,29 @@ namespace TheIsland.Visual
|
||||
{
|
||||
if (!IsAlive) return;
|
||||
|
||||
// Handle Movement
|
||||
if (_isMoving)
|
||||
{
|
||||
transform.position = Vector3.MoveTowards(transform.position, _targetPosition, _moveSpeed * Time.deltaTime);
|
||||
|
||||
// Flip sprite based on direction
|
||||
if (_spriteRenderer != null)
|
||||
{
|
||||
float dx = _targetPosition.x - transform.position.x;
|
||||
if (Mathf.Abs(dx) > 0.1f)
|
||||
{
|
||||
// FlipX = true means face Left (assuming sprite faces Right by default)
|
||||
// If sprite faces Front, we might need a different approach, but FlipX is standard for 2D.
|
||||
_spriteRenderer.flipX = dx < 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (Vector3.Distance(transform.position, _targetPosition) < 0.05f)
|
||||
{
|
||||
_isMoving = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Idle breathing animation (Squash and Stretch)
|
||||
_idleAnimTimer += Time.deltaTime;
|
||||
|
||||
@@ -103,8 +132,16 @@ namespace TheIsland.Visual
|
||||
_breathScale = 1f + breath;
|
||||
float antiBreath = 1f - (breath * 0.5f); // Squash X when stretching Y
|
||||
|
||||
// Bobbing: Move up and down
|
||||
_bobOffset = Mathf.Sin(_idleAnimTimer * 2f) * 0.08f;
|
||||
// Bobbing: Move up and down (only when idle)
|
||||
if (!_isMoving)
|
||||
{
|
||||
_bobOffset = Mathf.Sin(_idleAnimTimer * 2f) * 0.08f;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Hop while moving
|
||||
_bobOffset = Mathf.Abs(Mathf.Sin(_idleAnimTimer * 10f)) * 0.2f;
|
||||
}
|
||||
|
||||
if (_spriteRenderer != null && _originalSpriteScale != Vector3.zero)
|
||||
{
|
||||
@@ -128,6 +165,18 @@ namespace TheIsland.Visual
|
||||
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 IEnumerator JumpRoutine()
|
||||
{
|
||||
float timer = 0;
|
||||
@@ -438,7 +487,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;
|
||||
@@ -880,6 +949,17 @@ namespace TheIsland.Visual
|
||||
_moodBarFill.rectTransform.anchorMax = new Vector2(moodPercent, 1);
|
||||
_moodBarFill.color = GetMoodColor(data.mood_state);
|
||||
}
|
||||
|
||||
// Check for mood change (Visual Expression)
|
||||
if (_moodState != data.mood_state)
|
||||
{
|
||||
_moodState = data.mood_state;
|
||||
// Only regenerate if using placeholder sprite
|
||||
if (characterSprite == null && _spriteRenderer != null)
|
||||
{
|
||||
RegeneratePlaceholderSprite();
|
||||
}
|
||||
}
|
||||
if (_moodText != null)
|
||||
{
|
||||
string moodIndicator = GetMoodEmoji(data.mood_state);
|
||||
@@ -943,11 +1023,42 @@ 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
|
||||
|
||||
|
||||
@@ -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] 初始化完成");
|
||||
}
|
||||
|
||||
@@ -153,6 +153,8 @@ namespace TheIsland.Core
|
||||
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()
|
||||
@@ -180,6 +182,7 @@ namespace TheIsland.Core
|
||||
network.OnRevive -= HandleRevive;
|
||||
network.OnSocialInteraction -= HandleSocialInteraction;
|
||||
network.OnGiftEffect -= HandleGiftEffect; // Phase 8
|
||||
network.OnRandomEvent -= HandleRandomEvent; // Phase 17-C
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -536,6 +539,92 @@ namespace TheIsland.Core
|
||||
// Show notification
|
||||
ShowNotification(data.message);
|
||||
}
|
||||
#region Agent Action (Phase 13)
|
||||
private void HandleAgentAction(AgentActionData data)
|
||||
{
|
||||
Debug.Log($"[GameManager] Action: {data.agent_name} -> {data.action_type} at {data.location}");
|
||||
|
||||
// Resolve target position
|
||||
Vector3 targetPos = GetLocationPosition(data.location, data.target_name);
|
||||
|
||||
// Find agent and command movement
|
||||
if (_agentVisuals.TryGetValue(data.agent_id, out AgentVisual agentVisual))
|
||||
{
|
||||
agentVisual.MoveTo(targetPos);
|
||||
|
||||
// Optional: Show thought bubble or speech
|
||||
if (!string.IsNullOrEmpty(data.dialogue))
|
||||
{
|
||||
agentVisual.ShowSpeech(data.dialogue, 3f);
|
||||
}
|
||||
}
|
||||
else if (_agentUIs.TryGetValue(data.agent_id, out AgentUI agentUI))
|
||||
{
|
||||
// Fallback for UI-only agents (just show speech)
|
||||
if (!string.IsNullOrEmpty(data.dialogue))
|
||||
{
|
||||
agentUI.ShowSpeech(data.dialogue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3 GetLocationPosition(string location, string targetName)
|
||||
{
|
||||
// Map logical locations to world coordinates
|
||||
switch (location.ToLower())
|
||||
{
|
||||
case "tree_left":
|
||||
return new Vector3(-10f, 0f, 8f);
|
||||
case "tree_right":
|
||||
return new Vector3(10f, 0f, 8f);
|
||||
case "campfire":
|
||||
case "center":
|
||||
return new Vector3(0f, 0f, 0f);
|
||||
case "water":
|
||||
case "beach":
|
||||
return new Vector3(Random.Range(-5, 5), 0f, 4f);
|
||||
case "nearby":
|
||||
// Move to random nearby spot (wandering)
|
||||
return new Vector3(Random.Range(-12, 12), 0f, Random.Range(-2, 6));
|
||||
case "herb_patch":
|
||||
// Phase 16: Herb gathering location
|
||||
return new Vector3(-8f, 0f, -5f);
|
||||
case "agent":
|
||||
// Move to another agent
|
||||
int targetId = GetAgentIdByName(targetName);
|
||||
if (targetId >= 0 && _agentVisuals.TryGetValue(targetId, out AgentVisual target))
|
||||
{
|
||||
// Stand slightly offset from target
|
||||
return target.transform.position + new Vector3(1.5f, 0, 0);
|
||||
}
|
||||
return Vector3.zero;
|
||||
default:
|
||||
return new Vector3(0f, 0f, 0f); // Fallback to center
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Random Events (Phase 17-C)
|
||||
private void HandleRandomEvent(RandomEventData data)
|
||||
{
|
||||
Debug.Log($"[GameManager] Random Event: {data.event_type} - {data.message}");
|
||||
|
||||
// Display global notification banner
|
||||
string eventIcon = data.event_type switch
|
||||
{
|
||||
"storm_damage" => "⛈️ ",
|
||||
"treasure_found" => "💎 ",
|
||||
"beast_attack" => "🐺 ",
|
||||
"rumor_spread" => "💬 ",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
ShowNotification(eventIcon + data.message);
|
||||
|
||||
// Optional: Trigger visual effects based on event type
|
||||
// (Could add screen shake for storm, highlight agent for treasure, etc.)
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Agent Management
|
||||
|
||||
@@ -48,6 +48,17 @@ namespace TheIsland.Models
|
||||
public string mood_state; // "happy", "neutral", "sad", "anxious"
|
||||
public string social_tendency; // "introvert", "extrovert", "neutral"
|
||||
|
||||
// Survival (Phase 15)
|
||||
public bool is_sick;
|
||||
public int immunity;
|
||||
|
||||
// Autonomous Agency (Phase 13)
|
||||
public string current_action;
|
||||
public string location;
|
||||
|
||||
// Relationship 2.0 (Phase 17-B)
|
||||
public string social_role; // "leader", "follower", "loner", "neutral"
|
||||
|
||||
public bool IsAlive => status == "Alive";
|
||||
}
|
||||
|
||||
@@ -138,6 +149,10 @@ namespace TheIsland.Models
|
||||
public int resource_level;
|
||||
public int current_tick_in_day;
|
||||
public string time_of_day; // "dawn", "day", "dusk", "night"
|
||||
|
||||
// Resource Scarcity (Phase 17-A)
|
||||
public int tree_left_fruit;
|
||||
public int tree_right_fruit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -311,5 +326,64 @@ namespace TheIsland.Models
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,10 @@ namespace TheIsland.Network
|
||||
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
|
||||
@@ -349,11 +353,32 @@ namespace TheIsland.Network
|
||||
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;
|
||||
|
||||
@@ -56,7 +56,6 @@ 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
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user