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) {
|
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()
|
||||||
|
|||||||
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))
|
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
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user