feat: integrate skill tree UI and combat bonuses

Grant skill points on floor clear, add allocation UI with [ ] keys
during exploration, apply SkillPower bonus to Mage Fireball and Healer
Heal, initialize skills for new players, and deep copy skills in
GetState.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 15:47:01 +09:00
parent 65c062a1f7
commit b8697e414a
3 changed files with 80 additions and 6 deletions

View File

@@ -199,6 +199,9 @@ func (s *GameSession) signalCombat() {
func (s *GameSession) AddPlayer(p *entity.Player) { func (s *GameSession) AddPlayer(p *entity.Player) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
if p.Skills == nil {
p.Skills = &entity.PlayerSkills{BranchIndex: -1}
}
s.state.Players = append(s.state.Players, p) s.state.Players = append(s.state.Players, p)
} }
@@ -231,6 +234,10 @@ func (s *GameSession) GetState() GameState {
copy(cp.Relics, p.Relics) copy(cp.Relics, p.Relics)
cp.Effects = make([]entity.ActiveEffect, len(p.Effects)) cp.Effects = make([]entity.ActiveEffect, len(p.Effects))
copy(cp.Effects, p.Effects) copy(cp.Effects, p.Effects)
if p.Skills != nil {
skillsCopy := *p.Skills
cp.Skills = &skillsCopy
}
players[i] = &cp players[i] = &cp
} }
@@ -337,6 +344,21 @@ func (s *GameSession) TouchActivity(fingerprint string) {
s.lastActivity[fingerprint] = time.Now() s.lastActivity[fingerprint] = time.Now()
} }
// AllocateSkillPoint spends one skill point into the given branch for the player.
func (s *GameSession) AllocateSkillPoint(fingerprint string, branchIdx int) error {
s.mu.Lock()
defer s.mu.Unlock()
for _, p := range s.state.Players {
if p.Fingerprint == fingerprint {
if p.Skills == nil || p.Skills.Points <= p.Skills.Allocated {
return fmt.Errorf("no skill points available")
}
return p.Skills.Allocate(branchIdx, p.Class)
}
}
return fmt.Errorf("player not found")
}
// BuyItem handles shop purchases // BuyItem handles shop purchases
func (s *GameSession) BuyItem(playerID string, itemIdx int) bool { func (s *GameSession) BuyItem(playerID string, itemIdx int) bool {
s.mu.Lock() s.mu.Lock()

View File

@@ -147,10 +147,15 @@ func (s *GameSession) resolvePlayerActions() {
} }
s.addLog(fmt.Sprintf("%s used Taunt! Enemies focus on %s for 2 turns", p.Name, p.Name)) s.addLog(fmt.Sprintf("%s used Taunt! Enemies focus on %s for 2 turns", p.Name, p.Name))
case entity.ClassMage: case entity.ClassMage:
skillPower := 0
if p.Skills != nil {
skillPower = p.Skills.GetSkillPower(p.Class)
}
multiplier := 0.8 + float64(skillPower)/100.0
intents = append(intents, combat.AttackIntent{ intents = append(intents, combat.AttackIntent{
PlayerATK: p.EffectiveATK(), PlayerATK: p.EffectiveATK(),
TargetIdx: -1, TargetIdx: -1,
Multiplier: 0.8, Multiplier: multiplier,
IsAoE: true, IsAoE: true,
}) })
intentOwners = append(intentOwners, p.Name) intentOwners = append(intentOwners, p.Name)
@@ -170,8 +175,12 @@ func (s *GameSession) resolvePlayerActions() {
} }
} }
} }
healAmount := 30
if p.Skills != nil {
healAmount += p.Skills.GetSkillPower(p.Class) / 2
}
before := target.HP before := target.HP
target.Heal(30) target.Heal(healAmount)
s.addLog(fmt.Sprintf("%s healed %s for %d HP", p.Name, target.Name, target.HP-before)) s.addLog(fmt.Sprintf("%s healed %s for %d HP", p.Name, target.Name, target.HP-before))
case entity.ClassRogue: case entity.ClassRogue:
currentRoom := s.state.Floor.Rooms[s.state.Floor.CurrentRoom] currentRoom := s.state.Floor.Rooms[s.state.Floor.CurrentRoom]
@@ -356,6 +365,13 @@ func (s *GameSession) advanceFloor() {
s.addLog("You conquered the Catacombs!") s.addLog("You conquered the Catacombs!")
return return
} }
// Grant 1 skill point per floor clear
for _, p := range s.state.Players {
if p.Skills == nil {
p.Skills = &entity.PlayerSkills{BranchIndex: -1}
}
p.Skills.Points++
}
s.state.FloorNum++ s.state.FloorNum++
s.state.Floor = dungeon.GenerateFloor(s.state.FloorNum, rand.New(rand.NewSource(time.Now().UnixNano()))) s.state.Floor = dungeon.GenerateFloor(s.state.FloorNum, rand.New(rand.NewSource(time.Now().UnixNano())))
s.state.Phase = PhaseExploring s.state.Phase = PhaseExploring

