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:
@@ -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()
|
||||
|
||||
20
game/turn.go
20
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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user