Files
8bitMyGoUI/gallery/demos_rpg.go
T
ZacharyZhang-NY f48075230f Add game components 246-268; data.DrawSlot, Canvas.TextVeil
Health, mana and experience bars, quarter hearts, lives, a spinning
coin counter, score board, stage timer, minimap with fog, compass,
hotbar, paper-doll equipment, skill tree, skill buttons with cooldown
wipes, status effects, a boss bar, damage numbers, quest log and
tracker, party frames, character sheet, level-up screen and shop.
data exports DrawSlot so every slot looks alike; pixel adds TextVeil
for dissolving text. Messages the font cannot draw are fixed and
TestMessagesHaveGlyphs holds every message to the font.
2026-10-12 01:53:20 -04:00

230 lines
8.6 KiB
Go

package gallery
import (
"fmt"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/data"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/game"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/icon"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/layout"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
var rpgEntries = []Entry{
{258, "SkillTree", "技能树", "game", "Spend points on open skills: click once to choose, twice to learn.", demoSkillTree},
{259, "SkillButton", "技能按钮", "game", "Cast, then wait out the clockwise cooldown wipe.", demoSkillButton},
{260, "StatusEffects", "状态效果", "game", "Buffs edged green, debuffs red, turns left in the corner.", demoStatusEffects},
{261, "BossBar", "首领血条", "game", "A boss in phases. Hit it until it gets angry.", demoBossBar},
{262, "DamageNumbers", "伤害数字", "game", "Numbers float up and dissolve. Criticals in gold!", demoDamageNumbers},
{263, "QuestLog", "任务日志", "game", "Choose a quest to track; its objectives show beside it.", demoQuestLog},
{264, "QuestTracker", "任务追踪", "game", "Compact objectives with counts. Go slay one.", demoQuestTracker},
{265, "PartyFrames", "队伍面板", "game", "Cards with HP and MP. Brom needs a phoenix down.", demoPartyFrames},
{266, "CharacterSheet", "角色面板", "game", "Who you are: class, level and stats.", demoCharacterSheet},
{267, "LevelUpScreen", "升级分配", "game", "LEVEL UP! Spend points with ◀ ▶, then DONE.", demoLevelUpScreen},
{268, "Shop", "商店", "game", "Buy at the price, sell at half. Too dear? It goes grey.", demoShop},
}
type rpgDemo struct {
skills []game.Skill
cursor, points, boss, quest int
slain int
cast time.Time
effects []game.Effect
hits []game.Hit
attrs []game.Attribute
wares []game.Ware
gold, raise int
}
func rpgState(c *ui.Context) *rpgDemo {
return ui.Local(c.Root(), demoKey("rpg"), func() rpgDemo {
return rpgDemo{skills: talents(), points: 2, boss: 1000, gold: 300, raise: 3,
effects: []game.Effect{{Sprite: icon.Bolt, Name: "Haste", Turns: 3}, {Sprite: icon.Shield, Name: "Guard", Turns: 2}, {Sprite: icon.Skull, Name: "Poison", Turns: 4, Debuff: true}},
attrs: []game.Attribute{{Name: "STR", Base: 12}, {Name: "DEX", Base: 9}, {Name: "INT", Base: 7}, {Name: "LUK", Base: 5}},
wares: []game.Ware{{Item: data.Item{Sprite: icon.Potion, Name: "Potion"}, Price: 30, Owned: 2}, {Item: data.Item{Sprite: icon.Key, Name: "Skeleton key"}, Price: 120},
{Item: data.Item{Sprite: icon.Sword, Name: "Flame sword"}, Price: 900}}}
})
}
// talents is a root, two branches and a capstone.
func talents() []game.Skill {
return []game.Skill{
{Name: "Slash", Sprite: icon.Sword, At: game.Spot{X: 1}, Learned: true},
{Name: "Guard", Sprite: icon.Shield, At: game.Spot{Y: 1}, Needs: []int{0}},
{Name: "Fireball", Sprite: icon.Flame, At: game.Spot{X: 2, Y: 1}, Needs: []int{0}},
{Name: "Bulwark", Sprite: icon.Crown, At: game.Spot{Y: 2}, Needs: []int{1}},
{Name: "Meteor", Sprite: icon.Star, At: game.Spot{X: 2, Y: 2}, Needs: []int{2}},
{Name: "Starfall", Sprite: icon.Sparkle, At: game.Spot{X: 1, Y: 3}, Needs: []int{3, 4}},
}
}
func demoSkillTree(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := rpgState(c)
ui.Column(c).Gap(k.D(6)).Children(func() {
r := game.SkillTree(c, "Talents", s.skills, &s.cursor, &s.points, game.SkillTreeOptions{})
if r.Changed {
log(s.skills[s.cursor].Name)
}
if r.Learned >= 0 {
log("learned " + s.skills[r.Learned].Name)
}
if small(c, "RESPEC", s.points == 2) {
s.skills, s.points = talents(), 2
log("a clean slate")
}
})
}
func demoSkillButton(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := rpgState(c)
// The spell cools for three seconds, in tenths.
cool := 1 - float64(k.Since(c, s.cast, 10, 30))/30
ui.Row(c).Gap(k.D(6)).Children(func() {
if game.SkillButton(c, "Fireball", icon.Flame, "Q", cool, game.SkillButtonOptions{}).Clicked {
s.cast = k.Now()
log("fireball!")
}
if game.SkillButton(c, "Heal", icon.Heart, "E", 0, game.SkillButtonOptions{}).Clicked {
log("you feel better")
}
game.SkillButton(c, "Meteor", icon.Star, "R", 0, game.SkillButtonOptions{Disabled: true})
})
}
func demoStatusEffects(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := rpgState(c)
ui.Row(c).Gap(k.D(8)).AlignItems(ui.Center).Children(func() {
game.StatusEffects(c, "Effects", s.effects, game.StatusEffectsOptions{})
if small(c, "END TURN", len(s.effects) == 0) {
left := s.effects[:0]
for _, ef := range s.effects {
if ef.Turns--; ef.Turns > 0 {
left = append(left, ef)
}
}
s.effects = left
log(fmt.Sprintf("%d effects left", len(s.effects)))
}
})
}
func demoBossBar(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := rpgState(c)
ui.Column(c).Gap(k.D(6)).Children(func() {
game.BossBar(c, "SLIME KING", s.boss, 1000, game.BossBarOptions{Width: 220, Phases: []float64{0.66, 0.33}})
if small(c, "ATTACK", s.boss == 0) {
was := s.boss
s.boss = max(0, s.boss-90)
log(fmt.Sprintf("hit! %d HP left", s.boss))
for _, mark := range []int{660, 330} {
if was > mark && s.boss <= mark {
log("the Slime King wobbles angrily!")
}
}
if s.boss == 0 {
log("victory!")
}
}
})
}
func demoDamageNumbers(c *ui.Context, log func(string)) {
k := pixel.K(c)
s, dice := rpgState(c), gameState(c).dice
ui.Column(c).Gap(k.D(4)).Children(func() {
// The numbers float over a slime having a bad day.
layout.Layers(c, func() {
ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) {
g := k.Canvas(p, r)
g.Sprite((g.W-3*slime.W)/2, g.H-3*slime.H, slime, 0, pixel.Ink, 3)
})
}, func() {
game.DamageNumbers(c, "Damage", &s.hits, game.DamageNumbersOptions{Width: 180, Height: 60})
})
if small(c, "ATTACK", false) {
h := game.Hit{Value: 5 + dice.IntN(30), X: 20 + dice.IntN(140), Y: 25 + dice.IntN(25)}
// One hit in five is critical, for double.
if h.Crit = dice.IntN(5) == 0; h.Crit {
h.Value *= 2
}
s.hits = append(s.hits, h)
log(fmt.Sprintf("hit for %d", h.Value))
}
})
}
// adventures are the quest demos' quests.
func adventures(slain int) []game.Quest {
return []game.Quest{
{Title: "Slime trouble", Objectives: []game.Objective{{Text: "Slay slimes", Have: slain, Need: 5}, {Text: "Tell the mayor", Have: slain / 5, Need: 1}}},
{Title: "Lost cat", Objectives: []game.Objective{{Text: "Find Mittens", Have: 1, Need: 1}, {Text: "Bring her home", Need: 1}}},
{Title: "Herb run", Objectives: []game.Objective{{Text: "Pick moonleaf", Have: 2, Need: 6}}},
}
}
func demoQuestLog(c *ui.Context, log func(string)) {
s := rpgState(c)
quests := adventures(s.slain)
if game.QuestLog(c, "Quests", quests, &s.quest, game.QuestLogOptions{}).Changed {
log("tracking " + quests[s.quest].Title)
}
}
func demoQuestTracker(c *ui.Context, log func(string)) {
k := pixel.K(c)
s := rpgState(c)
ui.Column(c).Gap(k.D(6)).Children(func() {
game.QuestTracker(c, adventures(s.slain)[0], game.QuestTrackerOptions{})
if small(c, "SLAY A SLIME", s.slain == 5) {
s.slain++
log(fmt.Sprintf("%d of 5", s.slain))
}
})
}
func demoPartyFrames(c *ui.Context, log func(string)) {
game.PartyFrames(c, "Party", []game.Member{
{Name: "Aria", Portrait: game.Hero, HP: 34, MaxHP: 40, MP: 18, MaxMP: 20},
{Name: "Cyra", Portrait: game.Hero, HP: 9, MaxHP: 30, MP: 31, MaxMP: 40},
{Name: "Brom", Portrait: game.Hero, MaxHP: 50, MP: 4, MaxMP: 10},
}, game.PartyFramesOptions{})
}
func demoCharacterSheet(c *ui.Context, log func(string)) {
game.CharacterSheet(c, game.Character{Name: "Aria", Class: "Knight", Level: 7, Portrait: game.Hero,
Stats: []game.Stat{{Name: "STR", Value: 14}, {Name: "DEX", Value: 9}, {Name: "INT", Value: 7}, {Name: "LUK", Value: 11}}}, game.CharacterSheetOptions{})
}
func demoLevelUpScreen(c *ui.Context, log func(string)) {
s := rpgState(c)
r := game.LevelUpScreen(c, "Level up", s.attrs, &s.raise, game.LevelUpScreenOptions{})
if r.Changed {
log(fmt.Sprintf("%d points left", s.raise))
}
if r.Done {
for i := range s.attrs {
s.attrs[i].Base, s.attrs[i].Added = s.attrs[i].Base+s.attrs[i].Added, 0
}
s.raise = 3
log("stronger! three more points, for the demo")
}
}
func demoShop(c *ui.Context, log func(string)) {
s := rpgState(c)
r := game.Shop(c, "Item shop", s.wares, &s.gold, game.ShopOptions{})
if r.Bought >= 0 {
log("bought " + s.wares[r.Bought].Item.Name)
}
if r.Sold >= 0 {
log("sold " + s.wares[r.Sold].Item.Name)
}
}