diff --git a/assets/fonts/GlassAntiqua-Regular.ttf b/assets/fonts/GlassAntiqua-Regular.ttf new file mode 100644 index 0000000..8f9a067 Binary files /dev/null and b/assets/fonts/GlassAntiqua-Regular.ttf differ diff --git a/assets/textures/charmap.bmp b/assets/textures/charmap.bmp index 73b27e7..a156b68 100644 Binary files a/assets/textures/charmap.bmp and b/assets/textures/charmap.bmp differ diff --git a/assets/ui/helloworld.xml b/assets/ui/helloworld.xml new file mode 100644 index 0000000..9a1965a --- /dev/null +++ b/assets/ui/helloworld.xml @@ -0,0 +1,9 @@ + + + 0 + + diff --git a/assets/ui/schema.xsd b/assets/ui/schema.xsd new file mode 100644 index 0000000..67d40ce --- /dev/null +++ b/assets/ui/schema.xsd @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmd/game/main.go b/cmd/game/main.go index 0732851..ff6ff2c 100644 --- a/cmd/game/main.go +++ b/cmd/game/main.go @@ -13,7 +13,7 @@ func main() { l, err := level.Load("assets/levels/level1.bmlvl") if err != nil { - panic(err) + log.Fatal(err) } level.Set(l) @@ -25,7 +25,6 @@ func main() { // game loop for core.Running() { - firstperson.GetMouseInput() firstperson.MovePlayer() err = firstperson.RenderViewport() if err != nil { diff --git a/engine/core/window.go b/engine/core/window.go index 033d68b..eeb4f3f 100644 --- a/engine/core/window.go +++ b/engine/core/window.go @@ -17,6 +17,8 @@ package core import ( + "runtime" + "github.com/charmbracelet/log" "github.com/veandco/go-sdl2/sdl" ) @@ -143,6 +145,8 @@ func Start(t string) { log.Fatal("window already started") } + runtime.LockOSThread() + var err error title = t @@ -229,6 +233,7 @@ func Stop() { _ = window.Destroy() _ = renderer.Destroy() sdl.Quit() + log.Print("Engine stopped") } // Running returns whether the game loop should continue or not. @@ -242,7 +247,33 @@ func Running() bool { return running } +// Mouse states. +var ( + // MouseX is the current X position of the mouse. + MouseX int32 + // MouseY is the current Y position of the mouse. + MouseY int32 + // MouseDeltaX is the change in X position of the mouse since the last frame. + MouseDeltaX int32 + // MouseDeltaY is the change in Y position of the mouse since the last frame. + MouseDeltaY int32 + // MouseState is the current state of the mouse. + MouseState uint32 +) + func eventLoop() { + var mouseX, mouseY int32 + mouseX, mouseY, MouseState = sdl.GetMouseState() + if cursorLocked { + MouseDeltaX = mouseX - MouseX + MouseDeltaY = mouseY - MouseY + } else { + MouseDeltaX = 0 + MouseDeltaY = 0 + } + MouseX = mouseX + MouseY = mouseY + keyStates = sdl.GetKeyboardState() for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { switch event.GetType() { @@ -257,13 +288,30 @@ func eventLoop() { // Present draws the frame to the screen. // Call this at the very end of the game loop. func Present() { + if cursorHover { + sdl.SetCursor(sdl.CreateSystemCursor(sdl.SYSTEM_CURSOR_HAND)) + } else { + sdl.SetCursor(sdl.CreateSystemCursor(sdl.SYSTEM_CURSOR_ARROW)) + } renderer.Present() frameTime := uint32(sdl.GetTicks64() - frameStartTime) if frameTime < targetFrameTime { sdl.Delay(targetFrameTime - frameTime) } + + cursorHover = false } func FPS() float32 { return 1.0 / DeltaTime } + +func ScreenRect() *sdl.Rect { + return &sdl.Rect{W: width, H: height} +} + +var cursorHover = false + +func NotifyCursorHover() { + cursorHover = true +} diff --git a/engine/firstperson/input.go b/engine/firstperson/input.go index 4b439e0..1257a41 100644 --- a/engine/firstperson/input.go +++ b/engine/firstperson/input.go @@ -33,28 +33,12 @@ const ( turnSpeed float32 = 0.1 ) -var ( - MouseX int32 - MouseDeltaX int32 - MouseState uint32 -) - -func GetMouseInput() { - MouseX, _, MouseState = sdl.GetMouseState() - if core.IsCursorLocked() { - MouseDeltaX = MouseX - core.CenterX() - core.Window().WarpMouseInWindow(core.CenterX(), core.CenterY()) - } else { - MouseDeltaX = 0 - } -} - func KeyDown(key uint8) bool { return core.KeyStates()[key] != 0 } func MovePlayer() { - core.P.Angle += float32(MouseDeltaX) * turnSpeed * core.DeltaTime + core.P.Angle += float32(core.MouseDeltaX) * turnSpeed * core.DeltaTime if KeyDown(KeyForward) { core.P.Speed = 1 diff --git a/engine/firstperson/window.go b/engine/firstperson/window.go index e1eb6ff..3b74287 100644 --- a/engine/firstperson/window.go +++ b/engine/firstperson/window.go @@ -18,13 +18,11 @@ package firstperson import ( "github.com/bloodmagesoftware/bloodmage-engine/engine/core" - "github.com/bloodmagesoftware/bloodmage-engine/engine/level" "github.com/charmbracelet/log" "github.com/chewxy/math32" ) func Init() { - level.CollisionRound = 0.125 f := func() { log.Debug("firstperson window resize") screenDist = core.HalfWidthF() / math32.Tan(halfFov) diff --git a/engine/font/register.go b/engine/font/register.go index ba4f3cf..38a0ef7 100644 --- a/engine/font/register.go +++ b/engine/font/register.go @@ -14,28 +14,101 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +// Package font is for dealing with ttf fonts in the ui. +// +// Call font.Init() before using any other font function. +// Call font.Quit() at the end of your game. package font import ( - "github.com/bloodmagesoftware/bloodmage-engine/engine/textures" + "fmt" + "os" + + "github.com/veandco/go-sdl2/ttf" +) + +var ( + // fonts holds all registered fonts wether they are currently loaded or not. + fonts map[string]*font + // defaultFont is the font that is used when no other font is specified. + defaultFont *font ) type font struct { - startChar uint32 - endChar uint32 - texture *textures.Texture - charWidth int32 - charHeight int32 - collumnCount int32 -} - -func Load(texturePath string, startChar uint32, endChar uint32, charWidth int32, charHeight int32, collumnCount int32) *font { - return &font{ - startChar: startChar, - endChar: endChar, - texture: textures.Unregistered(texturePath), - charWidth: charWidth, - charHeight: charHeight, - collumnCount: collumnCount, + path string + ttf *ttf.Font +} + +// Font ensures the font is loaded and returns it. +func (f *font) Font() (*ttf.Font, error) { + if f.ttf != nil { + return f.ttf, nil + } + + font, err := ttf.OpenFont(f.path, 32) + if err != nil { + return nil, err + } + f.ttf = font + + return font, nil +} + +func Init() error { + return ttf.Init() +} + +// Quit closes all loaded fonts. +// After calling this function you can no longer use any font functions before calling Init() again. +func Quit() { + for _, font := range fonts { + if font.ttf != nil { + font.ttf.Close() + font.ttf = nil + } + } + ttf.Quit() +} + +func Register(fontPath string, name string) error { + if fonts == nil { + fonts = make(map[string]*font) + } + if _, ok := fonts[name]; ok { + return fmt.Errorf("font %s already registered", name) + } + // check if file exists with os.Stat() + if _, err := os.Stat(fontPath); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("font %s does not exist", fontPath) + } else { + return fmt.Errorf("error checking if font %s exists: %s", fontPath, err) + } + } + fonts[name] = &font{ + path: fontPath, + } + return nil +} + +func SetDefault(name string) error { + if _, ok := fonts[name]; !ok { + return fmt.Errorf("font %s not registered", name) + } + defaultFont = fonts[name] + return nil +} + +func Default() (*ttf.Font, error) { + if defaultFont != nil { + return defaultFont.Font() + } + return nil, fmt.Errorf("no default font set") +} + +func Get(name string) (*ttf.Font, error) { + if font, ok := fonts[name]; ok { + return font.Font() } + return nil, fmt.Errorf("font %s not registered", name) } diff --git a/engine/font/rune.go b/engine/font/rune.go deleted file mode 100644 index df504fd..0000000 --- a/engine/font/rune.go +++ /dev/null @@ -1,44 +0,0 @@ -package font - -import ( - "errors" - "fmt" - - "github.com/veandco/go-sdl2/sdl" -) - -func (f *font) Rune(r rune) (*sdl.Texture, *sdl.Rect, error) { - // get rune as number - rn := uint32(r) - - // check if rune is in range - if rn < f.startChar || rn > f.endChar { - return nil, nil, fmt.Errorf("rune %v (%v) is not in range %v-%v", rn, r, f.startChar, f.endChar) - } - - // get rune index - runeIndex := rn - f.startChar - - // get rune position in texture atlas raster (x, y) - runeX := runeIndex % uint32(f.collumnCount) - runeY := runeIndex / uint32(f.collumnCount) - - // get rune position in texture atlas (x, y, w, h) - runeRect := &sdl.Rect{ - X: int32(runeX) * f.charWidth, - Y: int32(runeY) * f.charHeight, - W: f.charWidth, - H: f.charHeight, - } - - // get rune texture - runeTexture, err := f.texture.Texture() - if err != nil { - return nil, nil, errors.Join( - errors.New("Failed to get font texture"), - err, - ) - } - - return runeTexture, runeRect, nil -} diff --git a/engine/level/level.go b/engine/level/level.go index ae17e3c..58336e3 100644 --- a/engine/level/level.go +++ b/engine/level/level.go @@ -21,6 +21,7 @@ import ( "path/filepath" "github.com/bloodmagesoftware/bloodmage-engine/engine/textures" + "github.com/charmbracelet/log" "github.com/veandco/go-sdl2/sdl" "google.golang.org/protobuf/proto" ) @@ -171,25 +172,13 @@ func Collision(x int, y int) bool { return currentLevel.SaveCollision(x, y) } -var CollisionRound = float32(0.4921875) - -func CollisionF(x float32, y float32) bool { - for y1 := int(y - CollisionRound); y1 <= int(y+CollisionRound); y1++ { - for x1 := int(x - CollisionRound); x1 <= int(x+CollisionRound); x1++ { - if Collision(x1, y1) { - return true - } - } - } - return false -} - func (self *Level) WallTexture(x int, y int) *textures.Texture { if !InBounds(x, y) { return textures.DefaultTexture() } return textures.Get(textures.Key(self.Walls[y*int(self.Width)+x])) } + func (self *Level) SetWall(x int, y int, wall byte) { if !self.InBounds(x, y) { return @@ -206,7 +195,7 @@ func (self *Level) FloorTexture() *sdl.Texture { if err != nil { t, err = textures.DefaultTexture().Texture() if err != nil { - panic(err) + log.Fatal(err) } } return t @@ -221,7 +210,7 @@ func (self *Level) CeilingTexture() *sdl.Texture { if err != nil { t, err = textures.DefaultTexture().Texture() if err != nil { - panic(err) + log.Fatal(err) } } return t diff --git a/engine/textures/register.go b/engine/textures/register.go index df8ffb7..a5fc398 100644 --- a/engine/textures/register.go +++ b/engine/textures/register.go @@ -20,6 +20,7 @@ import ( "image/color" "github.com/bloodmagesoftware/bloodmage-engine/engine/core" + "github.com/charmbracelet/log" "github.com/veandco/go-sdl2/sdl" ) @@ -33,7 +34,7 @@ var ( ) func Register(texturepath string, key Key) *Texture { - t := Unregistered(texturepath) + t := unregistered(texturepath) // add texture to registry registry[key] = t @@ -41,7 +42,7 @@ func Register(texturepath string, key Key) *Texture { return t } -func Unregistered(texturepath string) *Texture { +func unregistered(texturepath string) *Texture { t := &Texture{ path: texturepath, } @@ -65,13 +66,13 @@ func DefaultTexture() *Texture { // create pink texture for missing textures using sdl s, err := sdl.CreateRGBSurface(0, 1, 1, 32, 0, 0, 0, 0) if err != nil { - panic(err) + log.Fatal(err) } s.Set(0, 0, color.RGBA{R: 255, G: 0, B: 255, A: 255}) t, err := core.Renderer().CreateTextureFromSurface(s) if err != nil { - panic(err) + log.Fatal(err) } s.Free() diff --git a/engine/topdown/input.go b/engine/topdown/input.go deleted file mode 100644 index 7ef9830..0000000 --- a/engine/topdown/input.go +++ /dev/null @@ -1,69 +0,0 @@ -// Bloodmage Engine -// Copyright (C) 2024 Frank Mayer -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package topdown - -import ( - "github.com/bloodmagesoftware/bloodmage-engine/engine/core" - "github.com/bloodmagesoftware/bloodmage-engine/engine/level" - "github.com/chewxy/math32" - "github.com/veandco/go-sdl2/sdl" -) - -const ( - velocity = 2 -) - -func ProcessInput() { - core.P.Speed = 0.0 - core.P.Strafe = 0.0 - - if core.KeyStates()[sdl.SCANCODE_DOWN] == 1 { - core.P.Speed = 1 - } else if core.KeyStates()[sdl.SCANCODE_UP] == 1 { - core.P.Speed = -1 - } - - if core.KeyStates()[sdl.SCANCODE_LEFT] == 1 { - core.P.Strafe = -1 - } else if core.KeyStates()[sdl.SCANCODE_RIGHT] == 1 { - core.P.Strafe = 1 - } -} - -func MovePlayer() { - // calculate direction vector (normalized) - length := math32.Sqrt(core.P.Speed*core.P.Speed + core.P.Strafe*core.P.Strafe) - if length == 0 { - return - } - - dirX := core.P.Strafe / length * velocity - dirY := core.P.Speed / length * velocity - - // calculate new position - newX := core.P.X + dirX*core.DeltaTime - newY := core.P.Y + dirY*core.DeltaTime - - // check if new position is valid - if !level.CollisionF(newX, core.P.Y) { - core.P.X = newX - } - - if !level.CollisionF(core.P.X, newY) { - core.P.Y = newY - } -} diff --git a/engine/topdown/renderer.go b/engine/topdown/renderer.go deleted file mode 100644 index 7626578..0000000 --- a/engine/topdown/renderer.go +++ /dev/null @@ -1,75 +0,0 @@ -// Bloodmage Engine -// Copyright (C) 2024 Frank Mayer -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package topdown - -import ( - "github.com/bloodmagesoftware/bloodmage-engine/engine/core" - "github.com/bloodmagesoftware/bloodmage-engine/engine/level" - "github.com/veandco/go-sdl2/sdl" -) - -func minInt32(a, b int32) int32 { - if a < b { - return a - } - return b -} - -// RenderViewport renders a tow down view of the current level with the player in the center. -func RenderViewport() { - tileSize := minInt32(core.Width(), core.Height()) / 10 - - // draw floor - rect := sdl.Rect{X: 0, Y: 0, W: tileSize, H: tileSize} - for y := 0; y != level.Height(); y++ { - rect.Y = int32(y)*tileSize - int32(core.P.Y*float32(tileSize)) + int32(core.Height())/2 - - if rect.Y > int32(core.Height()) { - continue - } - - if rect.Y+rect.H < 0 { - continue - } - - for x := 0; x != level.Width(); x++ { - rect.X = int32(x)*tileSize - int32(core.P.X*float32(tileSize)) + int32(core.Width())/2 - - if rect.X > int32(core.Width()) { - continue - } - - if rect.X+rect.W < 0 { - continue - } - - if level.Collision(x, y) { - _ = core.Renderer().SetDrawColor(128, 128, 128, 255) - } else { - _ = core.Renderer().SetDrawColor(8, 8, 8, 255) - } - - _ = core.Renderer().FillRect(&rect) - } - } - - // draw player - rect.X = int32(core.Width())/2 - tileSize/2 - rect.Y = int32(core.Height())/2 - tileSize/2 - _ = core.Renderer().SetDrawColor(255, 255, 255, 255) - _ = core.Renderer().FillRect(&rect) -} diff --git a/engine/topdown/topdown.go b/engine/topdown/topdown.go deleted file mode 100644 index 4701cff..0000000 --- a/engine/topdown/topdown.go +++ /dev/null @@ -1,29 +0,0 @@ -// Bloodmage Engine -// Copyright (C) 2024 Frank Mayer -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package topdown - -import ( - "github.com/bloodmagesoftware/bloodmage-engine/engine/core" - "github.com/charmbracelet/log" -) - -func Init() { - f := func() { - log.Debug("topdown window resize") - } - core.OnResize(&f) -} diff --git a/engine/ui/button.go b/engine/ui/button.go new file mode 100644 index 0000000..9697a4a --- /dev/null +++ b/engine/ui/button.go @@ -0,0 +1,105 @@ +// Bloodmage Engine +// Copyright (C) 2024 Frank Mayer +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ui + +import ( + "errors" + + "github.com/bloodmagesoftware/bloodmage-engine/engine/core" + "github.com/veandco/go-sdl2/sdl" +) + +type Button struct { + doc *document + id string + mouseDown bool + content Element + rect sdl.Rect +} + +func newButton() *Button { + return &Button{ + doc: nil, + id: "", + mouseDown: false, + content: nil, + rect: sdl.Rect{}, + } +} + +func (b *Button) AppendChild(e Element) error { + if b.content != nil { + return errors.New("button already has content") + } + b.content = e + return nil +} + +func (b *Button) SetAttribute(key, value string) error { + switch key { + case "id": + b.id = value + default: + return errors.New("unknown attribute: " + key) + } + return nil +} + +func (b *Button) setDocument(doc *document) { + b.doc = doc +} + +func (b *Button) setTextContent(content string) error { + return errors.New("button cannot have text content") +} + +func (b *Button) setRect(rect *sdl.Rect) { + b.rect.X = rect.X + b.rect.Y = rect.Y + b.rect.W = rect.W + b.rect.H = rect.H +} + +func (b *Button) MouseOver() bool { + if b.rect.X <= core.MouseX && core.MouseX <= b.rect.X+b.rect.W && b.rect.Y <= core.MouseY && core.MouseY <= b.rect.Y+b.rect.H { + core.NotifyCursorHover() + return true + } + b.mouseDown = false + return false +} + +// Clicked returns true if these is a rising edge of the left mouse button +func (b *Button) Clicked() bool { + lMouseDown := core.MouseState&sdl.ButtonLMask() != 0 + if b.MouseOver() { + if lMouseDown { + b.mouseDown = true + return false + } else { + if b.mouseDown { + b.mouseDown = false + return true + } else { + return false + } + } + } else { + // b.mouseDown = false + return false + } +} diff --git a/engine/ui/document.go b/engine/ui/document.go new file mode 100644 index 0000000..554e1ad --- /dev/null +++ b/engine/ui/document.go @@ -0,0 +1,85 @@ +// Bloodmage Engine +// Copyright (C) 2024 Frank Mayer +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ui + +type document struct { + root Element + idMap map[string]Element +} + +func newDocument() document { + return document{ + idMap: make(map[string]Element), + } +} + +func (d *document) RootElement() Element { + return d.root +} + +func (d *document) GetElementById(id string) (Element, bool) { + e, ok := d.idMap[id] + return e, ok +} + +func (d *document) GetTextElementById(id string) (*Text, bool) { + e, ok := d.idMap[id] + if !ok { + return nil, false + } + t, ok := e.(*Text) + if !ok { + return nil, false + } + return t, true +} + +func (d *document) GetButtonElementById(id string) (*Button, bool) { + e, ok := d.idMap[id] + if !ok { + return nil, false + } + b, ok := e.(*Button) + if !ok { + return nil, false + } + return b, true +} + +func (d *document) GetImageElementById(id string) (*Image, bool) { + e, ok := d.idMap[id] + if !ok { + return nil, false + } + i, ok := e.(*Image) + if !ok { + return nil, false + } + return i, true +} + +func (d *document) GetListElementById(id string) (*List, bool) { + e, ok := d.idMap[id] + if !ok { + return nil, false + } + l, ok := e.(*List) + if !ok { + return nil, false + } + return l, true +} diff --git a/engine/ui/image.go b/engine/ui/image.go new file mode 100644 index 0000000..a9df7c3 --- /dev/null +++ b/engine/ui/image.go @@ -0,0 +1,132 @@ +// Bloodmage Engine +// Copyright (C) 2024 Frank Mayer +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ui + +import ( + "errors" + "strconv" + + "github.com/bloodmagesoftware/bloodmage-engine/engine/core" + "github.com/veandco/go-sdl2/sdl" +) + +type Image struct { + doc *document + id string + src string + width int32 + height int32 + texture *sdl.Texture +} + +func newImage() *Image { + return &Image{ + doc: nil, + id: "", + src: "", + width: -1, + height: -1, + texture: nil, + } +} + +func (i *Image) AppendChild(e Element) error { + return errors.New("Image cannot have children") +} + +func (i *Image) SetAttribute(key, value string) error { + switch key { + case "id": + i.id = value + case "src": + return i.SetSrc(value) + case "width": + if w, err := strconv.Atoi(value); err != nil { + return err + } else if w < 0 { + return errors.New("width must be greater than 0") + } else { + i.width = int32(w) + } + case "height": + if h, err := strconv.Atoi(value); err != nil { + return err + } else if h < 0 { + return errors.New("height must be greater than 0") + } else { + i.height = int32(h) + } + default: + return errors.New("unknown attribute: " + key) + } + return nil +} + +func (i *Image) setDocument(doc *document) { + i.doc = doc +} + +func (i *Image) Src() string { + return i.src +} + +func (i *Image) SetSrc(src string) error { + if i.src == src { + return nil + } + if i.texture != nil { + err := i.texture.Destroy() + if err != nil { + return err + } + } + surface, err := sdl.LoadBMP(src) + if err != nil { + return err + } + i.texture, err = core.Renderer().CreateTextureFromSurface(surface) + if err != nil { + return err + } + surface.Free() + i.src = src + return nil +} + +func (i *Image) Texture() (*sdl.Texture, error) { + if i.texture == nil { + return nil, errors.New("texture is nil") + } + return i.texture, nil +} + +func (i *Image) rect() (*sdl.Rect, error) { + if i.width <= 0 { + return nil, errors.New("image width must be greater than 0") + } + if i.height <= 0 { + return nil, errors.New("image height must be greater than 0") + } + return &sdl.Rect{ + X: 0, Y: 0, + W: i.width, H: i.height, + }, nil +} + +func (i *Image) setTextContent(content string) error { + return errors.New("Image cannot have text content") +} diff --git a/engine/ui/list.go b/engine/ui/list.go new file mode 100644 index 0000000..45fbd22 --- /dev/null +++ b/engine/ui/list.go @@ -0,0 +1,75 @@ +// Bloodmage Engine +// Copyright (C) 2024 Frank Mayer +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ui + +import "errors" + +type orientation uint8 + +const ( + orientation_unset orientation = iota + orientation_vertical + orientation_horizontal +) + +type List struct { + doc *document + orientation + items []Element +} + +func newList() *List { + return &List{ + doc: nil, + orientation: orientation_unset, + items: nil, + } +} + +func (l *List) AppendChild(e Element) error { + l.items = append(l.items, e) + return nil +} + +func (l *List) SetAttribute(key, value string) error { + switch key { + case "orientation": + switch value { + case "vertical": + l.orientation = orientation_vertical + case "horizontal": + l.orientation = orientation_horizontal + default: + return errors.New("unknown orientation: " + value) + } + default: + return errors.New("unknown attribute: " + key) + } + return nil +} + +func (l *List) setDocument(doc *document) { + l.doc = doc +} + +func (l *List) Items() []Element { + return l.items +} + +func (l *List) setTextContent(content string) error { + return errors.New("list cannot have text content") +} diff --git a/engine/ui/parser.go b/engine/ui/parser.go new file mode 100644 index 0000000..de35348 --- /dev/null +++ b/engine/ui/parser.go @@ -0,0 +1,131 @@ +// Bloodmage Engine +// Copyright (C) 2024 Frank Mayer +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ui + +import ( + "encoding/xml" + "errors" + "fmt" + "os" + "strings" + + "github.com/bloodmagesoftware/bloodmage-engine/engine/utils" + "github.com/veandco/go-sdl2/sdl" +) + +type Element interface { + AppendChild(Element) error + SetAttribute(string, string) error + setTextContent(string) error + setDocument(*document) + draw() (drawFn, *sdl.Rect, error) +} + +func CreateElement(name string) (Element, error) { + switch name { + case "List": + return newList(), nil + case "Button": + return newButton(), nil + case "Text": + return newText(), nil + case "Image": + return newImage(), nil + default: + return nil, fmt.Errorf("unknown element type: %s", name) + } +} + +func Parse(path string) (*document, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + decoder := xml.NewDecoder(file) + + var root Element + doc := newDocument() + elStack := utils.NewStack[Element]() + + for { + token, err := decoder.Token() + if err != nil { + break + } + + switch t := token.(type) { + case xml.StartElement: + // create element + el, err := CreateElement(t.Name.Local) + if err != nil { + return nil, err + } + // might be root element + if root == nil { + root = el + doc.root = el + } + el.setDocument(&doc) + // set attributes + for _, attr := range t.Attr { + if attr.Name.Space != "" { + continue + } + if attr.Name.Local == "id" { + doc.idMap[attr.Value] = el + } + if err := el.SetAttribute(attr.Name.Local, attr.Value); err != nil { + return nil, errors.Join( + fmt.Errorf("error setting attribute %s=%s on element %s", attr.Name.Local, attr.Value, t.Name.Local), + err, + ) + } + } + // append to parent if exists + if parent, hasParent := elStack.Peek(); hasParent { + if err := (*parent).AppendChild(el); err != nil { + return nil, err + } + } + // push to stack for future children + elStack.Push(el) + + case xml.CharData: + text := strings.TrimSpace(string(t)) + if text == "" { + continue + } + if el, hasElement := elStack.Peek(); hasElement { + if err := (*el).setTextContent(text); err != nil { + return nil, err + } + } + + case xml.EndElement: + // pop from stack because we're done with this element + elStack.Pop() + } + } + + if root == nil { + return nil, fmt.Errorf("no root element found") + } + + return &doc, nil +} diff --git a/engine/ui/render.go b/engine/ui/render.go new file mode 100644 index 0000000..423bfd4 --- /dev/null +++ b/engine/ui/render.go @@ -0,0 +1,205 @@ +// Bloodmage Engine +// Copyright (C) 2024 Frank Mayer +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ui + +import ( + "errors" + "fmt" + + "github.com/bloodmagesoftware/bloodmage-engine/engine/core" + "github.com/bloodmagesoftware/bloodmage-engine/engine/font" + "github.com/charmbracelet/log" + "github.com/veandco/go-sdl2/sdl" + "github.com/veandco/go-sdl2/ttf" +) + +type drawFn func(dest *sdl.Rect) error + +func (document *document) Draw() error { + fn, rect, err := document.root.draw() + if err != nil { + return err + } + if rect == nil { + rect = core.ScreenRect() + } + if err := fn(rect); err != nil { + return err + } + return nil +} + +// draw functions + +func (image *Image) draw() (drawFn, *sdl.Rect, error) { + t, err := image.Texture() + if err != nil { + return nil, defaultRect, err + } + rect, err := image.rect() + if err != nil { + return nil, defaultRect, err + } + + return func(dest *sdl.Rect) error { + return core.Renderer().Copy(t, nil, dest) + }, rect, nil +} + +func (button *Button) draw() (drawFn, *sdl.Rect, error) { + if button.content == nil { + return nil, defaultRect, errors.New("Button content is nil") + } + + fn, rect, err := button.content.draw() + return func(dest *sdl.Rect) error { + button.setRect(dest) + return fn(dest) + }, rect, err +} + +func (list *List) draw() (drawFn, *sdl.Rect, error) { + fnList := make([]drawFn, len(list.items)) + rectList := make([]*sdl.Rect, len(list.items)) + + expectedSize := sdl.Rect{ + X: 0, Y: 0, + W: 0, H: 0, + } + + for i, child := range list.items { + fn, srcRect, err := child.draw() + if err != nil { + return nil, defaultRect, errors.Join( + fmt.Errorf("Error retrieving draw function for list item %d", i), + err, + ) + } + if srcRect == nil { + return nil, srcRect, fmt.Errorf("List item %d returned nil rect", i) + } + if fn == nil { + return nil, srcRect, fmt.Errorf("List item %d returned nil draw function", i) + } + fnList[i] = fn + rectList[i] = srcRect + switch list.orientation { + case orientation_horizontal: + expectedSize.W += srcRect.W + if srcRect.H > expectedSize.H { + expectedSize.H = srcRect.H + } + case orientation_vertical: + expectedSize.H += srcRect.H + if srcRect.W > expectedSize.W { + expectedSize.W = srcRect.W + } + } + } + + switch list.orientation { + case orientation_horizontal: + return func(dest *sdl.Rect) error { + x := dest.X + for i, fn := range fnList { + itemRect := rectList[i] + width := itemRect.W + itemRect.X + itemDestRect := &sdl.Rect{ + X: x, Y: dest.Y, + W: width, H: itemRect.H, + } + if x+width > dest.W { + log.Warnf("list item %d is too wide %d > %d", i, x+width, dest.W) + itemDestRect.W = 0 + } + itemDestRect.X = x + itemDestRect.W = width + if err := fn(itemDestRect); err != nil { + return err + } + x += width + } + return nil + }, &expectedSize, nil + case orientation_vertical: + return func(dest *sdl.Rect) error { + y := dest.Y + for i, fn := range fnList { + itemRect := rectList[i] + height := itemRect.H + itemRect.Y + itemDestRect := &sdl.Rect{ + X: dest.X, Y: y, + W: itemRect.W, H: height, + } + if y+height > dest.H { + log.Warnf("list item %d is too tall %d > %d", i, y+height, dest.H) + itemDestRect.H = 0 + } + itemDestRect.Y = y + itemDestRect.H = height + if err := fn(itemDestRect); err != nil { + return err + } + y += height + } + return nil + }, &expectedSize, nil + default: + return nil, &expectedSize, errors.New("Invalid orientation") + } +} + +func (text *Text) draw() (drawFn, *sdl.Rect, error) { + var err error + var f *ttf.Font + if text.font == "" { + if f, err = font.Default(); err != nil { + return nil, defaultRect, errors.Join( + errors.New("No font specified for text element"), + err, + ) + } + } else { + if f, err = font.Get(text.font); err != nil { + return nil, defaultRect, errors.Join( + fmt.Errorf("Font %s not found", text.font), + err, + ) + } + } + + surface, err := f.RenderUTF8Solid(text.content, text.color) + if err != nil { + return nil, defaultRect, err + } + defer surface.Free() + + scrRect := sdl.Rect{ + X: 0, Y: 0, + W: surface.W, H: surface.H, + } + + texture, err := core.Renderer().CreateTextureFromSurface(surface) + if err != nil { + log.Fatal(err) + } + + return func(dest *sdl.Rect) error { + defer texture.Destroy() + return core.Renderer().Copy(texture, &scrRect, dest) + }, &scrRect, nil +} diff --git a/engine/ui/text.go b/engine/ui/text.go new file mode 100644 index 0000000..e30e007 --- /dev/null +++ b/engine/ui/text.go @@ -0,0 +1,82 @@ +// Bloodmage Engine +// Copyright (C) 2024 Frank Mayer +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ui + +import ( + "errors" + + "github.com/veandco/go-sdl2/sdl" +) + +type Text struct { + doc *document + id string + content string + font string + color sdl.Color +} + +func newText() *Text { + return &Text{ + doc: nil, + id: "", + content: "", + font: "", + color: sdl.Color{R: 255, G: 255, B: 255, A: 255}, + } +} + +func (t *Text) AppendChild(e Element) error { + return errors.New("Text cannot have children") +} + +func (t *Text) SetAttribute(key, value string) error { + switch key { + case "id": + t.id = value + case "content": + t.content = value + case "font": + t.font = value + case "color": + var err error + if t.color, err = ParseColor(value); err != nil { + return err + } + default: + return errors.New("unknown attribute: " + key) + } + return nil +} + +func (t *Text) setDocument(doc *document) { + t.doc = doc +} + +func (t *Text) Content() string { + return t.content +} + +func (t *Text) SetContent(content string) error { + t.content = content + return nil +} + +func (t *Text) setTextContent(content string) error { + t.content = content + return nil +} diff --git a/engine/ui/utils.go b/engine/ui/utils.go new file mode 100644 index 0000000..02b1269 --- /dev/null +++ b/engine/ui/utils.go @@ -0,0 +1,147 @@ +// Bloodmage Engine +// Copyright (C) 2024 Frank Mayer +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ui + +import ( + "fmt" + "strconv" + "strings" + + "github.com/veandco/go-sdl2/sdl" +) + +var ( + // default rect + defaultRect = &sdl.Rect{X: 0, Y: 0, W: 0, H: 0} +) + +// parseColor parses a color string and returns the color channel values. +// +// Possible formats: +// - #RRGGBB +// - #RRGGBBAA +// - rgb(R, G, B) +// - rgba(R, G, B, A) +func ParseColorChannels(color string) (r uint8, g uint8, b uint8, a uint8, err error) { + // Remove any whitespace + color = strings.ReplaceAll(color, " ", "") + + // Check for hex color format + if strings.HasPrefix(color, "#") { + color = strings.TrimPrefix(color, "#") + + switch len(color) { + case 6: + // #RRGGBB format + if r, err = parseHex(color[0:2]); err != nil { + return 0, 0, 0, 0, err + } + if g, err = parseHex(color[2:4]); err != nil { + return 0, 0, 0, 0, err + } + if b, err = parseHex(color[4:6]); err != nil { + return 0, 0, 0, 0, err + } + a = 255 + case 8: + // #RRGGBBAA format + if r, err = parseHex(color[0:2]); err != nil { + return 0, 0, 0, 0, err + } + if g, err = parseHex(color[2:4]); err != nil { + return 0, 0, 0, 0, err + } + if b, err = parseHex(color[4:6]); err != nil { + return 0, 0, 0, 0, err + } + if a, err = parseHex(color[6:8]); err != nil { + return 0, 0, 0, 0, err + } + default: + return 0, 0, 0, 0, fmt.Errorf("invalid hex color format: %s", color) + } + } else if strings.HasPrefix(color, "rgb(") && strings.HasSuffix(color, ")") { + // cut off the "rgb(" and ")" using indexes + // split the values by comma + rgbValues := strings.Split(color[4:len(color)-1], ",") + if len(rgbValues) != 3 { + return 0, 0, 0, 0, fmt.Errorf("invalid rgb color format: %s", color) + } + if r64, err := strconv.ParseUint(rgbValues[0], 10, 8); err == nil { + r = uint8(r64) + } else { + return 0, 0, 0, 0, err + } + if g64, err := strconv.ParseUint(rgbValues[1], 10, 8); err == nil { + g = uint8(g64) + } else { + return 0, 0, 0, 0, err + } + if b64, err := strconv.ParseUint(rgbValues[2], 10, 8); err == nil { + b = uint8(b64) + } else { + return 0, 0, 0, 0, err + } + a = 255 + } else if strings.HasPrefix(color, "rgba(") && strings.HasSuffix(color, ")") { + // cut off the "rgba(" and ")" using indexes + // split the values by comma + rgbaValues := strings.Split(color[5:len(color)-1], ",") + if len(rgbaValues) != 4 { + return 0, 0, 0, 0, fmt.Errorf("invalid rgba color format: %s", color) + } + if r64, err := strconv.ParseUint(rgbaValues[0], 10, 8); err == nil { + r = uint8(r64) + } else { + return 0, 0, 0, 0, err + } + if g64, err := strconv.ParseUint(rgbaValues[1], 10, 8); err == nil { + g = uint8(g64) + } else { + return 0, 0, 0, 0, err + } + if b64, err := strconv.ParseUint(rgbaValues[2], 10, 8); err == nil { + b = uint8(b64) + } else { + return 0, 0, 0, 0, err + } + if a64, err := strconv.ParseUint(rgbaValues[3], 10, 8); err == nil { + a = uint8(a64) + } else { + return 0, 0, 0, 0, err + } + } else { + return 0, 0, 0, 0, fmt.Errorf("unsupported color format: %s", color) + } + + return r, g, b, a, nil +} + +// Helper function to parse hexadecimal values +func parseHex(hex string) (uint8, error) { + value, err := strconv.ParseUint(hex, 16, 8) + if err != nil { + return 0, err + } + return uint8(value), nil +} + +// ParseColor parses a color string into an sdl.Color struct. +func ParseColor(color string) (sdl.Color, error) { + r, g, b, a, err := ParseColorChannels(color) + return sdl.Color{R: r, G: g, B: b, A: a}, err +} diff --git a/engine/ui/utils_test.go b/engine/ui/utils_test.go new file mode 100644 index 0000000..3f209a2 --- /dev/null +++ b/engine/ui/utils_test.go @@ -0,0 +1,87 @@ +// Bloodmage Engine +// Copyright (C) 2024 Frank Mayer +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ui_test + +import ( + "fmt" + "testing" + + "github.com/bloodmagesoftware/bloodmage-engine/engine/ui" +) + +func TestParseColorChannels(t *testing.T) { + t.Parallel() + + tests := []struct { + color string + r uint8 + g uint8 + b uint8 + a uint8 + err bool + }{ + {"#000000", 0, 0, 0, 255, false}, + {"#00000000", 0, 0, 0, 0, false}, + {"#ffffff", 255, 255, 255, 255, false}, + {"#ffffffff", 255, 255, 255, 255, false}, + {"#ff0000", 255, 0, 0, 255, false}, + {"#ff000000", 255, 0, 0, 0, false}, + {"#00ff00", 0, 255, 0, 255, false}, + {"#00ff0000", 0, 255, 0, 0, false}, + {"#0000ff", 0, 0, 255, 255, false}, + {"#0000ff00", 0, 0, 255, 0, false}, + {"rgb(0, 0, 0)", 0, 0, 0, 255, false}, + {"rgb(11,22,23)", 11, 22, 23, 255, false}, + {"rgb(11, 22, 23)", 11, 22, 23, 255, false}, + {"rgb(255, 255, 255)", 255, 255, 255, 255, false}, + {"rgb(255, 0, 0)", 255, 0, 0, 255, false}, + {"rgb(0, 255, 0)", 0, 255, 0, 255, false}, + {"rgb(0, 0, 255)", 0, 0, 255, 255, false}, + {"rgba(0, 0, 0, 0)", 0, 0, 0, 0, false}, + {"rgba(255, 255, 255, 255)", 255, 255, 255, 255, false}, + {"rgba(255, 0, 0, 255)", 255, 0, 0, 255, false}, + {"rgba(0, 255, 0, 255)", 0, 255, 0, 255, false}, + {"rgba(0, 0, 255, 255)", 0, 0, 255, 255, false}, + {"#000", 0, 0, 0, 0, true}, + {"#0000000", 0, 0, 0, 0, true}, + {"#000000000", 0, 0, 0, 0, true}, + {"#0000000000", 0, 0, 0, 0, true}, + {"#00000000000", 0, 0, 0, 0, true}, + {"#üdußnz", 0, 0, 0, 0, true}, + {"#üdußnzßz", 0, 0, 0, 0, true}, + {"rgb(foo, bar, baz)", 0, 0, 0, 0, true}, + {"rgba(foo, bar, baz, qux)", 0, 0, 0, 0, true}, + {"#ff1493", 255, 20, 147, 255, false}, + {"#ff149380", 255, 20, 147, 128, false}, + {"rgb(255, 20, 147)", 255, 20, 147, 255, false}, + {"rgb(255, 20, 147, 128)", 0, 0, 0, 0, true}, + {"rgba(255, 20, 147, 128)", 255, 20, 147, 128, false}, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("test %d: %s", i, test.color), func(t *testing.T) { + r, g, b, a, err := ui.ParseColorChannels(test.color) + if err != nil && !test.err { + t.Errorf("unexpected error: %s", err) + } else if err == nil && test.err { + t.Errorf("expected error, got nil (%d, %d, %d, %d)", r, g, b, a) + } else if r != test.r || g != test.g || b != test.b || a != test.a { + t.Errorf("expected (%d, %d, %d, %d), got (%d, %d, %d, %d)", test.r, test.g, test.b, test.a, r, g, b, a) + } + }) + } +} diff --git a/engine/utils/stack.go b/engine/utils/stack.go new file mode 100644 index 0000000..22ea4f9 --- /dev/null +++ b/engine/utils/stack.go @@ -0,0 +1,39 @@ +package utils + +type Stack[T any] struct { + data []T +} + +func NewStack[T any]() *Stack[T] { + return &Stack[T]{ + data: make([]T, 0), + } +} + +func (s *Stack[T]) Push(data T) { + s.data = append(s.data, data) +} + +func (s *Stack[T]) Pop() (*T, bool) { + if len(s.data) == 0 { + return nil, false + } + data := s.data[len(s.data)-1] + s.data = s.data[:len(s.data)-1] + return &data, true +} + +func (s *Stack[T]) Empty() bool { + return len(s.data) == 0 +} + +func (s *Stack[T]) Len() int { + return len(s.data) +} + +func (s *Stack[T]) Peek() (*T, bool) { + if len(s.data) == 0 { + return nil, false + } + return &s.data[len(s.data)-1], true +} diff --git a/engine/utils/stack_test.go b/engine/utils/stack_test.go new file mode 100644 index 0000000..09ee803 --- /dev/null +++ b/engine/utils/stack_test.go @@ -0,0 +1,56 @@ +package utils_test + +import ( + "testing" + + "github.com/bloodmagesoftware/bloodmage-engine/engine/utils" +) + +func TestStack(t *testing.T) { + t.Parallel() + s := utils.NewStack[int]() + + if s.Len() != 0 { + t.Errorf("Stack should be empty") + } + + s.Push(1) + s.Push(2) + s.Push(3) + + if s.Len() != 3 { + t.Errorf("Stack should have 3 elements") + } + + if v, b := s.Pop(); *v != 3 || !b { + t.Errorf("Stack should return 3") + } + + if s.Len() != 2 { + t.Errorf("Stack should have 2 elements") + } + + if v, b := s.Pop(); *v != 2 || !b { + t.Errorf("Stack should return 2") + } + + if s.Len() != 1 { + t.Errorf("Stack should have 1 element") + } + + if v, b := s.Pop(); *v != 1 || !b { + t.Errorf("Stack should return 1") + } + + if s.Len() != 0 { + t.Errorf("Stack should be empty") + } + + if v, b := s.Pop(); v != nil || b { + t.Errorf("Stack should return nil and false") + } + + if s.Len() != 0 { + t.Errorf("Stack should be empty") + } +} diff --git a/examples/firstperson/main.go b/examples/firstperson/main.go index 90f8c99..60615e8 100644 --- a/examples/firstperson/main.go +++ b/examples/firstperson/main.go @@ -1,43 +1,90 @@ package main import ( + "fmt" + "github.com/bloodmagesoftware/bloodmage-engine/engine/core" "github.com/bloodmagesoftware/bloodmage-engine/engine/firstperson" + "github.com/bloodmagesoftware/bloodmage-engine/engine/font" "github.com/bloodmagesoftware/bloodmage-engine/engine/level" "github.com/bloodmagesoftware/bloodmage-engine/engine/textures" + "github.com/bloodmagesoftware/bloodmage-engine/engine/ui" "github.com/charmbracelet/log" "github.com/veandco/go-sdl2/sdl" + "github.com/veandco/go-sdl2/ttf" ) func main() { + var err error core.InitOptions() l := level.New() level.Set(l) + + // register textures textures.Register("assets/textures/2.bmp", 2) textures.Register("assets/textures/1.bmp", 1) + // register fonts + if err = font.Init(); err != nil { + log.Fatal(err) + } + if err = font.Register("./assets/fonts/GlassAntiqua-Regular.ttf", "Glass Antiqua"); err != nil { + log.Fatal(err) + } + if err = font.SetDefault("Glass Antiqua"); err != nil { + log.Fatal(err) + } + + // set player start position core.P.X = 1.5 core.P.Y = 1.5 + // inet game mode firstperson.Init() core.Start("First Person Example") defer core.Stop() - var err error + core.LockCursor(false) - core.LockCursor(true) + err = ttf.Init() + if err != nil { + log.Fatal(err) + } + + document, err := ui.Parse("./assets/ui/helloworld.xml") + if err != nil { + log.Fatal(err) + } + + btnEl, ok := document.GetButtonElementById("btn") + if !ok { + log.Fatal("Could not find element with id 'btn'") + } + i := 0 + + counterEl, ok := document.GetTextElementById("counter") + if !ok { + log.Fatal("Could not find element with id 'counter'") + } // game loop for core.Running() { if core.KeyStates()[sdl.SCANCODE_ESCAPE] != 0 { break } - firstperson.GetMouseInput() firstperson.MovePlayer() - err = firstperson.RenderViewport() - if err != nil { - log.Error(err) + if err = firstperson.RenderViewport(); err != nil { + log.Fatal(err) + } + + if err = document.Draw(); err != nil { + log.Fatal(err) + } + + if btnEl.Clicked() { + i++ + _ = counterEl.SetContent(fmt.Sprintf("Clicked %d times", i)) } // draw frame diff --git a/examples/topdown/main.go b/examples/topdown/main.go deleted file mode 100644 index f11cf20..0000000 --- a/examples/topdown/main.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "github.com/bloodmagesoftware/bloodmage-engine/engine/core" - "github.com/bloodmagesoftware/bloodmage-engine/engine/level" - "github.com/bloodmagesoftware/bloodmage-engine/engine/topdown" -) - -func main() { - core.InitOptions() - - l := level.New() - level.Set(l) - - core.P.X = 1.5 - core.P.Y = 1.5 - - topdown.Init() - core.Start("Top Down Example") - defer core.Stop() - - core.LockCursor(true) - - // game loop - for core.Running() { - topdown.ProcessInput() - topdown.MovePlayer() - topdown.RenderViewport() - - // draw frame - core.Present() - } -}