feat(engine): 实现阵营技能树系统
- 新增 FactionSkill、FactionSkillTree、FactionSkills 数据模型 - 创建 faction_skills.py 模块处理技能解锁和效果应用 - 技能解锁条件:power >= cost 且前置技能已解锁 - 技能效果:increase_positive_emotion、reduce_conflict、increase_fear、suppress_others - 添加 4 个技能树测试用例,全部 30 个测试通过 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ from .factions import (
|
||||
update_factions, apply_faction_influence, check_and_trigger_faction_event
|
||||
)
|
||||
from .voting import process_votes, apply_votes_to_factions
|
||||
from .faction_skills import check_and_unlock_skills, apply_skill_effects
|
||||
|
||||
MAX_EVENTS = 20
|
||||
MAX_MEMORY_PER_AGENT = 3
|
||||
@@ -106,6 +107,12 @@ def process_events(state: WorldState, events: List[Event]) -> WorldState:
|
||||
# 派系影响世界情绪
|
||||
apply_faction_influence(state)
|
||||
|
||||
# 检查并解锁技能
|
||||
check_and_unlock_skills(state)
|
||||
|
||||
# 应用已解锁技能效果
|
||||
apply_skill_effects(state)
|
||||
|
||||
return state
|
||||
|
||||
|
||||
|
||||
126
engine-python/app/faction_skills.py
Normal file
126
engine-python/app/faction_skills.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""阵营技能树系统 - 允许阵营通过累积能量解锁技能"""
|
||||
from typing import List, Optional
|
||||
from .models import (
|
||||
WorldState, FactionSkill, FactionSkillTree, FactionSkills
|
||||
)
|
||||
|
||||
|
||||
def get_default_faction_skills() -> FactionSkills:
|
||||
"""返回默认技能树配置"""
|
||||
return FactionSkills(
|
||||
optimists=FactionSkillTree(
|
||||
skills={
|
||||
"festival_boost": FactionSkill(
|
||||
unlocked=False,
|
||||
cost=10,
|
||||
effect="increase_positive_emotion",
|
||||
requires=[]
|
||||
),
|
||||
"unity": FactionSkill(
|
||||
unlocked=False,
|
||||
cost=20,
|
||||
effect="reduce_conflict",
|
||||
requires=["festival_boost"]
|
||||
)
|
||||
}
|
||||
),
|
||||
fearful=FactionSkillTree(
|
||||
skills={
|
||||
"panic_spread": FactionSkill(
|
||||
unlocked=False,
|
||||
cost=10,
|
||||
effect="increase_fear",
|
||||
requires=[]
|
||||
),
|
||||
"control": FactionSkill(
|
||||
unlocked=False,
|
||||
cost=20,
|
||||
effect="suppress_others",
|
||||
requires=["panic_spread"]
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _check_requires_met(skill_tree: FactionSkillTree, requires: List[str]) -> bool:
|
||||
"""检查前置技能是否已解锁"""
|
||||
for req in requires:
|
||||
if req not in skill_tree.skills:
|
||||
return False
|
||||
if not skill_tree.skills[req].unlocked:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def check_and_unlock_skills(state: WorldState) -> List[str]:
|
||||
"""检查并解锁技能
|
||||
|
||||
规则:
|
||||
1. faction.power >= skill.cost
|
||||
2. 技能尚未解锁
|
||||
3. 所有 requires 已解锁
|
||||
|
||||
返回:本次解锁的技能列表
|
||||
"""
|
||||
unlocked = []
|
||||
|
||||
# 检查乐观派技能
|
||||
for skill_id, skill in state.faction_skills.optimists.skills.items():
|
||||
if skill.unlocked:
|
||||
continue
|
||||
if state.factions.optimists.power < skill.cost:
|
||||
continue
|
||||
if not _check_requires_met(state.faction_skills.optimists, skill.requires):
|
||||
continue
|
||||
|
||||
# 解锁技能
|
||||
skill.unlocked = True
|
||||
state.factions.optimists.power -= skill.cost
|
||||
unlocked.append(f"optimists:{skill_id}")
|
||||
|
||||
# 检查恐惧派技能
|
||||
for skill_id, skill in state.faction_skills.fearful.skills.items():
|
||||
if skill.unlocked:
|
||||
continue
|
||||
if state.factions.fearful.power < skill.cost:
|
||||
continue
|
||||
if not _check_requires_met(state.faction_skills.fearful, skill.requires):
|
||||
continue
|
||||
|
||||
# 解锁技能
|
||||
skill.unlocked = True
|
||||
state.factions.fearful.power -= skill.cost
|
||||
unlocked.append(f"fearful:{skill_id}")
|
||||
|
||||
return unlocked
|
||||
|
||||
|
||||
def apply_skill_effects(state: WorldState) -> None:
|
||||
"""应用已解锁技能的持续效果
|
||||
|
||||
效果说明:
|
||||
- increase_positive_emotion: 每 tick 提升 town_mood +1
|
||||
- reduce_conflict: 降低恐惧派能量增长(每 tick -1)
|
||||
- increase_fear: 每 tick 降低 town_mood -1
|
||||
- suppress_others: 降低乐观派能量增长(每 tick -1)
|
||||
"""
|
||||
# 乐观派技能效果
|
||||
for skill_id, skill in state.faction_skills.optimists.skills.items():
|
||||
if not skill.unlocked:
|
||||
continue
|
||||
|
||||
if skill.effect == "increase_positive_emotion":
|
||||
state.town_mood = min(10, state.town_mood + 1)
|
||||
elif skill.effect == "reduce_conflict":
|
||||
state.factions.fearful.power = max(0, state.factions.fearful.power - 1)
|
||||
|
||||
# 恐惧派技能效果
|
||||
for skill_id, skill in state.faction_skills.fearful.skills.items():
|
||||
if not skill.unlocked:
|
||||
continue
|
||||
|
||||
if skill.effect == "increase_fear":
|
||||
state.town_mood = max(-10, state.town_mood - 1)
|
||||
elif skill.effect == "suppress_others":
|
||||
state.factions.optimists.power = max(0, state.factions.optimists.power - 1)
|
||||
@@ -66,6 +66,25 @@ class Votes(BaseModel):
|
||||
voted_users: List[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class FactionSkill(BaseModel):
|
||||
"""单个技能"""
|
||||
unlocked: bool = False
|
||||
cost: int = 10
|
||||
effect: str = ""
|
||||
requires: List[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class FactionSkillTree(BaseModel):
|
||||
"""单个阵营的技能树"""
|
||||
skills: Dict[str, FactionSkill] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class FactionSkills(BaseModel):
|
||||
"""所有阵营的技能树"""
|
||||
optimists: FactionSkillTree = Field(default_factory=FactionSkillTree)
|
||||
fearful: FactionSkillTree = Field(default_factory=FactionSkillTree)
|
||||
|
||||
|
||||
class FactionEventResult(BaseModel):
|
||||
"""阵营事件触发结果"""
|
||||
type: Optional[str] = None # "festival" | "panic" | None
|
||||
@@ -113,6 +132,7 @@ class WorldState(BaseModel):
|
||||
factions: Factions = Field(default_factory=Factions)
|
||||
story_arcs: Dict[str, StoryArc] = Field(default_factory=dict)
|
||||
votes: Votes = Field(default_factory=Votes)
|
||||
faction_skills: FactionSkills = Field(default_factory=FactionSkills)
|
||||
|
||||
|
||||
class Event(BaseModel):
|
||||
|
||||
@@ -2,6 +2,7 @@ import json
|
||||
from pathlib import Path
|
||||
from .models import WorldState, AgentState, GlobalMeter, Stance, Factions, FactionData
|
||||
from .story_arcs import get_default_story_arcs
|
||||
from .faction_skills import get_default_faction_skills
|
||||
|
||||
STATE_FILE = Path(__file__).parent.parent / "state.json"
|
||||
|
||||
@@ -35,6 +36,7 @@ def get_default_state() -> WorldState:
|
||||
fearful=FactionData(power=0, threshold=10, skill="panic", members=[])
|
||||
),
|
||||
story_arcs=get_default_story_arcs(),
|
||||
faction_skills=get_default_faction_skills(),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -481,3 +481,63 @@ def test_votes_trigger_faction_event(client):
|
||||
|
||||
# 要么触发了事件,要么能量在累积
|
||||
assert triggered or factions["optimists"]["power"] > 0
|
||||
|
||||
|
||||
# ==================== 技能树系统测试 ====================
|
||||
|
||||
def test_faction_skills_exists_in_world_state(client):
|
||||
"""测试 world_state 包含 faction_skills 字段"""
|
||||
resp = client.post("/step", json={"events": []})
|
||||
data = resp.json()
|
||||
|
||||
assert "faction_skills" in data["world_state"]
|
||||
skills = data["world_state"]["faction_skills"]
|
||||
assert "optimists" in skills
|
||||
assert "fearful" in skills
|
||||
|
||||
|
||||
def test_faction_skills_has_required_fields(client):
|
||||
"""测试技能包含必要字段"""
|
||||
resp = client.post("/step", json={"events": []})
|
||||
data = resp.json()
|
||||
|
||||
skills = data["world_state"]["faction_skills"]
|
||||
opt_skills = skills["optimists"]["skills"]
|
||||
|
||||
assert "festival_boost" in opt_skills
|
||||
skill = opt_skills["festival_boost"]
|
||||
assert "unlocked" in skill
|
||||
assert "cost" in skill
|
||||
assert "effect" in skill
|
||||
assert "requires" in skill
|
||||
|
||||
|
||||
def test_skill_unlock_when_power_sufficient(client):
|
||||
"""测试能量足够时解锁技能"""
|
||||
# 多轮投票累积能量到 10+
|
||||
for i in range(6):
|
||||
events = [
|
||||
{"type": "vote", "faction": "optimists", "text": "", "user": f"u{i}_a", "ts": i},
|
||||
{"type": "vote", "faction": "optimists", "text": "", "user": f"u{i}_b", "ts": i+0.1},
|
||||
]
|
||||
client.post("/step", json={"events": events})
|
||||
|
||||
# 再执行一轮让投票生效
|
||||
resp = client.post("/step", json={"events": []})
|
||||
data = resp.json()
|
||||
|
||||
skills = data["world_state"]["faction_skills"]["optimists"]["skills"]
|
||||
# festival_boost cost=10,应该已解锁
|
||||
assert skills["festival_boost"]["unlocked"] is True
|
||||
|
||||
|
||||
def test_skill_requires_prerequisite(client):
|
||||
"""测试技能需要前置技能"""
|
||||
resp = client.post("/step", json={"events": []})
|
||||
data = resp.json()
|
||||
|
||||
skills = data["world_state"]["faction_skills"]["optimists"]["skills"]
|
||||
# unity 需要 festival_boost 作为前置
|
||||
assert skills["unity"]["requires"] == ["festival_boost"]
|
||||
# 初始状态下 unity 未解锁
|
||||
assert skills["unity"]["unlocked"] is False
|
||||
|
||||
Reference in New Issue
Block a user