From b8697e414a17c09c808efa35ec23e0e22559dea0 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Wed, 25 Mar 2026 15:47:01 +0900 Subject: [PATCH] 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) --- game/session.go | 22 ++++++++++++++++++++++ game/turn.go | 20 ++++++++++++++++++-- ui/game_view.go | 44 ++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/game/session.go b/game/session.go index c98182a..97d8955 100644 --- a/game/session.go +++ b/game/session.go @@ -199,6 +199,9 @@ func (s *GameSession) signalCombat() { func (s *GameSession) AddPlayer(p *entity.Player) { s.mu.Lock() defer s.mu.Unlock() + if p.Skills == nil { + p.Skills = &entity.PlayerSkills{BranchIndex: -1} + } s.state.Players = append(s.state.Players, p) } @@ -231,6 +234,10 @@ func (s *GameSession) GetState() GameState { copy(cp.Relics, p.Relics) cp.Effects = make([]entity.ActiveEffect, len(p.Effects)) copy(cp.Effects, p.Effects) + if p.Skills != nil { + skillsCopy := *p.Skills + cp.Skills = &skillsCopy + } players[i] = &cp } @@ -337,6 +344,21 @@ func (s *GameSession) TouchActivity(fingerprint string) { 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 func (s *GameSession) BuyItem(playerID string, itemIdx int) bool { s.mu.Lock() diff --git a/game/turn.go b/game/turn.go index f6d5679..b1abb09 100644 --- a/game/turn.go +++ b/game/turn.go @@ -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)) 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{ PlayerATK: p.EffectiveATK(), TargetIdx: -1, - Multiplier: 0.8, + Multiplier: multiplier, IsAoE: true, }) 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 - target.Heal(30) + target.Heal(healAmount) s.addLog(fmt.Sprintf("%s healed %s for %d HP", p.Name, target.Name, target.HP-before)) case entity.ClassRogue: currentRoom := s.state.Floor.Rooms[s.state.Floor.CurrentRoom] @@ -356,6 +365,13 @@ func (s *GameSession) advanceFloor() { s.addLog("You conquered the Catacombs!") 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.Floor = dungeon.GenerateFloor(s.state.FloorNum, rand.New(rand.NewSource(time.Now().UnixNano()))) s.state.Phase = PhaseExploring diff --git a/ui/game_view.go b/ui/game_view.go index 84a206a..1124006 100644 --- a/ui/game_view.go +++ b/ui/game_view.go @@ -169,6 +169,18 @@ func (s *GameScreen) Update(msg tea.Msg, ctx *Context) (Screen, tea.Cmd) { 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() if isUp(key) { 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 { - 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) - hudView := renderHUD(state, targetCursor, moveCursor) + hudView := renderHUD(state, targetCursor, moveCursor, fingerprint) logView := renderCombatLog(state.CombatLog) if chatting { @@ -270,7 +282,7 @@ func renderMap(floor *dungeon.Floor) string { 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 border := lipgloss.NewStyle(). 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") }