diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2023-01-12 20:58:14 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-01-27 16:43:44 +0100 |
| commit | 9df5cc7b55d8ac9ecd775a14a14f80a6c36c4d74 (patch) | |
| tree | b0ed4b509da71d0d6320f979a1eebbd91fdf7321 | |
| parent | 41d16fb4910fc0402b6b081302c953f7dabc8cf9 (diff) | |
| download | muhqs-game-9df5cc7b55d8ac9ecd775a14a14f80a6c36c4d74.tar.gz muhqs-game-9df5cc7b55d8ac9ecd775a14a14f80a6c36c4d74.zip | |
implement unit movement
| -rw-r--r-- | assets/symbols/archer.png | bin | 45278 -> 18667 bytes | |||
| -rw-r--r-- | go/assets/retrieve.go | 3 | ||||
| -rw-r--r-- | go/client/main.go | 126 | ||||
| -rw-r--r-- | go/game/card.go | 151 | ||||
| -rw-r--r-- | go/game/deck.go | 68 | ||||
| -rw-r--r-- | go/game/discardPiles.go | 7 | ||||
| -rw-r--r-- | go/game/hand.go | 29 | ||||
| -rw-r--r-- | go/game/map.go | 8 | ||||
| -rw-r--r-- | go/game/permanent.go | 10 | ||||
| -rw-r--r-- | go/game/player.go | 23 | ||||
| -rw-r--r-- | go/game/range.go | 39 | ||||
| -rw-r--r-- | go/game/range_test.go | 35 | ||||
| -rw-r--r-- | go/game/set.go | 63 | ||||
| -rw-r--r-- | go/game/state.go | 36 | ||||
| -rw-r--r-- | go/game/tile.go | 4 | ||||
| -rw-r--r-- | go/game/unit.go | 89 | ||||
| -rw-r--r-- | go/go.mod | 11 | ||||
| -rw-r--r-- | go/go.sum | 9 | ||||
| -rw-r--r-- | go/ui/map.go | 109 | ||||
| -rw-r--r-- | go/ui/mapView.go | 212 |
20 files changed, 869 insertions, 163 deletions
diff --git a/assets/symbols/archer.png b/assets/symbols/archer.png Binary files differindex e83f2d17..f641f0a6 100644 --- a/assets/symbols/archer.png +++ b/assets/symbols/archer.png diff --git a/go/assets/retrieve.go b/go/assets/retrieve.go index fea3d141..492a983d 100644 --- a/go/assets/retrieve.go +++ b/go/assets/retrieve.go @@ -22,7 +22,8 @@ func retrieveAsset(path string) *ebiten.Image { log.Printf("loading %s\n", path) resp, err := http.Get(PROTOCOL + path) if err != nil { - log.Fatal(err) + log.Println(err) + return nil } defer resp.Body.Close() diff --git a/go/client/main.go b/go/client/main.go index b91b15f7..b0696fdf 100644 --- a/go/client/main.go +++ b/go/client/main.go @@ -6,37 +6,122 @@ import ( "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/hajimehoshi/ebiten/v2/inpututil" "muhq.space/muhqs-game/go/assets" "muhq.space/muhqs-game/go/game" "muhq.space/muhqs-game/go/ui" ) -type Game struct{} - const ( - screenWidth = 1920 - screenHeight = 1080 -) - -var ( - m *ui.MapLayer - players []game.Player + screenWidth = 1280 + screenHeight = 1024 ) -func (g *Game) Update() error { - return nil +type HandLayer struct { } -func (g *Game) Draw(screen *ebiten.Image) { +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) - m.Draw(screen) - ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f FPS: %0.2f", ebiten.ActualTPS(), ebiten.ActualFPS())) +} + +type Game struct { + gameState *game.State + handLayer *HandLayer + mapView *ui.MapView + activeObject interface{} +} + +func NewGame() *Game { + g := &Game{} + g.gameState = game.NewState() + return g +} + +func NewGameFromMap(mapName string) *Game { + g := NewGame() + g.loadMap(mapName) + return g +} + +func (g *Game) loadMap(mapName string) { + m, err := game.GetMap(mapName) + if err != nil { + log.Fatal(err) + } + g.gameState.Map = m + g.mapView = ui.NewMapView(g.gameState) +} + +func (g *Game) addPlayer(name string, deck *game.Deck) { + g.gameState.AddNewPlayer(name, deck) +} + +func (g *Game) getPlayer(name string) *game.Player { + for _, p := range g.gameState.Players { + if p.Name == name { + return p + } + } + return nil +} + +func (g *Game) moveUnit(unit *game.Unit, tile *game.Tile) { + g.gameState.MoveUnit(unit, tile) + g.mapView.ClearTileHighlights() + g.mapView.ForceRedraw() +} + +func (g *Game) findObjectAt(x, y int) interface{} { + if obj := g.mapView.FindObjectAt(x, y); obj != nil { + return obj + } + + return nil +} + +func (g *Game) Update() error { + 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 { + 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 { + fmt.Printf("No object found at cursor position (%d, %d)\n", x, y) + } + } + return nil +} + +func (g *Game) Draw(screen *ebiten.Image) { + g.handLayer.Draw(screen) + g.mapView.Draw(screen) + ebitenutil.DebugPrint(screen, + fmt.Sprintf("TPS: %0.2f FPS: %0.2f", ebiten.ActualTPS(), ebiten.ActualFPS())) } func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { @@ -46,13 +131,14 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { func main() { ebiten.SetWindowSize(screenWidth, screenHeight) ebiten.SetWindowTitle("Muhq's Game") - _m, err := game.GetMap("2P-ring-street") - if err != nil { - log.Fatal(err) - } - m = ui.NewMapLayerFromMap(_m) - if err := ebiten.RunGame(&Game{}); err != nil { + g := NewGameFromMap("2P-ring-street") + + g.addPlayer("muhq", game.NewDeckFromDeckList(g.gameState.Map.StartDeckList)) + muhq := g.getPlayer("muhq") + g.gameState.AddNewUnit("base/archer", game.Position{0, 0}, muhq) + + if err := ebiten.RunGame(g); err != nil { log.Fatal(err) } } diff --git a/go/game/card.go b/go/game/card.go index 6cd20257..f77a274a 100644 --- a/go/game/card.go +++ b/go/game/card.go @@ -1,18 +1,151 @@ package game +import ( + "io" + "log" + "net/http" + "path" + + "gopkg.in/yaml.v3" +) + type CardType int const ( - Unit CardType = iota + 1 - Spell - Artifact - Equipment - Potion - Itention + UndefinedCardType CardType = iota + UnitType + SpellType + ArtifactType + EquipmentType + PotionType + ItentionType +) + +func (ct CardType) String() string { + switch ct { + case UndefinedCardType: + return "undefined" + case UnitType: + return "unit" + case SpellType: + return "spell" + case ArtifactType: + return "artifact" + case EquipmentType: + return "equipment" + case PotionType: + return "potion" + case ItentionType: + return "itention" + } + log.Panicf("Switch not exhausting %d", ct) + return "" +} + +var CardTypes = struct { + Unit CardType + Spell CardType + Artifact CardType + Equipment CardType + Potion CardType + Itention CardType +}{ + Unit: UnitType, + Spell: SpellType, + Artifact: ArtifactType, + Equipment: EquipmentType, + Potion: PotionType, + Itention: ItentionType, +} + +var CardTypeNames = map[string]CardType{ + "unit": UnitType, + "spell": SpellType, + "artifact": ArtifactType, + "equipment": EquipmentType, + "potion": PotionType, + "itention": ItentionType, +} + +const ( + CARD_DATA_URL string = "data/cards" ) type Card struct { - Name string - Set SetIdentifier - Type CardType + Name string + Set SetIdentifier + Type CardType + Values map[string]interface{} +} + +var cards = map[string]map[string][]byte{} + +func retrieveCardDefinition(cardPath string) []byte { + url := PROTOCOL + path.Join(BASE_URL, CARD_DATA_URL, cardPath+".yml") + log.Printf("loading %s\n", url) + resp, err := http.Get(url) + if err != nil { + log.Fatal(err) + } + + defer resp.Body.Close() + data, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + return data +} + +func (card *Card) parseDefinition(definition []byte) { + data := make(map[interface{}]interface{}) + err := yaml.Unmarshal(definition, &data) + if err != nil { + log.Fatalf("parsing %s failed with: %v\n", definition, err) + } + + name, ok := data["name"] + if !ok { + log.Fatalf("no name in card yaml %s\n", definition) + } + delete(data, "name") + + switch name.(type) { + case string: + card.Name = name.(string) + case map[string]interface{}: + cardName := name.(map[string]interface{})["en"] + card.Name = cardName.(string) + default: + log.Fatalf("Unexpected type %T of yml card field 'Name'\n", name) + } + + t, ok := data["type"] + if !ok { + log.Fatalf("no type in card yaml %s\n", definition) + } + delete(data, "type") + + card.Type = CardTypeNames[t.(string)] + + for k, v := range data { + card.Values[k.(string)] = v + } +} + +func NewCard(cardPath string) *Card { + cardName := path.Base(cardPath) + set := path.Dir(cardPath) + + cardDefinition, found := cards[set][cardName] + if !found { + cardDefinition = retrieveCardDefinition(cardPath) + if cards[set] == nil { + cards[set] = map[string][]byte{} + } + cards[set][cardName] = cardDefinition + } + + c := Card{"", SetIdentifierNames[set], UndefinedCardType, map[string]interface{}{}} + c.parseDefinition(cardDefinition) + return &c } diff --git a/go/game/deck.go b/go/game/deck.go index 33126ae8..f4290322 100644 --- a/go/game/deck.go +++ b/go/game/deck.go @@ -1,5 +1,71 @@ package game +import ( + "log" + "math/rand" + "strconv" + "strings" + "time" +) + type Deck struct { - Cards []Card + Cards []*Card +} + +func NewDeck() *Deck { + d := Deck{[]*Card{}} + return &d +} + +func (d *Deck) AddCards(cards []*Card) { + d.Cards = append(d.Cards, cards...) +} + +func (d *Deck) AddCard(card *Card) { + d.Cards = append(d.Cards, card) +} + +func (d *Deck) Size() int { + return len(d.Cards) +} + +func (d *Deck) Shuffle() { + rand.Seed(time.Now().UnixNano()) + rand.Shuffle(d.Size(), func(i, j int) { + d.Cards[i], d.Cards[j] = d.Cards[j], d.Cards[i] + }) +} + +func (d *Deck) Draw(amount int) []*Card { + drawn := d.Cards[-amount:] + d.Cards = d.Cards[:amount] + return drawn +} + +func NewDeckFromCardPaths(cardPaths []string) *Deck { + d := NewDeck() + for _, cardPath := range cardPaths { + card := NewCard(cardPath) + d.AddCard(card) + } + return d +} + +func NewDeckFromDeckList(deckList string) *Deck { + cardPaths := []string{} + lines := strings.Split(deckList, "\n") + for _, line := range lines { + tokens := strings.SplitN(line, " ", 2) + cardPath := tokens[1] + amount, err := strconv.Atoi(tokens[0]) + if err != nil { + log.Fatal(err) + } + + for i := 0; i < amount; i++ { + cardPaths = append(cardPaths, cardPath) + } + } + + return NewDeckFromCardPaths(cardPaths) } diff --git a/go/game/discardPiles.go b/go/game/discardPiles.go index 9c1c1e45..f7cf8994 100644 --- a/go/game/discardPiles.go +++ b/go/game/discardPiles.go @@ -1,5 +1,10 @@ package game type DiscardPile struct { - Cards []Card + Cards []*Card +} + +func NewDiscardPile() *DiscardPile { + dp := DiscardPile{[]*Card{}} + return &dp } diff --git a/go/game/hand.go b/go/game/hand.go index 42187f5a..fde4a686 100644 --- a/go/game/hand.go +++ b/go/game/hand.go @@ -1,5 +1,32 @@ package game type Hand struct { - Cards []Card + Cards []*Card +} + +func NewHand() *Hand { + h := Hand{[]*Card{}} + return &h +} + +func (h *Hand) Size() int { + return len(h.Cards) +} + +func (h *Hand) AddCards(cards []*Card) { + h.Cards = append(h.Cards, cards...) +} + +func (h *Hand) AddCard(card *Card) { + h.Cards = append(h.Cards, card) +} + +func (h *Hand) RemoveCard(card *Card) { + for i, c := range h.Cards { + if c != card { + continue + } + + h.Cards = append(h.Cards[:i], h.Cards[i+1:]...) + } } diff --git a/go/game/map.go b/go/game/map.go index cecce9ca..ebf59d25 100644 --- a/go/game/map.go +++ b/go/game/map.go @@ -1,8 +1,8 @@ package game import ( - "fmt" "errors" + "fmt" "io" "log" "math/rand" @@ -20,7 +20,8 @@ const ( BASE_URL string = "localhost:8000/" MAP_URL_PART string = "html/build/maps" - DEFAULT_RESSOURCE_GAIN int = 5 + DEFAULT_RESSOURCE_GAIN int = 5 + DEFAULT_START_DECK string = "3 misc/farmer" ) type MapYml struct { @@ -32,6 +33,7 @@ type Map struct { Tiles [][]Tile symbols map[string]string RessourceGain int + StartDeckList string } func readMap(r io.Reader) (*Map, error) { @@ -52,7 +54,7 @@ func ParseMap(data []byte) (*Map, error) { mapYml.Symbols[" "] = "neutral" - m := &Map{[][]Tile{}, mapYml.Symbols, DEFAULT_RESSOURCE_GAIN} + m := &Map{[][]Tile{}, mapYml.Symbols, DEFAULT_RESSOURCE_GAIN, DEFAULT_START_DECK} rows := strings.Split(mapYml.Map, "\n") for y := 0; y < len(rows); y++ { diff --git a/go/game/permanent.go b/go/game/permanent.go index a4418e3e..37111c4d 100644 --- a/go/game/permanent.go +++ b/go/game/permanent.go @@ -1,8 +1,10 @@ package game type Permanent struct { - Card *Card - Tile *Tile - Damage int - Pile []*Permanent + Card *Card + Tile *Tile + Damage int + Pile []*Permanent + Controller *Player + Owner *Player } diff --git a/go/game/player.go b/go/game/player.go index 6b9041ad..20ac13a5 100644 --- a/go/game/player.go +++ b/go/game/player.go @@ -1,5 +1,9 @@ package game +const ( + MAX_DRAW int = 3 +) + type Player struct { Id int Name string @@ -7,4 +11,23 @@ type Player struct { DiscardPile *DiscardPile Deck *Deck Ressource int + gameState *State +} + +func NewPlayer(id int, name string, deck *Deck, gameState *State) *Player { + p := Player{id, name, NewHand(), NewDiscardPile(), deck, 0, gameState} + return &p +} + +func (p *Player) Draw() { + toDraw := MAX_DRAW - p.Hand.Size() + if toDraw < 1 { + return + } + + drawnCards := p.Deck.Draw(toDraw) + p.Hand.AddCards(drawnCards) +} + +func (p *Player) PlayPermanent(card *Card) { } diff --git a/go/game/range.go b/go/game/range.go new file mode 100644 index 00000000..86ec2f50 --- /dev/null +++ b/go/game/range.go @@ -0,0 +1,39 @@ +package game + +import ( + "math" +) + +func IsPositionInRange(origin Position, pos Position, r int) bool { + x, y := float64(pos.X-origin.X), float64(pos.Y-origin.Y) + ax, ay := math.Abs(x), math.Abs(y) + var abs float64 + if pos.X > pos.Y { + abs = ax + math.Floor(ay/2) + } else { + abs = ay + math.Floor(ax/2) + } + + return abs <= float64(r) +} + +func PositionsInRange(origin Position, r int, includeOrigin bool) []Position { + positions := []Position{} + for x := -r; x <= r; x++ { + for y := -r; y <= r; y++ { + pos := Position{origin.X + x, origin.Y + y} + if pos.X < 0 || pos.Y < 0 { + continue + } + + if !includeOrigin && pos == origin { + continue + } + + if IsPositionInRange(origin, pos, r) { + positions = append(positions, pos) + } + } + } + return positions +} diff --git a/go/game/range_test.go b/go/game/range_test.go new file mode 100644 index 00000000..afa164de --- /dev/null +++ b/go/game/range_test.go @@ -0,0 +1,35 @@ +package game + +import ( + "golang.org/x/exp/slices" + "testing" +) + +func TestPositionsInRange(t *testing.T) { + origin := Position{0, 0} + positionsInRange1 := []Position{Position{0, 1}, Position{1, 0}, Position{1, 1}} + calculatedPositions := PositionsInRange(origin, 1, false) + if len(positionsInRange1) != len(calculatedPositions) { + t.Fatalf("Unexpected number of calculated positions") + } + + for _, p := range positionsInRange1 { + if !slices.Contains(calculatedPositions, p) { + t.Fatalf("Position %v not in calculated positions", p) + } + } + + origin = Position{2, 2} + positionsInRange2 := []Position{Position{0, 1}, Position{0, 2}, Position{0, 3}, + Position{1, 0}, Position{1, 1}, Position{1, 2}, Position{1, 3}, Position{1, 4}, + Position{2, 0}, Position{2, 1}, Position{2, 3}, Position{2, 4}, + Position{3, 0}, Position{3, 1}, Position{3, 2}, Position{3, 3}, Position{3, 4}, + Position{4, 1}, Position{4, 2}, Position{4, 3}} + calculatedPositions = PositionsInRange(origin, 2, false) + + for _, p := range positionsInRange2 { + if !slices.Contains(calculatedPositions, p) { + t.Fatalf("Position %v not in calculated positions", p) + } + } +} diff --git a/go/game/set.go b/go/game/set.go index 4136eac6..b0565eca 100644 --- a/go/game/set.go +++ b/go/game/set.go @@ -1,5 +1,9 @@ package game +import ( + "log" +) + type SetIdentifier int const ( @@ -13,3 +17,62 @@ const ( Tyrant Exp1 ) + +func (si SetIdentifier) String() string { + switch si { + case Base: + return "base" + case Magic: + return "magic" + case Equipments: + return "equipments" + case Misc: + return "misc" + case Nautics: + return "nautics" + case Potions: + return "potions" + case Kraken: + return "kraken" + case Tyrant: + return "tyrant" + case Exp1: + return "exp1" + } + log.Panicf("Switch not exhausting %d", si) + return "" +} + +var SetIdentifiers = struct { + Base SetIdentifier + Magic SetIdentifier + Equipments SetIdentifier + Misc SetIdentifier + Nautics SetIdentifier + Potions SetIdentifier + Kraken SetIdentifier + Tyrant SetIdentifier + Exp1 SetIdentifier +}{ + Base: Base, + Magic: Magic, + Equipments: Equipments, + Misc: Misc, + Nautics: Nautics, + Potions: Potions, + Kraken: Kraken, + Tyrant: Tyrant, + Exp1: Exp1, +} + +var SetIdentifierNames = map[string]SetIdentifier{ + "base": Base, + "magic": Magic, + "equipments": Equipments, + "misc": Misc, + "nautics": Nautics, + "potions": Potions, + "kraken": Kraken, + "tyrant": Tyrant, + "exp1": Exp1, +} diff --git a/go/game/state.go b/go/game/state.go index 9dcf73c4..caae8106 100644 --- a/go/game/state.go +++ b/go/game/state.go @@ -1,12 +1,32 @@ package game type State struct { - Decks []Deck - Stores []Store - Map Map - Stack Stack - Hands []Hand - DiscardPiles []DiscardPile - Players []Player - Permanents []Permanent + Decks []*Deck + Stores []*Store + Map *Map + Stack *Stack + Hands []*Hand + DiscardPiles []*DiscardPile + Players []*Player + Permanents []*Permanent + Units []*Unit +} + +func NewState() *State { + return &State{} +} + +func (s *State) AddNewPlayer(name string, deck *Deck) { + p := NewPlayer(len(s.Players), name, deck, s) + 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.Units = append(s.Units, unit) +} + +func (s *State) MoveUnit(u *Unit, t *Tile) { + u.move(s, t) } diff --git a/go/game/tile.go b/go/game/tile.go index fb62420b..50688d83 100644 --- a/go/game/tile.go +++ b/go/game/tile.go @@ -85,3 +85,7 @@ func NewTileFromString(tile string, pos Position) (Tile, error) { } return t, nil } + +func (t *Tile) IsFree() bool { + return t.Permanent == nil +} diff --git a/go/game/unit.go b/go/game/unit.go new file mode 100644 index 00000000..4c07d625 --- /dev/null +++ b/go/game/unit.go @@ -0,0 +1,89 @@ +package game + +import ( + "golang.org/x/exp/slices" + "log" + "strconv" + "strings" +) + +type UnitStates int + +const ( + Paralysis UnitStates = iota + 1 + Poison + Panic + Rage + Ward +) + +type Attack struct { + Damage int + Range int +} + +func parseAttack(attack string) Attack { + tokens := strings.Split(attack, " ") + a, err := strconv.Atoi(tokens[0]) + if err != nil { + log.Fatalf("Attack %s has no valid Damage value\n", attack) + } + + r := 1 + if len(tokens) == 3 { + r, err = strconv.Atoi(tokens[2]) + if err != nil { + log.Fatalf("Attack %s has no valid Range value\n", attack) + } + } + return Attack{a, r} +} + +type Unit struct { + Permanent + Health int + Movement int + Attack Attack + Upkeep int + // FullActions []FullActions + // FreeActions []FreeActions + States []UnitStates + // Effects []Effects +} + +func NewUnit(cardPath string, tile *Tile, owner *Player) *Unit { + card := NewCard(cardPath) + + return &Unit{ + Permanent{card, tile, 0, []*Permanent{}, owner, owner}, + card.Values["health"].(int), + card.Values["movement"].(int), + parseAttack(card.Values["attack"].(string)), + card.Values["upkeep"].(int), + []UnitStates{}, + } +} + +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) + } + + u.Tile.Permanent = nil + u.Tile = tile +} + +func (u *Unit) MoveRangeTiles(m *Map) []*Tile { + tiles := []*Tile{} + for _, pos := range PositionsInRange(u.Tile.Position, u.Movement, false) { + tile := m.TileAt(pos) + if u.IsAvailableTile(tile) { + tiles = append(tiles, tile) + } + } + return tiles +} + +func (u *Unit) IsAvailableTile(tile *Tile) bool { + return tile.IsFree() +} @@ -2,16 +2,19 @@ module muhq.space/muhqs-game/go go 1.19 -require github.com/hajimehoshi/ebiten/v2 v2.4.13 +require ( + github.com/hajimehoshi/ebiten/v2 v2.4.13 + golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 + 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 v0.0.0-20190731235908-ec7cb31e5a56 // 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.0.0-20220818161305-2296e01440c6 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + golang.org/x/sys v0.1.0 // indirect ) @@ -24,8 +24,11 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= 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/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= @@ -58,8 +61,9 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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 h1:Sx/u41w+OwrInGdEckYmEuU5gHoGSL4QbDz3S9s6j4U= 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/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= @@ -75,6 +79,7 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/ui/map.go b/go/ui/map.go deleted file mode 100644 index 2e83ad2d..00000000 --- a/go/ui/map.go +++ /dev/null @@ -1,109 +0,0 @@ -package ui - -import ( - "log" - "math" - - "github.com/hajimehoshi/ebiten/v2" - "muhq.space/muhqs-game/go/assets" - "muhq.space/muhqs-game/go/game" -) - -const ( - TILE_WIDTH int = 50 - TILE_HEIGHT int = 50 -) - -type MapLayer struct { - m *game.Map - img *ebiten.Image -} - -func NewMapLayerFromMap(m *game.Map) *MapLayer { - mv := MapLayer{m, nil} - return &mv -} - -func rotateStreetImg(x, y int, radian float64, op *ebiten.DrawImageOptions) { - // Move the image's center to the screen's upper-left corner. - // This is a preparation for rotating. When geometry matrices are applied, - // the origin point is the upper-left corner. - op.GeoM.Translate(-float64(TILE_WIDTH)/2, -float64(TILE_HEIGHT)/2) - - // Rotate the image. As a result, the anchor point of this rotate is - // the center of the image. - op.GeoM.Rotate(radian) - - // Reset translation - op.GeoM.Translate(float64(TILE_WIDTH)/2, float64(TILE_HEIGHT)/2) -} - -func (m *MapLayer) rotateStreet(x, y int, op *ebiten.DrawImageOptions) { - connections, left, right, above, below := m.m.FindStreetConnections(x, y) - - if connections == 0 { - // This street is not connected to another street -> - // check any other non neutral tiles - connections, left, right, above, below = m.m.FindAnyConnections(x, y) - } - - // This street is not connected to anything. Seams odd! - if connections == 0 { - log.Printf("Street at (%d, %d) is not connected", x, y) - } else if (connections == 1 || - (connections == 2 && ((left && right) || (above && below)))) && - (above || below) { - rotateStreetImg(x, y, math.Pi/2, op) - } else if connections == 2 { - // normal orientation above and right - if right && below { - rotateStreetImg(x, y, 3*math.Pi/2, op) - } else if left && below { - rotateStreetImg(x, y, math.Pi, op) - } else if above && left { - rotateStreetImg(x, y, math.Pi/2, op) - } - } else if connections == 3 { - // normal orientation left above right - if above && right && below { - rotateStreetImg(x, y, 3*math.Pi/2, op) - } else if left && below && right { - rotateStreetImg(x, y, math.Pi, op) - } else if below && left && above { - rotateStreetImg(x, y, math.Pi/2, op) - } - } -} - -func (m *MapLayer) Draw(screen *ebiten.Image) { - if m.img == nil { - // TODO: support non symetric maps - maxWidth := len(m.m.Tiles[0]) * TILE_WIDTH - maxHeight := len(m.m.Tiles) * TILE_HEIGHT - m.img = ebiten.NewImage(maxWidth, maxHeight) - - x_px, y_px := 0.0, 0.0 - for y := 0; y < len(m.m.Tiles); y++ { - for x := 0; x < len(m.m.Tiles[y]); x++ { - tile := &m.m.Tiles[y][x] - tileImg := assets.GetTile(tile.Raw) - - op := &ebiten.DrawImageOptions{} - // Rotate Street - if tile.Type == game.TileTypes.Street { - m.rotateStreet(x, y, op) - } - - op.GeoM.Translate(x_px, y_px) - - m.img.DrawImage(tileImg, op) - - x_px += float64(TILE_WIDTH) - } - x_px = 0 - y_px += float64(TILE_HEIGHT) - } - } - - screen.DrawImage(m.img, &ebiten.DrawImageOptions{}) -} diff --git a/go/ui/mapView.go b/go/ui/mapView.go new file mode 100644 index 00000000..10929f68 --- /dev/null +++ b/go/ui/mapView.go @@ -0,0 +1,212 @@ +package ui + +import ( + "log" + "math" + "strings" + + "golang.org/x/exp/slices" + + "github.com/hajimehoshi/ebiten/v2" + "muhq.space/muhqs-game/go/assets" + "muhq.space/muhqs-game/go/game" +) + +const ( + PERMANENT_WIDTH int = 40 + PERMANENT_HEIGHT int = 40 + + TILE_WIDTH int = 50 + TILE_HEIGHT int = 50 +) + +type MapView struct { + 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{}} + return &vw +} + +func rotateStreetImg(x, y int, radian float64, op *ebiten.DrawImageOptions) { + // Move the image's center to the screen's upper-left corner. + // This is a preparation for rotating. When geometry matrices are applied, + // the origin point is the upper-left corner. + op.GeoM.Translate(-float64(TILE_WIDTH)/2, -float64(TILE_HEIGHT)/2) + + // Rotate the image. As a result, the anchor point of this rotate is + // the center of the image. + op.GeoM.Rotate(radian) + + // Reset translation + op.GeoM.Translate(float64(TILE_WIDTH)/2, float64(TILE_HEIGHT)/2) +} + +func (vw *MapView) rotateStreet(x, y int, op *ebiten.DrawImageOptions) { + connections, left, right, above, below := vw.gameState.Map.FindStreetConnections(x, y) + + if connections == 0 { + // This street is not connected to another street -> + // check any other non neutral tiles + connections, left, right, above, below = vw.gameState.Map.FindAnyConnections(x, y) + } + + // This street is not connected to anything. Seams odd! + if connections == 0 { + log.Printf("Street at (%d, %d) is not connected", x, y) + } else if (connections == 1 || + (connections == 2 && ((left && right) || (above && below)))) && + (above || below) { + rotateStreetImg(x, y, math.Pi/2, op) + } else if connections == 2 { + // normal orientation above and right + if right && below { + rotateStreetImg(x, y, 3*math.Pi/2, op) + } else if left && below { + rotateStreetImg(x, y, math.Pi, op) + } else if above && left { + rotateStreetImg(x, y, math.Pi/2, op) + } + } else if connections == 3 { + // normal orientation left above right + if above && right && below { + rotateStreetImg(x, y, 3*math.Pi/2, op) + } else if left && below && right { + rotateStreetImg(x, y, math.Pi, op) + } else if below && left && above { + rotateStreetImg(x, y, math.Pi/2, op) + } + } +} + +func (vw *MapView) newLayerImage() *ebiten.Image { + // TODO: support non symetric maps + maxWidth := len(vw.gameState.Map.Tiles[0]) * TILE_WIDTH + maxHeight := len(vw.gameState.Map.Tiles) * TILE_HEIGHT + return ebiten.NewImage(maxWidth, maxHeight) +} + +func (vw *MapView) drawMapLayer(screen *ebiten.Image) { + if vw.mapLayer == nil { + vw.mapLayer = vw.newLayerImage() + + x_px, y_px := 0.0, 0.0 + for y := 0; y < len(vw.gameState.Map.Tiles); y++ { + for x := 0; x < len(vw.gameState.Map.Tiles[y]); x++ { + pos := game.Position{x, y} + tile := vw.gameState.Map.TileAt(pos) + tileImg := assets.GetTile(tile.Raw) + + op := &ebiten.DrawImageOptions{} + // Rotate Street + if tile.Type == game.TileTypes.Street { + vw.rotateStreet(x, y, op) + } + + op.GeoM.Translate(x_px, y_px) + + if slices.Contains(vw.tileHighlights, pos) { + op.ColorM.Scale(1, 0, 0, 0.5) + } + + vw.mapLayer.DrawImage(tileImg, op) + + x_px += float64(TILE_WIDTH) + } + x_px = 0 + y_px += float64(TILE_HEIGHT) + } + } + + screen.DrawImage(vw.mapLayer, &ebiten.DrawImageOptions{}) +} + +func (vw *MapView) drawPermanentsLayer(screen *ebiten.Image) { + if vw.permanentsLayer == nil { + vw.permanentsLayer = vw.newLayerImage() + + for _, p := range vw.gameState.Permanents { + permanentSymbol := assets.GetSymbol(strings.ToLower(p.Card.Name)) + // TODO: Implement generic symbols + if permanentSymbol == nil { + continue + } + + 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)) + vw.permanentsLayer.DrawImage(permanentSymbol, op) + } + } + + screen.DrawImage(vw.permanentsLayer, &ebiten.DrawImageOptions{}) +} + +func (vw *MapView) Draw(screen *ebiten.Image) { + vw.drawMapLayer(screen) + vw.drawPermanentsLayer(screen) +} + +func (vw *MapView) ForceRedraw() { + vw.mapLayer = nil + vw.permanentsLayer = nil +} + +func (vw *MapView) FindObjectAt(x_px, y_px int) interface{} { + x := x_px / TILE_WIDTH + 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 + if pos.X == x && pos.Y == y { + return u + } + } + + if y < len(vw.gameState.Map.Tiles) && x < len(vw.gameState.Map.Tiles[y]) { + return &vw.gameState.Map.Tiles[y][x] + } + + return nil +} + +func (vw *MapView) HighlightPositions(pos []game.Position) { + vw.ForceRedraw() + vw.tileHighlights = pos +} + +func (vw *MapView) HighlightTiles(tiles []*game.Tile) { + pos := make([]game.Position, 0, len(tiles)) + for _, t := range tiles { + pos = append(pos, t.Position) + } + vw.HighlightPositions(pos) +} + +func (vw *MapView) HighlightTile(t *game.Tile) { + vw.HighlightTiles([]*game.Tile{t}) +} + +func (vw *MapView) ClearTileHighlights() { + vw.HighlightTiles([]*game.Tile{}) +} + +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) ClearPermanentsHighlights() { + vw.HighlightPermanents([]*game.Permanent{}) +} |