View File

@@ -169,6 +169,18 @@ func (s *GameScreen) Update(msg tea.Msg, ctx *Context) (Screen, tea.Cmd) {
return s, nil return s, nil
} }
} }
// Skill point allocation
if isKey(key, "[") || isKey(key, "]") {
if ctx.Session != nil {
branchIdx := 0
if isKey(key, "]") {
branchIdx = 1
}
ctx.Session.AllocateSkillPoint(ctx.Fingerprint, branchIdx)
s.gameState = ctx.Session.GetState()
}
return s, nil
}
neighbors := s.getNeighbors() neighbors := s.getNeighbors()
if isUp(key) { if isUp(key) {
if s.moveCursor > 0 { if s.moveCursor > 0 {
@@ -229,12 +241,12 @@ func (s *GameScreen) Update(msg tea.Msg, ctx *Context) (Screen, tea.Cmd) {
} }
func (s *GameScreen) View(ctx *Context) string { func (s *GameScreen) View(ctx *Context) string {
return renderGame(s.gameState, ctx.Width, ctx.Height, s.targetCursor, s.moveCursor, s.chatting, s.chatInput) return renderGame(s.gameState, ctx.Width, ctx.Height, s.targetCursor, s.moveCursor, s.chatting, s.chatInput, ctx.Fingerprint)
} }
func renderGame(state game.GameState, width, height int, targetCursor int, moveCursor int, chatting bool, chatInput string) string { func renderGame(state game.GameState, width, height int, targetCursor int, moveCursor int, chatting bool, chatInput string, fingerprint string) string {
mapView := renderMap(state.Floor) mapView := renderMap(state.Floor)
hudView := renderHUD(state, targetCursor, moveCursor) hudView := renderHUD(state, targetCursor, moveCursor, fingerprint)
logView := renderCombatLog(state.CombatLog) logView := renderCombatLog(state.CombatLog)
if chatting { if chatting {
@@ -270,7 +282,7 @@ func renderMap(floor *dungeon.Floor) string {
return header + "\n" + dungeon.RenderFloor(floor, floor.CurrentRoom, true) return header + "\n" + dungeon.RenderFloor(floor, floor.CurrentRoom, true)
} }
func renderHUD(state game.GameState, targetCursor int, moveCursor int) string { func renderHUD(state game.GameState, targetCursor int, moveCursor int, fingerprint string) string {
var sb strings.Builder var sb strings.Builder
border := lipgloss.NewStyle(). border := lipgloss.NewStyle().
Border(lipgloss.NormalBorder()). Border(lipgloss.NormalBorder()).
@@ -378,6 +390,30 @@ func renderHUD(state game.GameState, targetCursor int, moveCursor int) string {
} }
} }
} }
// Show skill tree allocation UI if player has unspent points
for _, p := range state.Players {
if p.Fingerprint == fingerprint && p.Skills != nil && p.Skills.Points > p.Skills.Allocated && p.Skills.Allocated < 3 {
branches := entity.GetBranches(p.Class)
sb.WriteString("\n")
skillStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("213")).Bold(true)
sb.WriteString(skillStyle.Render(fmt.Sprintf(" Skill Point Available! (%d unspent)", p.Skills.Points-p.Skills.Allocated)))
sb.WriteString("\n")
for i, branch := range branches {
key := "["
if i == 1 {
key = "]"
}
nextNode := p.Skills.Allocated
if p.Skills.BranchIndex >= 0 && p.Skills.BranchIndex != i {
sb.WriteString(fmt.Sprintf(" [%s] %s (locked)\n", key, branch.Name))
} else if nextNode < 3 {
node := branch.Nodes[nextNode]
sb.WriteString(fmt.Sprintf(" [%s] %s -> %s\n", key, branch.Name, node.Name))
}
}
break
}
}
sb.WriteString("[Up/Down] Select [Enter] Move [Q] Quit") sb.WriteString("[Up/Down] Select [Enter] Move [Q] Quit")
} }