diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2023-01-20 03:18:06 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-01-27 16:43:44 +0100 |
| commit | c4cb9637f5c85d03aa162968f0dfec3c193fc9dc (patch) | |
| tree | 3ad32e5dfd7c4ae72f66b86e77edef7b700a8b7a | |
| parent | 9df5cc7b55d8ac9ecd775a14a14f80a6c36c4d74 (diff) | |
| download | muhqs-game-c4cb9637f5c85d03aa162968f0dfec3c193fc9dc.tar.gz muhqs-game-c4cb9637f5c85d03aa162968f0dfec3c193fc9dc.zip | |
intermediate commit
Implement actions and multiple ui widgets
| -rw-r--r-- | go/Makefile | 14 | ||||
| -rw-r--r-- | go/client/main.go | 236 | ||||
| -rw-r--r-- | go/game/action.go | 133 | ||||
| -rw-r--r-- | go/game/ai.go | 123 | ||||
| -rw-r--r-- | go/game/artifact.go | 48 | ||||
| -rw-r--r-- | go/game/card.go | 21 | ||||
| -rw-r--r-- | go/game/equipment.go | 5 | ||||
| -rw-r--r-- | go/game/hand.go | 5 | ||||
| -rw-r--r-- | go/game/permanent.go | 60 | ||||
| -rw-r--r-- | go/game/player.go | 9 | ||||
| -rw-r--r-- | go/game/pos.go | 12 | ||||
| -rw-r--r-- | go/game/range.go | 8 | ||||
| -rw-r--r-- | go/game/stack.go | 29 | ||||
| -rw-r--r-- | go/game/state.go | 77 | ||||
| -rw-r--r-- | go/game/tile.go | 15 | ||||
| -rw-r--r-- | go/game/unit.go | 100 | ||||
| -rw-r--r-- | go/go.mod | 21 | ||||
| -rw-r--r-- | go/go.sum | 37 | ||||
| -rw-r--r-- | go/ui/buffer.go | 81 | ||||
| -rw-r--r-- | go/ui/button.go | 53 | ||||
| -rw-r--r-- | go/ui/choice.go | 68 | ||||
| -rw-r--r-- | go/ui/colors.go | 7 | ||||
| -rw-r--r-- | go/ui/font.go | 24 | ||||
| -rw-r--r-- | go/ui/mapView.go | 43 | ||||
| -rw-r--r-- | go/ui/textBox.go | 67 | ||||
| -rw-r--r-- | go/ui/widget.go | 10 |
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 +} @@ -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 ) @@ -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{} +} |
