aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-01-20 03:18:06 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2025-01-27 16:43:44 +0100
commitc4cb9637f5c85d03aa162968f0dfec3c193fc9dc (patch)
tree3ad32e5dfd7c4ae72f66b86e77edef7b700a8b7a
parent9df5cc7b55d8ac9ecd775a14a14f80a6c36c4d74 (diff)
downloadmuhqs-game-c4cb9637f5c85d03aa162968f0dfec3c193fc9dc.tar.gz
muhqs-game-c4cb9637f5c85d03aa162968f0dfec3c193fc9dc.zip
intermediate commit
Implement actions and multiple ui widgets
-rw-r--r--go/Makefile14
-rw-r--r--go/client/main.go236
-rw-r--r--go/game/action.go133
-rw-r--r--go/game/ai.go123
-rw-r--r--go/game/artifact.go48
-rw-r--r--go/game/card.go21
-rw-r--r--go/game/equipment.go5
-rw-r--r--go/game/hand.go5
-rw-r--r--go/game/permanent.go60
-rw-r--r--go/game/player.go9
-rw-r--r--go/game/pos.go12
-rw-r--r--go/game/range.go8
-rw-r--r--go/game/stack.go29
-rw-r--r--go/game/state.go77
-rw-r--r--go/game/tile.go15
-rw-r--r--go/game/unit.go100
-rw-r--r--go/go.mod21
-rw-r--r--go/go.sum37
-rw-r--r--go/ui/buffer.go81
-rw-r--r--go/ui/button.go53
-rw-r--r--go/ui/choice.go68
-rw-r--r--go/ui/colors.go7
-rw-r--r--go/ui/font.go24
-rw-r--r--go/ui/mapView.go43
-rw-r--r--go/ui/textBox.go67
-rw-r--r--go/ui/widget.go10
26 files changed, 1187 insertions, 119 deletions
diff --git a/go/Makefile b/go/Makefile
index 0744d6c9..6678a0fa 100644
--- a/go/Makefile
+++ b/go/Makefile
@@ -1,4 +1,4 @@
-PACKAGES := assets game server client webtools
+PACKAGES := assets game server client webtools ui
APPS := server client
OBJS := $(foreach app,$(APPS),$(app)/$(app))
@@ -9,22 +9,22 @@ all: $(OBJS) $(WASM)
server/server:
@echo Building Server ...
- pushd $(@D) >/dev/null; go build; popd >/dev/null
+ pushd $(@D) >/dev/null && go build && popd >/dev/null
client/client:
@echo Building Client ...
- pushd $(@D) >/dev/null; go build; popd >/dev/null
+ pushd $(@D) >/dev/null && go build && popd >/dev/null
client/client.wasm:
@echo Building Client.wasm ...
- pushd $(@D) >/dev/null; env GOOS=js GOARCH=wasm go build -o $(@F); popd >/dev/null
+ pushd $(@D) >/dev/null && env GOOS=js GOARCH=wasm go build -o $(@F) && popd >/dev/null
webtools/webtools.wasm:
@echo Building Webtools.wasm ...
- pushd $(@D) >/dev/null; env GOOS=js GOARCH=wasm go build -o $(@F); popd >/dev/null
+ pushd $(@D) >/dev/null && env GOOS=js GOARCH=wasm go build -o $(@F) && popd >/dev/null
fmt:
- for package in $(PACKAGES); do pushd $$package >/dev/null; go fmt; popd >/dev/null; done
+ set -e; for package in $(PACKAGES); do pushd $$package >/dev/null && go fmt && popd >/dev/null; done
test:
- for package in $(PACKAGES); do pushd $$package >/dev/null; go test; popd >/dev/null; done
+ for package in $(PACKAGES); do pushd $$package >/dev/null && go test && popd >/dev/null; done
diff --git a/go/client/main.go b/go/client/main.go
index b0696fdf..ec8fff5e 100644
--- a/go/client/main.go
+++ b/go/client/main.go
@@ -2,6 +2,7 @@ package main
import (
"fmt"
+ "image/color"
"log"
"github.com/hajimehoshi/ebiten/v2"
@@ -16,6 +17,13 @@ import (
const (
screenWidth = 1280
screenHeight = 1024
+
+ DEFAULT_BUTTON_HEIGHT = 40
+
+ RESOLVE_BUTTON_X = 500
+ STACK_BUFFER_WIDTH = 300
+
+ HOVER_THRESHOLD = 60
)
type HandLayer struct {
@@ -23,23 +31,71 @@ type HandLayer struct {
func (l *HandLayer) Draw(screen *ebiten.Image) {
cardImg := assets.GetCard("base/archer", "en")
- // _, cardImgHeight := cardImg.Size()
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(0, float64(screenHeight))
op.GeoM.Scale(0.5, 0.5)
screen.DrawImage(cardImg, op)
}
+var (
+ playerGreen = color.RGBA{1, 0xff, 1, 0xff}
+ playerBlue = color.RGBA{1, 1, 0xff, 0xff}
+)
+
+type hoverDetector struct {
+ last_x, last_y int
+ ticks int
+ resetFunc func()
+}
+
+func (h *hoverDetector) update(x, y int) {
+ if x == h.last_x && y == h.last_y {
+ h.ticks++
+ } else {
+ h.last_x, h.last_y = x, y
+ h.reset()
+ }
+}
+func (h *hoverDetector) reset() {
+ h.ticks = 0
+ if h.resetFunc != nil {
+ h.resetFunc()
+ h.resetFunc = nil
+ }
+}
+
+func (h *hoverDetector) hovering() bool {
+ return h.ticks >= HOVER_THRESHOLD
+}
+
+func (h *hoverDetector) startedHovering() bool {
+ return h.ticks == HOVER_THRESHOLD
+}
+
type Game struct {
- gameState *game.State
- handLayer *HandLayer
- mapView *ui.MapView
- activeObject interface{}
+ gameState *game.State
+ handLayer *HandLayer
+ mapView *ui.MapView
+ resolveButton *ui.SimpleButton
+ stackBuffer *ui.Buffer
+ widgets []ui.Widget
+ selectedObject interface{}
+ hoverDetector hoverDetector
}
func NewGame() *Game {
g := &Game{}
+ g.widgets = []ui.Widget{}
g.gameState = game.NewState()
+
+ g.resolveButton = ui.NewSimpleButton(screenWidth-125, screenHeight-DEFAULT_BUTTON_HEIGHT,
+ 125, DEFAULT_BUTTON_HEIGHT, func(*ui.SimpleButton) {
+ g.resolveAction()
+ }, "Resolve")
+ g.addWidget(g.resolveButton)
+
+ g.stackBuffer = ui.NewBuffer(screenWidth-STACK_BUFFER_WIDTH, g.resolveButton.Y-400,
+ STACK_BUFFER_WIDTH, 400)
return g
}
@@ -58,8 +114,8 @@ func (g *Game) loadMap(mapName string) {
g.mapView = ui.NewMapView(g.gameState)
}
-func (g *Game) addPlayer(name string, deck *game.Deck) {
- g.gameState.AddNewPlayer(name, deck)
+func (g *Game) addPlayer(name string, deck *game.Deck, color color.Color) {
+ g.gameState.AddNewPlayer(name, deck, color)
}
func (g *Game) getPlayer(name string) *game.Player {
@@ -71,13 +127,50 @@ func (g *Game) getPlayer(name string) *game.Player {
return nil
}
-func (g *Game) moveUnit(unit *game.Unit, tile *game.Tile) {
- g.gameState.MoveUnit(unit, tile)
+func (g *Game) addWidget(w ui.Widget) {
+ g.widgets = append(g.widgets, w)
+}
+
+func (g *Game) removeWidget(widget ui.Widget) {
+ nwidgets := len(g.widgets)
+ for i, w := range g.widgets {
+ if w != widget {
+ continue
+ }
+
+ g.widgets[i] = g.widgets[nwidgets-1]
+ g.widgets = g.widgets[:nwidgets-1]
+ return
+ }
+}
+
+func (g *Game) clearMapHighlights() {
+ g.mapView.ClearPermanentsHighlights()
g.mapView.ClearTileHighlights()
g.mapView.ForceRedraw()
}
+func (g *Game) declareAction(a game.Action) {
+ a.Declare(g.gameState)
+ g.clearMapHighlights()
+ g.stackBuffer.AddLine(a.String())
+}
+
+func (g *Game) resolveAction() {
+ if g.gameState.Stack.IsEmpty() {
+ return
+ }
+ g.gameState.Stack.Pop()
+ g.clearMapHighlights()
+ g.stackBuffer.RemoveLast()
+}
+
func (g *Game) findObjectAt(x, y int) interface{} {
+ for _, b := range g.widgets {
+ if obj := b.FindObjectAt(x, y); obj != nil {
+ return obj
+ }
+ }
if obj := g.mapView.FindObjectAt(x, y); obj != nil {
return obj
}
@@ -86,33 +179,98 @@ func (g *Game) findObjectAt(x, y int) interface{} {
}
func (g *Game) Update() error {
+ x, y := ebiten.CursorPosition()
+ g.hoverDetector.update(x, y)
+
+ obj := g.findObjectAt(x, y)
+
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
- x, y := ebiten.CursorPosition()
- obj := g.findObjectAt(x, y)
- if obj != nil {
- switch obj.(type) {
- case *game.Tile:
- tile := obj.(*game.Tile)
- if g.activeObject != nil {
- switch g.activeObject.(type) {
- case *game.Unit:
- unit := g.activeObject.(*game.Unit)
- g.moveUnit(unit, tile)
- g.activeObject = nil
- }
- } else {
+ if obj == nil {
+ fmt.Printf("No object found at cursor position (%d, %d)\n", x, y)
+ return nil
+ }
+
+ log.Printf("Active object is of type %T\n", g.selectedObject)
+
+ switch obj.(type) {
+ case ui.Button:
+ button := obj.(ui.Button)
+ button.Click(x, y)
+
+ case *game.Tile:
+ tile := obj.(*game.Tile)
+ if g.selectedObject != nil {
+ switch g.selectedObject.(type) {
+ case *game.MoveActionPrototype:
+ proto := g.selectedObject.(*game.MoveActionPrototype)
+ action := proto.Specialize(tile)
+ g.selectedObject = nil
+ g.declareAction(action)
g.mapView.HighlightTile(tile)
}
- case *game.Unit:
- unit := obj.(*game.Unit)
- g.activeObject = unit
- g.mapView.HighlightTiles(unit.MoveRangeTiles(g.gameState.Map))
- default:
- log.Fatalf("Object of type %T not handled", obj)
+ } else {
+ g.mapView.HighlightTile(tile)
}
- } else {
- fmt.Printf("No object found at cursor position (%d, %d)\n", x, y)
+
+ case game.Permanent:
+ perm := obj.(game.Permanent)
+ if g.selectedObject == nil {
+ actions := perm.GetAvailActionPrototypes()
+ if len(actions) > 0 {
+ g.selectedObject = perm
+ onClick := func(c *ui.Choice, x, y int) {
+ g.removeWidget(c)
+ g.selectedObject = actions[c.GetChoosen(x, y)]
+ }
+ g.addWidget(ui.NewActionChoice(500, 200, actions, onClick))
+ }
+ } else {
+ switch g.selectedObject.(type) {
+ case *game.AttackActionPrototype:
+ proto := g.selectedObject.(*game.AttackActionPrototype)
+ action := proto.Specialize(perm)
+ g.selectedObject = nil
+ g.declareAction(action)
+ g.mapView.HighlightPermanent(perm)
+ }
+ }
+
+ default:
+ log.Fatalf("Object of type %T not handled", obj)
+ }
+
+ switch g.selectedObject.(type) {
+ case *game.MoveActionPrototype:
+ proto := g.selectedObject.(*game.MoveActionPrototype)
+ g.mapView.HighlightTiles(proto.Unit.MoveRangeTiles(g.gameState.Map))
+ case *game.AttackActionPrototype:
+ proto := g.selectedObject.(*game.AttackActionPrototype)
+ perms := proto.Unit.AttackablePermanents(g.gameState.Map)
+ g.mapView.HighlightPermanents(perms)
+ }
+ } else if g.hoverDetector.startedHovering() && g.selectedObject == nil && obj != nil {
+ var hoverHint *ui.TextBox
+ switch obj.(type) {
+ case *game.Unit:
+ u := obj.(*game.Unit)
+ hoverHint = ui.NewUnitInfo(x+ui.TILE_WIDTH, y, u)
+ case game.Permanent:
+ p := obj.(game.Permanent)
+ hoverHint = ui.NewPermInfo(x+ui.TILE_WIDTH, y, p)
}
+
+ if hoverHint != nil {
+ g.addWidget(hoverHint)
+ g.hoverDetector.resetFunc = func() {
+ g.removeWidget(hoverHint)
+ }
+ }
+ } else if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
+ g.resolveAction()
+ } else if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
+ g.selectedObject = nil
+ g.hoverDetector.reset()
+ g.clearMapHighlights()
}
return nil
}
@@ -120,6 +278,12 @@ func (g *Game) Update() error {
func (g *Game) Draw(screen *ebiten.Image) {
g.handLayer.Draw(screen)
g.mapView.Draw(screen)
+ for _, b := range g.widgets {
+ b.Draw(screen)
+ }
+ if !g.gameState.Stack.IsEmpty() {
+ g.stackBuffer.Draw(screen)
+ }
ebitenutil.DebugPrint(screen,
fmt.Sprintf("TPS: %0.2f FPS: %0.2f", ebiten.ActualTPS(), ebiten.ActualFPS()))
}
@@ -129,14 +293,20 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
}
func main() {
+ ui.InitFont()
+
ebiten.SetWindowSize(screenWidth, screenHeight)
ebiten.SetWindowTitle("Muhq's Game")
g := NewGameFromMap("2P-ring-street")
- g.addPlayer("muhq", game.NewDeckFromDeckList(g.gameState.Map.StartDeckList))
+ g.addPlayer("muhq", game.NewDeckFromDeckList(g.gameState.Map.StartDeckList), playerBlue)
muhq := g.getPlayer("muhq")
- g.gameState.AddNewUnit("base/archer", game.Position{0, 0}, muhq)
+ g.gameState.AddNewUnit("base/archer", game.Position{X: 0, Y: 0}, muhq)
+
+ g.addPlayer("enemy", game.NewDeckFromDeckList(g.gameState.Map.StartDeckList), playerGreen)
+ enemy := g.getPlayer("enemy")
+ g.gameState.AddNewArtifact("base/palisade", game.Position{X: 1, Y: 1}, enemy)
if err := ebiten.RunGame(g); err != nil {
log.Fatal(err)
diff --git a/go/game/action.go b/go/game/action.go
index 723d4866..58eea8da 100644
--- a/go/game/action.go
+++ b/go/game/action.go
@@ -1,6 +1,133 @@
package game
-type Action struct {
- Owner *Player
- Card *Card
+import (
+ "fmt"
+ "log"
+)
+
+type ActionPrototype interface {
+ Specialize(...interface{}) Action
+ String() string
+}
+
+type Action interface {
+ Declare(s *State)
+ Resolve(s *State)
+ String() string
+}
+
+type FastAction interface {
+ Action
+ FastDeclare(s *State)
+}
+
+type ActionBase struct {
+ Source interface{}
+ Card *Card
+ resolveFunc func(s *State)
+ desc string
+}
+
+func (a *ActionBase) Declare(s *State) {
+ if !s.Stack.IsEmpty() {
+ log.Fatalf("Slow action can not be declared while other actions are on the stack")
+ }
+ s.Stack.Push(a)
+}
+
+func (a *ActionBase) Resolve(s *State) {
+ a.resolveFunc(s)
+}
+
+func (a *ActionBase) String() string {
+ if p, ok := a.Source.(Permanent); ok {
+ return fmt.Sprintf("%s: %s %s", p.GetController().Name, p.GetTile().Position.String(), a.desc)
+ }
+ return fmt.Sprintf("%s %s", a.Source, a.desc)
+}
+
+type MoveActionPrototype struct {
+ Unit *Unit
+}
+
+func NewMoveActionPrototype(u *Unit) *MoveActionPrototype {
+ return &MoveActionPrototype{u}
+}
+
+func (proto *MoveActionPrototype) String() string {
+ return fmt.Sprintf("move %d", proto.Unit.Movement)
+}
+
+func (proto *MoveActionPrototype) Specialize(args ...interface{}) Action {
+ target := args[0].(*Tile)
+ u := proto.Unit
+
+ resolveFunc := func(s *State) {
+ u.move(s, target)
+ }
+ desc := fmt.Sprintf("-> %s", target.Position.String())
+ return &ActionBase{u, u.GetCard(), resolveFunc, desc}
+}
+
+type AttackActionPrototype struct {
+ Unit *Unit
+}
+
+func NewAttackActionPrototype(u *Unit) *AttackActionPrototype {
+ return &AttackActionPrototype{u}
+}
+
+func (proto *AttackActionPrototype) String() string {
+ return fmt.Sprintf("attack %s", proto.Unit.Attack.String())
+}
+
+func (proto *AttackActionPrototype) Specialize(args ...interface{}) Action {
+ target := args[0].(Permanent)
+ u := proto.Unit
+ resolveFunc := func(s *State) {
+ s.Fight(u, target)
+ }
+ desc := fmt.Sprintf("x %s", target.GetTile().Position.String())
+ return &ActionBase{u, u.GetCard(), resolveFunc, desc}
+}
+
+type FullActionPrototype struct {
+ Permanent Permanent
+}
+
+func (proto *FullActionPrototype) String() string {
+ return "full_action"
+}
+
+func (proto *FullActionPrototype) Specialize(args ...interface{}) Action {
+ p := proto.Permanent
+ resolveFunc := args[0].(func(s *State))
+ desc := "full_action"
+ return &ActionBase{p, p.GetCard(), resolveFunc, desc}
+}
+
+type FreeActionPrototype struct {
+ Permanent Permanent
+}
+
+func (proto *FreeActionPrototype) String() string {
+ return "free_action"
+}
+
+func (proto *FreeActionPrototype) Specialize(args ...interface{}) Action {
+ p := proto.Permanent
+ resolveFunc := args[0].(func(s *State))
+ desc := "free_action"
+ return &FreeAction{ActionBase{p, p.GetCard(), resolveFunc, desc}}
+}
+
+type FreeAction struct {
+ ActionBase
+}
+
+func (a *FreeAction) FastDeclare(s *State) {
+ s.Stack.Push(a)
+}
+
+func (a *FreeAction) Resolve(s *State) {
}
diff --git a/go/game/ai.go b/go/game/ai.go
new file mode 100644
index 00000000..b1921639
--- /dev/null
+++ b/go/game/ai.go
@@ -0,0 +1,123 @@
+package game
+
+func moveToRandomTile(s *State, u *Unit) {
+ moveTargets := u.MoveRangeTiles(s.Map)
+ if len(moveTargets) > 0 {
+ moveTarget := moveTargets[s.Rand.Intn(len(moveTargets))-1]
+ s.MoveUnit(u, moveTarget)
+ }
+}
+
+func attackAttackableEnemyPerm(s *State, u *Unit) {
+ attackablePerms := u.AttackablePermanents(s.Map)
+ if len(attackablePerms) > 0 {
+ attackablePerm := attackablePerms[s.Rand.Intn(len(attackablePerms))-1]
+ s.Fight(u, attackablePerm)
+ }
+}
+
+// 1. If enemy Unit in attack Range then
+// attack enemy Unit in Range
+// 2. While move action available do
+// 3a. If enemy unit exists
+// move towards nearest enemy unit until it is in attack range
+// if attack action avail then proceed at 1.
+// 3b. Otherwise
+// move to random tile in movement range
+func AggressiveAi(s *State, u *Unit) {
+ // 1.
+ attackAttackableEnemyPerm(s, u)
+
+ // 2.
+ if u.AvailMoveActions > 0 {
+ enemyUnits := s.EnemyUnits(u.GetController())
+ if len(enemyUnits) > 0 { // 3a.
+ // TODO: implement path finding
+ if u.AvailAttackActions > 0 {
+ AggressiveAi(s, u)
+ }
+ } else { // 3b.
+ moveToRandomTile(s, u)
+ }
+ }
+}
+
+// 2.2 Shy
+// 1. If not in the attack Range of an enemy Unit then
+// activate full action if available
+// 2. If move action available then
+// move to most distant point from all enemy units
+// 3. If enemy Unit in attack Range then
+// attack enemy Unit in Range
+func ShyAi(s *State, u *Unit) {
+ // 1.
+ inEnemyAttackRange := false
+out:
+ for _, enemyUnit := range s.EnemyUnits(u.GetController()) {
+ for _, p := range enemyUnit.AttackablePermanents(s.Map) {
+ if p == u {
+ inEnemyAttackRange = true
+ break out
+ }
+ }
+ }
+ if !inEnemyAttackRange && u.HasFullAction() {
+ u.FullAction()
+ }
+
+ // 2.
+ if u.AvailMoveActions > 0 {
+ // TODO: implement path finding
+ }
+
+ // 3.
+ if u.AvailAttackActions > 0 {
+ attackAttackableEnemyPerm(s, u)
+ }
+}
+
+// 2.3 Wandering X
+// 1. If enemy unit in Range X then
+// execute aggressive AI
+// 2. If move action available then
+// move to random tile in movement Range
+// proceed at 1.
+func WanderingAi(s *State, u *Unit, x int) {
+ // 1.
+ for _, enemyUnit := range s.EnemyUnits(u.GetController()) {
+ if IsPositionInRange(u.GetTile().Position, enemyUnit.GetTile().Position, x) {
+ AggressiveAi(s, u)
+ break
+ }
+ }
+ // 2.
+ if u.AvailMoveActions > 0 {
+ moveToRandomTile(s, u)
+ WanderingAi(s, u, x)
+ }
+}
+
+// 2.4 Target-oriented TARGET
+// 1. Use full action if possible
+// 2a. If a TARGET exists and is reachable
+//
+// Move onto or towards nearest TARGET
+// 3a. If enemy Unit in attack Range
+// attack enemy Unit in Range
+//
+// 2b. Otherwise
+//
+// execute wandering 3 AI
+func TargetOrientedAi(s *State, u *Unit, t *Position) {
+ // 1.
+ if u.HasFullAction() {
+ u.FullAction()
+ }
+
+ if t != nil {
+ // TODO: path finding again
+ attackAttackableEnemyPerm(s, u)
+ } else {
+ WanderingAi(s, u, 3)
+ }
+}
diff --git a/go/game/artifact.go b/go/game/artifact.go
new file mode 100644
index 00000000..452d5a6a
--- /dev/null
+++ b/go/game/artifact.go
@@ -0,0 +1,48 @@
+package game
+
+import (
+ "log"
+ "strconv"
+ "strings"
+)
+
+type Artifact struct {
+ PermanentBase
+ Solid int
+}
+
+func NewArtifact(cardPath string, tile *Tile, owner *Player) *Artifact {
+ card := NewCard(cardPath)
+
+ a := &Artifact{NewPermanentBase(card, tile, owner), 0}
+ tile.Permanent = a
+
+ effects := card.getEffects()
+ for _, e := range effects {
+ if strings.HasPrefix(e, "Solid") {
+ tokens := strings.Split(e, " ")
+ var err error
+ a.Solid, err = strconv.Atoi(tokens[1])
+ if err != nil {
+ log.Fatalf("Invalid Solid definition %s\n", e)
+ }
+ }
+ }
+
+ return a
+}
+
+func (a *Artifact) Fight(p Permanent) {
+}
+
+func (a *Artifact) IsDestroyed() bool {
+ return a.Damage >= a.Solid
+}
+
+func (a *Artifact) Attackable() bool {
+ return a.Solid > 0
+}
+
+func (a *Artifact) GetAvailActionPrototypes() []ActionPrototype {
+ return []ActionPrototype{}
+}
diff --git a/go/game/card.go b/go/game/card.go
index f77a274a..bfa476a9 100644
--- a/go/game/card.go
+++ b/go/game/card.go
@@ -132,6 +132,27 @@ func (card *Card) parseDefinition(definition []byte) {
}
}
+func (card *Card) getEffects() []string {
+ effects, found := card.Values["effect"]
+ if !found {
+ return []string{}
+ }
+
+ if effect, ok := effects.(string); ok {
+ return []string{effect}
+ }
+
+ if effectsMap, ok := effects.(map[string]interface{}); ok {
+ enEffects := effectsMap["en"]
+ if effect, ok := enEffects.(string); ok {
+ return []string{effect}
+ }
+ return enEffects.([]string)
+ }
+ return effects.([]string)
+
+}
+
func NewCard(cardPath string) *Card {
cardName := path.Base(cardPath)
set := path.Dir(cardPath)
diff --git a/go/game/equipment.go b/go/game/equipment.go
new file mode 100644
index 00000000..1b78c8ad
--- /dev/null
+++ b/go/game/equipment.go
@@ -0,0 +1,5 @@
+package game
+
+type Equipment struct {
+ Artifact
+}
diff --git a/go/game/hand.go b/go/game/hand.go
index fde4a686..169c8cb3 100644
--- a/go/game/hand.go
+++ b/go/game/hand.go
@@ -22,11 +22,14 @@ func (h *Hand) AddCard(card *Card) {
}
func (h *Hand) RemoveCard(card *Card) {
+ ncards := len(h.Cards)
for i, c := range h.Cards {
if c != card {
continue
}
- h.Cards = append(h.Cards[:i], h.Cards[i+1:]...)
+ h.Cards[i] = h.Cards[ncards-1]
+ h.Cards = h.Cards[:ncards-1]
+ return
}
}
diff --git a/go/game/permanent.go b/go/game/permanent.go
index 37111c4d..7a9988e1 100644
--- a/go/game/permanent.go
+++ b/go/game/permanent.go
@@ -1,10 +1,66 @@
package game
-type Permanent struct {
+import (
+ "fmt"
+ "log"
+)
+
+type Permanent interface {
+ GetCard() *Card
+ GetTile() *Tile
+ GetDamage() int
+ GetPile() []Permanent
+ GetController() *Player
+ GetOwner() *Player
+
+ String() string
+ Fight(p Permanent)
+ AddDamage(damage int)
+ IsDestroyed() bool
+ Attackable() bool
+ GetAvailActionPrototypes() []ActionPrototype
+}
+
+type PermanentBase struct {
Card *Card
Tile *Tile
Damage int
- Pile []*Permanent
+ Pile []Permanent
Controller *Player
Owner *Player
}
+
+func NewPermanentBase(card *Card, tile *Tile, owner *Player) PermanentBase {
+ if tile.Permanent != nil {
+ log.Fatal("Tile at %v already occupied by %v", tile.Position, tile.Permanent)
+ }
+ return PermanentBase{card, tile, 0, []Permanent{}, owner, owner}
+}
+
+func (p *PermanentBase) GetCard() *Card {
+ return p.Card
+}
+func (p *PermanentBase) GetTile() *Tile {
+ return p.Tile
+}
+func (p *PermanentBase) GetDamage() int {
+ return p.Damage
+}
+func (p *PermanentBase) GetPile() []Permanent {
+ return p.Pile
+}
+func (p *PermanentBase) GetController() *Player {
+ return p.Controller
+}
+func (p *PermanentBase) GetOwner() *Player {
+ return p.Owner
+}
+
+func (p *PermanentBase) String() string {
+ return fmt.Sprintf("%s's %s@%s",
+ p.GetController().Name, p.GetCard().Name, p.GetTile().Position.String())
+}
+
+func (p *PermanentBase) AddDamage(damage int) {
+ p.Damage += damage
+}
diff --git a/go/game/player.go b/go/game/player.go
index 20ac13a5..78ba69a7 100644
--- a/go/game/player.go
+++ b/go/game/player.go
@@ -1,5 +1,9 @@
package game
+import (
+ "image/color"
+)
+
const (
MAX_DRAW int = 3
)
@@ -12,10 +16,11 @@ type Player struct {
Deck *Deck
Ressource int
gameState *State
+ Color color.Color
}
-func NewPlayer(id int, name string, deck *Deck, gameState *State) *Player {
- p := Player{id, name, NewHand(), NewDiscardPile(), deck, 0, gameState}
+func NewPlayer(id int, name string, deck *Deck, gameState *State, color color.Color) *Player {
+ p := Player{id, name, NewHand(), NewDiscardPile(), deck, 0, gameState, color}
return &p
}
diff --git a/go/game/pos.go b/go/game/pos.go
index 2b3459cd..2a646a83 100644
--- a/go/game/pos.go
+++ b/go/game/pos.go
@@ -1,10 +1,22 @@
package game
+import (
+ "fmt"
+)
+
type Position struct {
X int
Y int
}
+func (p *Position) String() string {
+ if *p == INVALID_POSITION() {
+ return "INVALID_POSITION"
+ }
+
+ return fmt.Sprintf("(%d, %d)", p.X, p.Y)
+}
+
func INVALID_POSITION() Position {
return Position{-1, -1}
}
diff --git a/go/game/range.go b/go/game/range.go
index 86ec2f50..e12e1e67 100644
--- a/go/game/range.go
+++ b/go/game/range.go
@@ -37,3 +37,11 @@ func PositionsInRange(origin Position, r int, includeOrigin bool) []Position {
}
return positions
}
+
+func TilesInRange(m *Map, p Permanent, r int) []*Tile {
+ tiles := []*Tile{}
+ for _, pos := range PositionsInRange(p.GetTile().Position, r, false) {
+ tiles = append(tiles, m.TileAt(pos))
+ }
+ return tiles
+}
diff --git a/go/game/stack.go b/go/game/stack.go
index 5d62e601..22c20fa3 100644
--- a/go/game/stack.go
+++ b/go/game/stack.go
@@ -1,5 +1,32 @@
package game
+import (
+ "log"
+)
+
type Stack struct {
- Actions []Action
+ gameState *State
+ Actions []Action
+}
+
+func NewStack(s *State) *Stack {
+ return &Stack{s, []Action{}}
+}
+
+func (s *Stack) IsEmpty() bool {
+ return len(s.Actions) == 0
+}
+
+func (s *Stack) Push(a Action) {
+ s.Actions = append(s.Actions, a)
+}
+
+func (s *Stack) Pop() {
+ l := len(s.Actions)
+ if l == 0 {
+ log.Fatalf("Can not pop from empty stack")
+ }
+ a := s.Actions[l-1]
+ s.Actions = s.Actions[:l-1]
+ a.Resolve(s.gameState)
}
diff --git a/go/game/state.go b/go/game/state.go
index caae8106..3d600093 100644
--- a/go/game/state.go
+++ b/go/game/state.go
@@ -1,5 +1,10 @@
package game
+import (
+ "image/color"
+ "math/rand"
+)
+
type State struct {
Decks []*Deck
Stores []*Store
@@ -8,25 +13,87 @@ type State struct {
Hands []*Hand
DiscardPiles []*DiscardPile
Players []*Player
- Permanents []*Permanent
+ Permanents []Permanent
Units []*Unit
+ Artifacts []*Artifact
+ Rand rand.Rand
}
func NewState() *State {
- return &State{}
+ s := &State{}
+ s.Stack = NewStack(s)
+ return s
}
-func (s *State) AddNewPlayer(name string, deck *Deck) {
- p := NewPlayer(len(s.Players), name, deck, s)
+func (s *State) AddNewPlayer(name string, deck *Deck, color color.Color) {
+ p := NewPlayer(len(s.Players), name, deck, s, color)
s.Players = append(s.Players, p)
}
func (s *State) AddNewUnit(card string, pos Position, owner *Player) {
unit := NewUnit(card, s.Map.TileAt(pos), owner)
- s.Permanents = append(s.Permanents, &unit.Permanent)
+ s.Permanents = append(s.Permanents, unit)
s.Units = append(s.Units, unit)
}
+func (s *State) removePermanent(p Permanent) {
+ for i, perm := range s.Permanents {
+ if perm == p {
+ s.Permanents[i] = s.Permanents[len(s.Permanents)-1]
+ s.Permanents = s.Permanents[:len(s.Permanents)-1]
+ }
+ }
+}
+
+func removePtr[P *Unit | *Artifact](collection []P, ptr P) []P {
+ for i, p := range collection {
+ if p == ptr {
+ collection[i] = collection[len(collection)-1]
+ return collection[:len(collection)-1]
+ }
+ }
+ return collection
+}
+
+func (s *State) DestroyPermanent(p Permanent) {
+ s.removePermanent(p)
+
+ switch p.(type) {
+ case *Unit:
+ s.Units = removePtr(s.Units, p.(*Unit))
+ case *Artifact:
+ s.Artifacts = removePtr(s.Artifacts, p.(*Artifact))
+ }
+}
+
+func (s *State) AddNewArtifact(card string, pos Position, owner *Player) {
+ artifact := NewArtifact(card, s.Map.TileAt(pos), owner)
+ s.Permanents = append(s.Permanents, artifact)
+ s.Artifacts = append(s.Artifacts, artifact)
+}
+
+func (s *State) Fight(p1, p2 Permanent) {
+ p1.Fight(p2)
+ p2.Fight(p1)
+ if p1.IsDestroyed() {
+ s.DestroyPermanent(p1)
+ }
+ if p2.IsDestroyed() {
+ s.DestroyPermanent(p2)
+ }
+}
+
func (s *State) MoveUnit(u *Unit, t *Tile) {
u.move(s, t)
}
+
+func (s *State) EnemyUnits(player *Player) []*Unit {
+ // TODO: support coop / teams
+ enemyUnits := []*Unit{}
+ for _, u := range s.Units {
+ if u.GetController() != player {
+ enemyUnits = append(enemyUnits, u)
+ }
+ }
+ return enemyUnits
+}
diff --git a/go/game/tile.go b/go/game/tile.go
index 50688d83..b8e8c83f 100644
--- a/go/game/tile.go
+++ b/go/game/tile.go
@@ -2,7 +2,6 @@ package game
import (
"log"
- "strconv"
"strings"
)
@@ -62,27 +61,19 @@ var TileNames = map[string]TileType{
type Tile struct {
Position Position
- Permanent *Permanent
+ Permanent Permanent
Type TileType
Raw string
- PlayerId int
}
func INVALID_TILE() Tile {
- return Tile{INVALID_POSITION(), nil, 0, "", -1}
+ return Tile{INVALID_POSITION(), nil, 0, ""}
}
func NewTileFromString(tile string, pos Position) (Tile, error) {
tokens := strings.Split(tile, " ")
tileType := TileNames[strings.ToLower(tokens[0])]
- t := Tile{pos, nil, tileType, tile, -1}
- if tokens[0] == "spawn" {
- playerId, err := strconv.Atoi(tokens[2])
- if err != nil {
- return INVALID_TILE(), err
- }
- t.PlayerId = playerId
- }
+ t := Tile{pos, nil, tileType, tile}
return t, nil
}
diff --git a/go/game/unit.go b/go/game/unit.go
index 4c07d625..b81ea8b3 100644
--- a/go/game/unit.go
+++ b/go/game/unit.go
@@ -1,16 +1,17 @@
package game
import (
+ "fmt"
"golang.org/x/exp/slices"
"log"
"strconv"
"strings"
)
-type UnitStates int
+type UnitState int
const (
- Paralysis UnitStates = iota + 1
+ Paralysis UnitState = iota + 1
Poison
Panic
Rage
@@ -22,6 +23,13 @@ type Attack struct {
Range int
}
+func (a *Attack) String() string {
+ if a.Range == 1 {
+ return fmt.Sprintf("%d", a.Damage)
+ }
+ return fmt.Sprintf("%d range %d", a.Damage, a.Range)
+}
+
func parseAttack(attack string) Attack {
tokens := strings.Split(attack, " ")
a, err := strconv.Atoi(tokens[0])
@@ -40,34 +48,46 @@ func parseAttack(attack string) Attack {
}
type Unit struct {
- Permanent
- Health int
- Movement int
- Attack Attack
- Upkeep int
- // FullActions []FullActions
- // FreeActions []FreeActions
- States []UnitStates
+ PermanentBase
+ Health int
+ Movement int
+ Attack Attack
+ Upkeep int
+ FullActions []*FullActionPrototype
+ FreeActions []*FreeActionPrototype
+ States []UnitState
// Effects []Effects
+ AvailMoveActions int
+ AvailAttackActions int
}
func NewUnit(cardPath string, tile *Tile, owner *Player) *Unit {
card := NewCard(cardPath)
- return &Unit{
- Permanent{card, tile, 0, []*Permanent{}, owner, owner},
+ u := &Unit{
+ NewPermanentBase(card, tile, owner),
card.Values["health"].(int),
card.Values["movement"].(int),
parseAttack(card.Values["attack"].(string)),
card.Values["upkeep"].(int),
- []UnitStates{},
+ []*FullActionPrototype{},
+ []*FreeActionPrototype{},
+ []UnitState{},
+ 1,
+ 1,
}
+
+ tile.Permanent = u
+ return u
}
func (u *Unit) move(s *State, tile *Tile) {
if !slices.Contains(u.MoveRangeTiles(s.Map), tile) {
log.Panicf("Unit %s at %v can not move to %v", u.Card.Name, u.Tile.Position, tile.Position)
}
+ if tile.Permanent != nil {
+ log.Panicf("Unit %s at %v can not move to occupied %v", u.Card.Name, u.Tile.Position, tile.Position)
+ }
u.Tile.Permanent = nil
u.Tile = tile
@@ -84,6 +104,60 @@ func (u *Unit) MoveRangeTiles(m *Map) []*Tile {
return tiles
}
+func (u *Unit) AttackableTiles(m *Map) []*Tile {
+ return TilesInRange(m, u, u.Attack.Range)
+}
+
+func (u *Unit) AttackablePermanents(m *Map) []Permanent {
+ permanents := []Permanent{}
+ for _, t := range u.AttackableTiles(m) {
+ if t.Permanent != nil && t.Permanent.Attackable() {
+ permanents = append(permanents, t.Permanent)
+ }
+ }
+ return permanents
+}
+
func (u *Unit) IsAvailableTile(tile *Tile) bool {
return tile.IsFree()
}
+
+func (u *Unit) Fight(p Permanent) {
+ p.AddDamage(u.Attack.Damage)
+}
+
+func (u *Unit) IsDestroyed() bool {
+ return u.PermanentBase.Damage >= u.Health
+}
+
+func (u *Unit) Attackable() bool {
+ return true
+}
+
+func (u *Unit) HasFullAction() bool {
+ return len(u.FullActions) > 0
+}
+
+func (u *Unit) FullAction() {
+}
+
+func (u *Unit) GetAvailActionPrototypes() []ActionPrototype {
+ prototypes := []ActionPrototype{}
+ if u.AvailMoveActions > 0 {
+ prototypes = append(prototypes, NewMoveActionPrototype(u))
+ }
+
+ if u.AvailAttackActions > 0 {
+ prototypes = append(prototypes, NewAttackActionPrototype(u))
+ }
+
+ for _, p := range u.FullActions {
+ prototypes = append(prototypes, p)
+ }
+
+ for _, p := range u.FreeActions {
+ prototypes = append(prototypes, p)
+ }
+
+ return prototypes
+}
diff --git a/go/go.mod b/go/go.mod
index 23ca4ee1..6f97bdaf 100644
--- a/go/go.mod
+++ b/go/go.mod
@@ -3,18 +3,19 @@ module muhq.space/muhqs-game/go
go 1.19
require (
- github.com/hajimehoshi/ebiten/v2 v2.4.13
- golang.org/x/exp v0.0.0-20221230185412-738e83a70c30
+ github.com/hajimehoshi/ebiten/v2 v2.4.15
+ golang.org/x/exp v0.0.0-20230116083435-1de6713980de
+ golang.org/x/image v0.3.0
gopkg.in/yaml.v3 v3.0.1
)
require (
- github.com/ebitengine/purego v0.0.0-20220905075623-aeed57cda744 // indirect
- github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad // indirect
- github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41 // indirect
- github.com/jezek/xgb v1.0.1 // indirect
- golang.org/x/exp/shiny v0.0.0-20230111222715-75897c7a292a // indirect
- golang.org/x/image v0.1.0 // indirect
- golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105 // indirect
- golang.org/x/sys v0.1.0 // indirect
+ github.com/ebitengine/purego v0.1.1 // indirect
+ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect
+ github.com/hajimehoshi/file2byteslice v1.0.0 // indirect
+ github.com/jezek/xgb v1.1.0 // indirect
+ golang.org/x/exp/shiny v0.0.0-20230116083435-1de6713980de // indirect
+ golang.org/x/mobile v0.0.0-20221110043201-43a038452099 // indirect
+ golang.org/x/sys v0.4.0 // indirect
+ golang.org/x/text v0.6.0 // indirect
)
diff --git a/go/go.sum b/go/go.sum
index 9f167880..1a81401d 100644
--- a/go/go.sum
+++ b/go/go.sum
@@ -1,19 +1,24 @@
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/ebitengine/purego v0.0.0-20220905075623-aeed57cda744 h1:A8UnJ/5OKzki4HBDwoRQz7I6sxKsokpMXcGh+fUxpfc=
github.com/ebitengine/purego v0.0.0-20220905075623-aeed57cda744/go.mod h1:Eh8I3yvknDYZeCuXH9kRNaPuHEwvXDCk378o9xszmHg=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad h1:kX51IjbsJPCvzV9jUoVQG9GEUqIq5hjfYzXTqQ52Rh8=
+github.com/ebitengine/purego v0.1.1 h1:HI8nW+LniW9Yb34k34jBs8nz+PNzsw68o7JF8jWFHHE=
+github.com/ebitengine/purego v0.1.1/go.mod h1:Eh8I3yvknDYZeCuXH9kRNaPuHEwvXDCk378o9xszmHg=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOYguHqHjSkDACcgoPIz3w0Dis/zJ1wyHHHU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/hajimehoshi/bitmapfont/v2 v2.2.2 h1:4z08Fk1m3pjtlO7BdoP48u5bp/Y8xmKshf44aCXgYpE=
github.com/hajimehoshi/bitmapfont/v2 v2.2.2/go.mod h1:Ua/x9Dkz7M9CU4zr1VHWOqGwjKdXbOTRsH7lWfb1Co0=
-github.com/hajimehoshi/ebiten/v2 v2.4.13 h1:ZZ5y+bFkAbUeD2WGquHF+xSbg83SIbcsxCwEVeZgHWM=
-github.com/hajimehoshi/ebiten/v2 v2.4.13/go.mod h1:BZcqCU4XHmScUi+lsKexocWcf4offMFwfp8dVGIB/G4=
-github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41 h1:s01qIIRG7vN/5ndLwkDktjx44ulFk6apvAjVBYR50Yo=
+github.com/hajimehoshi/ebiten/v2 v2.4.15 h1:yvhCrDv9y7TpdHtdux5ES/IwP6Pfplz5rJVxE0Z+ZPU=
+github.com/hajimehoshi/ebiten/v2 v2.4.15/go.mod h1:BZcqCU4XHmScUi+lsKexocWcf4offMFwfp8dVGIB/G4=
github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE=
+github.com/hajimehoshi/file2byteslice v1.0.0 h1:ljd5KTennqyJ4vG9i/5jS8MD1prof97vlH5JOdtw3WU=
+github.com/hajimehoshi/file2byteslice v1.0.0/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE=
github.com/hajimehoshi/go-mp3 v0.3.3/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo=
github.com/jakecoffman/cp v1.2.1/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg=
-github.com/jezek/xgb v1.0.1 h1:YUGhxps0aR7J2Xplbs23OHnV1mWaxFVcOl9b+1RQkt8=
github.com/jezek/xgb v1.0.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
+github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk=
+github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/jfreymuth/oggvorbis v1.0.4/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
@@ -25,18 +30,20 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
-golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 h1:m9O6OTJ627iFnN2JIWfdqlZCzneRO6EEBsHXI25P8ws=
-golang.org/x/exp v0.0.0-20221230185412-738e83a70c30/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
-golang.org/x/exp/shiny v0.0.0-20230111222715-75897c7a292a h1:tGTJlXP7PfpVCoqOY74MqILeMGqJwhOy42ToU8r+c20=
-golang.org/x/exp/shiny v0.0.0-20230111222715-75897c7a292a/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
+golang.org/x/exp v0.0.0-20230116083435-1de6713980de h1:DBWn//IJw30uYCgERoxCg84hWtA97F4wMiKOIh00Uf0=
+golang.org/x/exp v0.0.0-20230116083435-1de6713980de/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/exp/shiny v0.0.0-20230116083435-1de6713980de h1:4wuvfXFMr6CwrpmIpAbi8bGbPqbymMDbHfb6z7oYGW8=
+golang.org/x/exp/shiny v0.0.0-20230116083435-1de6713980de/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk=
golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
+golang.org/x/image v0.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg=
+golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105 h1:3vUV5x5+3LfQbgk7paCM6INOaJG9xXQbn79xoNkwfIk=
golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=
+golang.org/x/mobile v0.0.0-20221110043201-43a038452099 h1:aIu0lKmfdgtn2uTj7JI2oN4TUrQvgB+wzTPO23bCKt8=
+golang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -62,8 +69,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
-golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
+golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -71,6 +78,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
+golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
diff --git a/go/ui/buffer.go b/go/ui/buffer.go
new file mode 100644
index 00000000..cc9f2377
--- /dev/null
+++ b/go/ui/buffer.go
@@ -0,0 +1,81 @@
+package ui
+
+import (
+ "image/color"
+
+ "github.com/hajimehoshi/ebiten/v2"
+ "github.com/hajimehoshi/ebiten/v2/text"
+)
+
+var (
+ BUFFER_BACKGROUND = gray
+ BUFFER_FOREGROUND = color.White
+)
+
+type Buffer struct {
+ X, Y int
+ Width, Height int
+ img *ebiten.Image
+ lines []string
+ pos int
+}
+
+func NewBuffer(x, y int, width, height int) *Buffer {
+ return &Buffer{x, y, width, height, nil, []string{}, 0}
+}
+
+func (b *Buffer) render() {
+ img := ebiten.NewImage(b.Width, b.Height)
+ img.Fill(BUFFER_BACKGROUND)
+ y := 0
+ for _, line := range b.lines[b.pos:] {
+ bounds := text.BoundString(Font, line)
+ y += bounds.Dy()
+ if y > b.Height {
+ break
+ }
+ text.Draw(img, line, Font, 0, y, BUFFER_FOREGROUND)
+ }
+ b.img = img
+}
+
+func (b *Buffer) Draw(screen *ebiten.Image) {
+ if b.img == nil {
+ b.render()
+ }
+ op := &ebiten.DrawImageOptions{}
+ op.GeoM.Translate(float64(b.X), float64(b.Y))
+ screen.DrawImage(b.img, op)
+}
+
+func (b *Buffer) FindObjectAt(x, y int) interface{} {
+ return nil
+}
+
+func (b *Buffer) ForceRedraw() {
+ b.img = nil
+}
+
+func (b *Buffer) AddLines(lines []string) {
+ b.lines = append(b.lines, lines...)
+ b.ForceRedraw()
+}
+
+func (b *Buffer) AddLine(line string) {
+ b.AddLines([]string{line})
+}
+
+func (b *Buffer) ClearLines() {
+ b.lines = []string{}
+ b.ForceRedraw()
+}
+
+func (b *Buffer) RemoveLast() {
+ b.lines = b.lines[:len(b.lines)-1]
+ b.ForceRedraw()
+}
+
+func (b *Buffer) Remove(idx int) {
+ b.lines = append(b.lines[:idx], b.lines[idx+1:]...)
+ b.ForceRedraw()
+}
diff --git a/go/ui/button.go b/go/ui/button.go
new file mode 100644
index 00000000..93180204
--- /dev/null
+++ b/go/ui/button.go
@@ -0,0 +1,53 @@
+package ui
+
+import (
+ "image/color"
+
+ "github.com/hajimehoshi/ebiten/v2"
+ "github.com/hajimehoshi/ebiten/v2/ebitenutil"
+ "github.com/hajimehoshi/ebiten/v2/text"
+)
+
+type Button interface {
+ Widget
+ Contains(x, y int) bool
+ Click(x, y int)
+}
+
+type SimpleButton struct {
+ X, Y int
+ Width, Height int
+ img *ebiten.Image
+ OnClick func(b *SimpleButton)
+}
+
+func NewSimpleButton(x, y, w, h int, onClick func(b *SimpleButton), label string) *SimpleButton {
+ img := ebiten.NewImage(w, h)
+
+ // b := text.BoundString(f, label)
+ ebitenutil.DrawRect(img, float64(0), float64(0), float64(w), float64(h), gray)
+ text.Draw(img, label, Font, 20, 25, color.White)
+
+ return &SimpleButton{x, y, w, h, img, onClick}
+}
+
+func (b *SimpleButton) Draw(screen *ebiten.Image) {
+ op := &ebiten.DrawImageOptions{}
+ op.GeoM.Translate(float64(b.X), float64(b.Y))
+ screen.DrawImage(b.img, op)
+}
+
+func (b *SimpleButton) Click(x, y int) {
+ b.OnClick(b)
+}
+
+func (b *SimpleButton) Contains(x, y int) bool {
+ return x >= b.X && x <= b.X+b.Width && y >= b.Y && y <= b.Y+b.Height
+}
+
+func (b *SimpleButton) FindObjectAt(x, y int) interface{} {
+ if b.Contains(x, y) {
+ return b
+ }
+ return nil
+}
diff --git a/go/ui/choice.go b/go/ui/choice.go
new file mode 100644
index 00000000..9e9b7b2f
--- /dev/null
+++ b/go/ui/choice.go
@@ -0,0 +1,68 @@
+package ui
+
+import (
+ "github.com/hajimehoshi/ebiten/v2"
+ "muhq.space/muhqs-game/go/game"
+)
+
+type Choice struct {
+ X, Y int
+ Width, choiceHeight int
+ choices []string
+ onClick func(*Choice, int, int)
+ buttons []Button
+}
+
+func choiceButtonOnClickDummy(b *SimpleButton) {}
+
+func NewChoice(x, y int, width, choiceHeight int, choices []string,
+ onClick func(*Choice, int, int)) *Choice {
+
+ c := &Choice{x, y, width, choiceHeight, choices, onClick, []Button{}}
+
+ for i, choice := range c.choices {
+ b := NewSimpleButton(c.X, c.Y+i*c.choiceHeight, c.Width, c.choiceHeight,
+ choiceButtonOnClickDummy, choice)
+
+ c.buttons = append(c.buttons, b)
+ }
+ return c
+}
+
+func (c *Choice) Draw(screen *ebiten.Image) {
+ for _, b := range c.buttons {
+ b.Draw(screen)
+ }
+}
+
+func (c *Choice) GetChoosen(x, y int) int {
+ return (y - c.Y) / c.choiceHeight
+}
+
+func (c *Choice) Click(x, y int) {
+ c.onClick(c, x, y)
+}
+
+func (c *Choice) Contains(x, y int) bool {
+ height := len(c.choices) * c.choiceHeight
+ return x >= c.X && x <= c.X+c.Width && y >= c.Y && y <= c.Y+height
+}
+
+func (c *Choice) FindObjectAt(x, y int) interface{} {
+ if c.Contains(x, y) {
+ return c
+ }
+ return nil
+}
+
+func NewActionChoice(x, y int, actions []game.ActionPrototype, onClick func(*Choice, int, int)) *Choice {
+ // TODO: use dynamic with width and height
+ width := 200
+ choiceHeight := 40
+ labels := make([]string, 0, len(actions))
+ for _, action := range actions {
+ labels = append(labels, action.String())
+ }
+
+ return NewChoice(x, y, width, choiceHeight, labels, onClick)
+}
diff --git a/go/ui/colors.go b/go/ui/colors.go
new file mode 100644
index 00000000..a45aa16b
--- /dev/null
+++ b/go/ui/colors.go
@@ -0,0 +1,7 @@
+package ui
+
+import (
+ "image/color"
+)
+
+var gray = color.RGBA{0x80, 0x80, 0x80, 0xff}
diff --git a/go/ui/font.go b/go/ui/font.go
new file mode 100644
index 00000000..065ac0d8
--- /dev/null
+++ b/go/ui/font.go
@@ -0,0 +1,24 @@
+package ui
+
+import (
+ "log"
+
+ "golang.org/x/image/font"
+ "golang.org/x/image/font/gofont/goregular"
+ "golang.org/x/image/font/opentype"
+)
+
+var Font font.Face
+
+func InitFont() {
+ tt, err := opentype.Parse(goregular.TTF)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ opts := &opentype.FaceOptions{Size: 24, DPI: 72}
+ Font, err = opentype.NewFace(tt, opts)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/go/ui/mapView.go b/go/ui/mapView.go
index 10929f68..c292d526 100644
--- a/go/ui/mapView.go
+++ b/go/ui/mapView.go
@@ -13,7 +13,7 @@ import (
)
const (
- PERMANENT_WIDTH int = 40
+ PERMANENT_WIDTH int = 40
PERMANENT_HEIGHT int = 40
TILE_WIDTH int = 50
@@ -21,15 +21,15 @@ const (
)
type MapView struct {
- gameState *game.State
- mapLayer *ebiten.Image
- permanentsLayer *ebiten.Image
- tileHighlights []game.Position
- permanentsHighlights []*game.Permanent
+ gameState *game.State
+ mapLayer *ebiten.Image
+ permanentsLayer *ebiten.Image
+ tileHighlights []game.Position
+ permanentsHighlights []game.Permanent
}
func NewMapView(g *game.State) *MapView {
- vw := MapView{g, nil, nil, []game.Position{}, []*game.Permanent{}}
+ vw := MapView{g, nil, nil, []game.Position{}, []game.Permanent{}}
return &vw
}
@@ -131,16 +131,27 @@ func (vw *MapView) drawPermanentsLayer(screen *ebiten.Image) {
vw.permanentsLayer = vw.newLayerImage()
for _, p := range vw.gameState.Permanents {
- permanentSymbol := assets.GetSymbol(strings.ToLower(p.Card.Name))
+ permanentSymbol := assets.GetSymbol(strings.ToLower(p.GetCard().Name))
// TODO: Implement generic symbols
if permanentSymbol == nil {
continue
}
+ t := p.GetTile()
+ x_px := t.Position.X*TILE_WIDTH + (TILE_WIDTH-PERMANENT_WIDTH)/2
+ y_px := t.Position.Y*TILE_HEIGHT + (TILE_HEIGHT-PERMANENT_HEIGHT)/2
+
op := &ebiten.DrawImageOptions{}
- x_px := p.Tile.Position.X * TILE_WIDTH + (TILE_WIDTH - PERMANENT_WIDTH) / 2
- y_px := p.Tile.Position.Y * TILE_HEIGHT + (TILE_HEIGHT - PERMANENT_HEIGHT) / 2
op.GeoM.Translate(float64(x_px), float64(y_px))
+ op.ColorM.ScaleWithColor(p.GetController().Color)
+
+ for _, h := range vw.permanentsHighlights {
+ if p == h {
+ op.ColorM.Scale(255, 0.5, 0.5, 1)
+ break
+ }
+ }
+
vw.permanentsLayer.DrawImage(permanentSymbol, op)
}
}
@@ -163,8 +174,8 @@ func (vw *MapView) FindObjectAt(x_px, y_px int) interface{} {
y := y_px / TILE_HEIGHT
// TODO: detect if a permanent or the containing tile was selected
- for _, u := range vw.gameState.Units {
- pos := u.Tile.Position
+ for _, u := range vw.gameState.Permanents {
+ pos := u.GetTile().Position
if pos.X == x && pos.Y == y {
return u
}
@@ -198,15 +209,15 @@ func (vw *MapView) ClearTileHighlights() {
vw.HighlightTiles([]*game.Tile{})
}
-func (vw *MapView) HighlightPermanents(permanents []*game.Permanent) {
+func (vw *MapView) HighlightPermanents(permanents []game.Permanent) {
vw.ForceRedraw()
vw.permanentsHighlights = permanents
}
-func (vw *MapView) HighlightPermanent(p *game.Permanent) {
- vw.HighlightPermanents([]*game.Permanent{p})
+func (vw *MapView) HighlightPermanent(p game.Permanent) {
+ vw.HighlightPermanents([]game.Permanent{p})
}
func (vw *MapView) ClearPermanentsHighlights() {
- vw.HighlightPermanents([]*game.Permanent{})
+ vw.HighlightPermanents([]game.Permanent{})
}
diff --git a/go/ui/textBox.go b/go/ui/textBox.go
new file mode 100644
index 00000000..e07440b5
--- /dev/null
+++ b/go/ui/textBox.go
@@ -0,0 +1,67 @@
+package ui
+
+import (
+ "fmt"
+ "image/color"
+
+ "github.com/hajimehoshi/ebiten/v2"
+ "github.com/hajimehoshi/ebiten/v2/text"
+ "muhq.space/muhqs-game/go/game"
+)
+
+var (
+ TEXTBOX_BACKGROUND = gray
+ TEXTBOX_FOREGROUND = color.White
+)
+
+type TextBox struct {
+ X, Y int
+ Width, Height int
+ img *ebiten.Image
+ text string
+}
+
+func NewFixedTextBox(x, y int, width, height int, t string) *TextBox {
+ tb := &TextBox{x, y, width, height, nil, t}
+ tb.render()
+ return tb
+}
+
+func NewAutoTextBox(x, y int, t string) *TextBox {
+ tb := &TextBox{x, y, -1, -1, nil, t}
+ tb.render()
+ return tb
+}
+
+func NewUnitInfo(x, y int, u *game.Unit) *TextBox {
+ info := fmt.Sprintf("%s\nDamage: %d\nHealth: %d\nMovement: %d\nAttack %s\nUpkeep: %d",
+ u.String(), u.GetDamage(), u.Health, u.Movement, u.Attack.String(), u.Upkeep)
+ return NewAutoTextBox(x, y, info)
+}
+
+func NewPermInfo(x, y int, p game.Permanent) *TextBox {
+ info := fmt.Sprintf("%s\nDamage: %d",
+ p.String(), p.GetDamage())
+ return NewAutoTextBox(x, y, info)
+}
+
+func (tb *TextBox) render() {
+ b := text.BoundString(Font, tb.text)
+ if tb.Width == -1 || tb.Height == -1 {
+ tb.Width = b.Dx()
+ tb.Height = b.Dy()
+ }
+ tb.img = ebiten.NewImage(tb.Width, tb.Height)
+ tb.img.Fill(TEXTBOX_BACKGROUND)
+ text.Draw(tb.img, tb.text, Font, b.Min.X, -b.Min.Y, TEXTBOX_FOREGROUND)
+}
+
+func (tb *TextBox) Draw(screen *ebiten.Image) {
+ op := &ebiten.DrawImageOptions{}
+ op.GeoM.Translate(float64(tb.X), float64(tb.Y))
+ screen.DrawImage(tb.img, op)
+}
+
+func (tb TextBox) FindObjectAt(x, y int) interface{} {
+ return nil
+}
diff --git a/go/ui/widget.go b/go/ui/widget.go
new file mode 100644
index 00000000..c8fb0816
--- /dev/null
+++ b/go/ui/widget.go
@@ -0,0 +1,10 @@
+package ui
+
+import (
+ "github.com/hajimehoshi/ebiten/v2"
+)
+
+type Widget interface {
+ Draw(screen *ebiten.Image)
+ FindObjectAt(x, y int) interface{}
+}