commit afaa190648ebce94458f12063228f113da215bac Author: Sander Schobers Date: Thu Dec 19 06:59:45 2019 +0100 Added simple tile rendering. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca1f0e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Visual Studio Code +.vscode + +# Go +__debug_bin +rice-box.go +*.rice-box.* diff --git a/alui/button.go b/alui/button.go new file mode 100644 index 0000000..8fac85f --- /dev/null +++ b/alui/button.go @@ -0,0 +1,31 @@ +package alui + +import ( + "opslag.de/schobers/allg5" + "opslag.de/schobers/geom" +) + +var _ Control = &Button{} + +type Button struct { + ControlBase + + Text string +} + +func (b *Button) DesiredSize(ctx *Context) geom.PointF32 { + font := ctx.Fonts.Get(b.Font) + _, _, w, h := font.TextDimensions(b.Text) + return geom.PtF32(w+8, h+8) +} + +func (b *Button) Render(ctx *Context, bounds geom.RectangleF32) { + font := ctx.Fonts.Get(b.Font) + + fore := ctx.Palette.Primary + if b.Over { + fore = ctx.Palette.Dark + ctx.Cursor = allg5.MouseCursorLink + } + font.Draw(bounds.Min.X+4, bounds.Min.Y+4, fore, allg5.AlignLeft, b.Text) +} diff --git a/alui/container.go b/alui/container.go new file mode 100644 index 0000000..2e545fa --- /dev/null +++ b/alui/container.go @@ -0,0 +1,51 @@ +package alui + +import ( + "opslag.de/schobers/allg5" + "opslag.de/schobers/geom" +) + +type Container struct { + ControlBase + + Children []Control +} + +func (c *Container) Handle(e allg5.Event) { + c.ControlBase.Handle(e) + for _, child := range c.Children { + child.Handle(e) + } +} + +func (c *Container) DesiredSize(ctx *Context) geom.PointF32 { + var size geom.PointF32 + for _, child := range c.Children { + s := child.DesiredSize(ctx) + if geom.IsNaN32(s.X) || geom.IsNaN32(size.X) { + size.X = geom.NaN32() + } else { + size.X = geom.Max32(s.X, size.X) + } + if geom.IsNaN32(s.Y) || geom.IsNaN32(size.Y) { + size.Y = geom.NaN32() + } else { + size.Y = geom.Max32(s.Y, size.Y) + } + } + return size +} + +func (c *Container) Layout(ctx *Context, bounds geom.RectangleF32) { + c.ControlBase.Layout(ctx, bounds) + for _, child := range c.Children { + child.Layout(ctx, bounds) + } +} + +func (c *Container) Render(ctx *Context, bounds geom.RectangleF32) { + c.ControlBase.Render(ctx, bounds) + for _, child := range c.Children { + child.Render(ctx, bounds) + } +} diff --git a/alui/context.go b/alui/context.go new file mode 100644 index 0000000..309f042 --- /dev/null +++ b/alui/context.go @@ -0,0 +1,21 @@ +package alui + +import ( + "opslag.de/schobers/allg5" + "opslag.de/schobers/geom" +) + +type Context struct { + Display *allg5.Display + Fonts *Fonts + Cursor allg5.MouseCursor + Palette Palette +} + +func (c *Context) DisplayBounds() geom.RectangleF32 { + return geom.RectF32(0, 0, float32(c.Display.Width()), float32(c.Display.Height())) +} + +type State struct { + Font string +} diff --git a/alui/control.go b/alui/control.go new file mode 100644 index 0000000..7b57930 --- /dev/null +++ b/alui/control.go @@ -0,0 +1,73 @@ +package alui + +import ( + "opslag.de/schobers/allg5" + "opslag.de/schobers/geom" +) + +type Control interface { + Bounds() geom.RectangleF32 + + Handle(allg5.Event) + + DesiredSize(*Context) geom.PointF32 + Layout(*Context, geom.RectangleF32) + Render(*Context, geom.RectangleF32) +} + +type Bounds struct { + value geom.RectangleF32 +} + +var _ Control = &ControlBase{} + +type ControlBase struct { + _Bounds geom.RectangleF32 + Over bool + Font string + Foreground *allg5.Color + Background *allg5.Color + + OnClick func() + OnEnter func() + OnLeave func() +} + +func MouseEventToPos(e allg5.MouseEvent) geom.PointF32 { return geom.PtF32(float32(e.X), float32(e.Y)) } + +func (b *ControlBase) DesiredSize(*Context) geom.PointF32 { return geom.ZeroPtF32 } + +func (b *ControlBase) Handle(e allg5.Event) { + switch e := e.(type) { + case *allg5.MouseMoveEvent: + pos := MouseEventToPos(e.MouseEvent) + over := pos.In(b._Bounds) + if over != b.Over { + b.Over = over + if over { + if b.OnEnter != nil { + b.OnEnter() + } + } else { + if b.OnLeave != nil { + b.OnLeave() + } + } + } + case *allg5.MouseButtonDownEvent: + if !b.Over { + break + } + if e.Button == allg5.MouseButtonLeft { + if b.OnClick != nil { + b.OnClick() + } + } + } +} + +func (b *ControlBase) Layout(_ *Context, bounds geom.RectangleF32) { b._Bounds = bounds } + +func (b *ControlBase) Render(*Context, geom.RectangleF32) {} + +func (b *ControlBase) Bounds() geom.RectangleF32 { return b._Bounds } diff --git a/alui/fonts.go b/alui/fonts.go new file mode 100644 index 0000000..e35bede --- /dev/null +++ b/alui/fonts.go @@ -0,0 +1,55 @@ +package alui + +import ( + "opslag.de/schobers/allg5" +) + +type Fonts struct { + fonts map[string]*allg5.Font +} + +type FontDescription struct { + Path string + Size int + Name string +} + +func NewFonts() *Fonts { + return &Fonts{map[string]*allg5.Font{}} +} + +func (f *Fonts) Destroy() { + for _, font := range f.fonts { + font.Destroy() + } + f.fonts = nil +} + +func (f *Fonts) Load(path string, size int, name string) error { + font, err := allg5.LoadTTFFont(path, size) + if err != nil { + return err + } + if old := f.fonts[name]; old != nil { + old.Destroy() + } + f.fonts[name] = font + return nil +} + +func (f *Fonts) LoadFonts(descriptions ...FontDescription) error { + for _, desc := range descriptions { + err := f.Load(desc.Path, desc.Size, desc.Name) + if err != nil { + return err + } + } + return nil +} + +func (f *Fonts) Get(name string) *allg5.Font { + if name == "" { + return f.Get("default") + } + return f.fonts[name] +} diff --git a/alui/label.go b/alui/label.go new file mode 100644 index 0000000..73d2fa3 --- /dev/null +++ b/alui/label.go @@ -0,0 +1,35 @@ +package alui + +import ( + "opslag.de/schobers/allg5" + "opslag.de/schobers/geom" +) + +var _ Control = &Label{} + +type Label struct { + ControlBase + + Text string +} + +func (l *Label) DesiredSize(ctx *Context) geom.PointF32 { + font := ctx.Fonts.Get(l.Font) + _, _, w, h := font.TextDimensions(l.Text) + return geom.PtF32(w+8, h+8) +} + +func (l *Label) Render(ctx *Context, bounds geom.RectangleF32) { + font := ctx.Fonts.Get(l.Font) + + back := l.Background + fore := l.Foreground + if fore == nil { + fore = &ctx.Palette.Text + } + + if back != nil { + allg5.DrawFilledRectangle(bounds.Min.X, bounds.Min.Y, bounds.Max.X, bounds.Max.Y, *back) + } + font.Draw(bounds.Min.X+4, bounds.Min.Y+4, *fore, allg5.AlignLeft, l.Text) +} diff --git a/alui/orientation.go b/alui/orientation.go new file mode 100644 index 0000000..0ccee98 --- /dev/null +++ b/alui/orientation.go @@ -0,0 +1,8 @@ +package alui + +type Orientation string + +const ( + OrientationVertical Orientation = "vertical" + OrientationHorizontal = "horizontal" +) diff --git a/alui/overlay.go b/alui/overlay.go new file mode 100644 index 0000000..f7ae422 --- /dev/null +++ b/alui/overlay.go @@ -0,0 +1,39 @@ +package alui + +import ( + "opslag.de/schobers/allg5" + "opslag.de/schobers/geom" +) + +type Overlay struct { + ControlBase + + Proxy Control + + Visible bool +} + +func (o *Overlay) DesiredSize(ctx *Context) geom.PointF32 { + if o.Visible { + return o.Proxy.DesiredSize(ctx) + } + return geom.ZeroPtF32 +} + +func (o *Overlay) Handle(e allg5.Event) { + if o.Visible { + o.Proxy.Handle(e) + } +} + +func (o *Overlay) Render(ctx *Context, bounds geom.RectangleF32) { + if o.Visible { + o.Proxy.Render(ctx, bounds) + } +} + +func (o *Overlay) Layout(ctx *Context, bounds geom.RectangleF32) { + if o.Visible { + o.Proxy.Layout(ctx, bounds) + } +} diff --git a/alui/palette.go b/alui/palette.go new file mode 100644 index 0000000..d171ace --- /dev/null +++ b/alui/palette.go @@ -0,0 +1,15 @@ +package alui + +import ( + "opslag.de/schobers/allg5" +) + +type Palette struct { + Primary allg5.Color + Light allg5.Color + Dark allg5.Color + Text allg5.Color + TextLight allg5.Color + Icon allg5.Color + Accent allg5.Color +} diff --git a/alui/stackpanel.go b/alui/stackpanel.go new file mode 100644 index 0000000..37bf23a --- /dev/null +++ b/alui/stackpanel.go @@ -0,0 +1,99 @@ +package alui + +import ( + "opslag.de/schobers/allg5" + "opslag.de/schobers/geom" +) + +var _ Control = &StackPanel{} + +type StackPanel struct { + Container + + Orientation Orientation +} + +func (s *StackPanel) Handle(e allg5.Event) { + s.Container.Handle(e) +} + +func (s *StackPanel) asLength(p geom.PointF32) float32 { + switch s.Orientation { + case OrientationHorizontal: + return p.X + default: + return p.Y + } +} + +func (s *StackPanel) asWidth(p geom.PointF32) float32 { + switch s.Orientation { + case OrientationHorizontal: + return p.Y + default: + return p.X + } +} + +func (s *StackPanel) asSize(length, width float32) geom.PointF32 { + switch s.Orientation { + case OrientationHorizontal: + return geom.PtF32(length, width) + default: + return geom.PtF32(width, length) + } +} + +func (s *StackPanel) CalculateLayout(ctx *Context) ([]geom.PointF32, geom.PointF32) { + desired := make([]geom.PointF32, len(s.Children)) + for i, child := range s.Children { + desired[i] = child.DesiredSize(ctx) + } + var length, width float32 + for _, size := range desired { + w, l := s.asWidth(size), s.asLength(size) + if geom.IsNaN32(w) { + width = geom.NaN32() + } else if !geom.IsNaN32(width) { + width = geom.Max32(width, w) + } + if geom.IsNaN32(l) { + panic("not implemented") + } + length += l + } + switch s.Orientation { + case OrientationHorizontal: + return desired, geom.PtF32(length, width) + default: + return desired, geom.PtF32(width, length) + } +} + +func (s *StackPanel) DesiredSize(ctx *Context) geom.PointF32 { + _, size := s.CalculateLayout(ctx) + return size +} + +func (s *StackPanel) Layout(ctx *Context, bounds geom.RectangleF32) { + s.Container.Layout(ctx, bounds) + + desired, size := s.CalculateLayout(ctx) + width := s.asWidth(size) + + var offset = bounds.Min + for i, child := range s.Children { + length := s.asLength(desired[i]) + childSize := s.asSize(length, width) + var bottomRight = offset.Add(childSize) + var childBounds = geom.RectF32(offset.X, offset.Y, bottomRight.X, bottomRight.Y) + child.Layout(ctx, childBounds) + offset = offset.Add(s.asSize(length, 0)) + } +} + +func (s *StackPanel) Render(ctx *Context, bounds geom.RectangleF32) { + for _, child := range s.Children { + child.Render(ctx, child.Bounds()) + } +} diff --git a/alui/ui.go b/alui/ui.go new file mode 100644 index 0000000..168a998 --- /dev/null +++ b/alui/ui.go @@ -0,0 +1,47 @@ +package alui + +import ( + "opslag.de/schobers/allg5" + "opslag.de/schobers/geom" +) + +type UI struct { + ctx *Context + main Control +} + +func NewUI(disp *allg5.Display, main Control) *UI { + ctx := &Context{ + Display: disp, + Fonts: NewFonts(), + Palette: Palette{}, + } + return &UI{ctx, main} +} + +func (ui *UI) Context() *Context { return ui.ctx } + +func (ui *UI) Destroy() { + ui.ctx.Fonts.Destroy() +} + +func (ui *UI) Fonts() *Fonts { return ui.ctx.Fonts } + +func (ui *UI) SetPalette(p Palette) { ui.ctx.Palette = p } + +func (ui *UI) layoutBounds(bounds geom.RectangleF32) { + ui.main.Layout(ui.ctx, bounds) +} + +func (ui *UI) Handle(e allg5.Event) { ui.main.Handle(e) } + +func (ui *UI) Render() { + ui.RenderBounds(ui.ctx.DisplayBounds()) +} + +func (ui *UI) RenderBounds(bounds geom.RectangleF32) { + ui.ctx.Cursor = allg5.MouseCursorDefault + ui.layoutBounds(bounds) + ui.main.Render(ui.ctx, bounds) + ui.ctx.Display.SetMouseCursor(ui.ctx.Cursor) +} diff --git a/cmd/krampus19/context.go b/cmd/krampus19/context.go new file mode 100644 index 0000000..7683f9d --- /dev/null +++ b/cmd/krampus19/context.go @@ -0,0 +1,18 @@ +package main + +import ( + "opslag.de/schobers/allg5" + "opslag.de/schobers/fs/vfs" +) + +type Context struct { + Resources vfs.CopyDir + Bitmaps map[string]*allg5.Bitmap + Levels map[string]level +} + +func (c *Context) Destroy() { + for _, bmp := range c.Bitmaps { + bmp.Destroy() + } +} diff --git a/cmd/krampus19/fps.go b/cmd/krampus19/fps.go new file mode 100644 index 0000000..8d850a0 --- /dev/null +++ b/cmd/krampus19/fps.go @@ -0,0 +1,60 @@ +package main + +import "time" + +type FPS struct { + done chan struct{} + cnt chan struct{} + + curr int + + total int + frame int + frames []int + i int +} + +func NewFPS() *FPS { + fps := &FPS{ + done: make(chan struct{}), + cnt: make(chan struct{}, 100), + curr: 0, // to display + total: 0, // sum of frames + frame: 0, // current frame + frames: make([]int, 20), // all frames + i: 0, // frame index + } + go fps.count() + return fps +} + +func (f *FPS) count() { + ticker := time.NewTicker(50 * time.Millisecond) + for { + select { + case <-f.done: + return + case <-f.cnt: + f.frame++ + case <-ticker.C: + f.total -= f.frames[f.i] + f.frames[f.i] = f.frame + f.total += f.frames[f.i] + f.frame = 0 + f.i = (f.i + 1) % len(f.frames) + f.curr = f.total + } + } +} + +func (f *FPS) Count() { + f.cnt <- struct{}{} +} + +func (f *FPS) Current() int { + return f.curr +} + +func (f *FPS) Destroy() { + close(f.done) +} diff --git a/cmd/krampus19/game.go b/cmd/krampus19/game.go new file mode 100644 index 0000000..f99baaa --- /dev/null +++ b/cmd/krampus19/game.go @@ -0,0 +1,142 @@ +package main + +import ( + "fmt" + "image/png" + + "opslag.de/schobers/allg5" + "opslag.de/schobers/fs/vfs" + "opslag.de/schobers/krampus19/alui" +) + +type Game struct { + ctx *Context + ui *alui.UI + main alui.Container + + info *alui.Overlay + scene *alui.Overlay +} + +func (g *Game) initUI(disp *allg5.Display, fps *FPS) error { + disp.SetWindowTitle("Krampushack '19 - Title TBD - by Tharro") + ui := alui.NewUI(disp, &g.main) + fontPath, err := g.ctx.Resources.Retrieve("OpenSans-Regular.ttf") + if err != nil { + return err + } + err = ui.Fonts().LoadFonts(alui.FontDescription{Path: fontPath, Name: "default", Size: 12}) + if err != nil { + return err + } + g.ui = ui + g.info = &alui.Overlay{Proxy: &info{fps: fps}} + g.scene = &alui.Overlay{Proxy: &playScene{}, Visible: true} + g.main.Children = append(g.main.Children, g.scene, g.info) + return nil +} + +func (g *Game) loadBitmap(path, name string) error { + f, err := g.ctx.Resources.Open(path) + if err != nil { + return err + } + defer f.Close() + im, err := png.Decode(f) + if err != nil { + return err + } + bmp, err := allg5.NewBitmapFromImage(im, true) + if err != nil { + return err + } + g.ctx.Bitmaps[name] = bmp + return nil +} + +func (g *Game) loadBitmaps(pathToName map[string]string) error { + for path, name := range pathToName { + err := g.loadBitmap(path, name) + if err != nil { + return err + } + } + return nil +} + +func (g *Game) loadLevels(names ...string) error { + g.ctx.Levels = map[string]level{} + for _, name := range names { + fileName := fmt.Sprintf("levels/level%s.txt", name) + f, err := g.ctx.Resources.Open(fileName) + if err != nil { + return err + } + defer f.Close() + level, err := loadLevelAsset(f) + if err != nil { + return err + } + g.ctx.Levels[name] = level + } + return nil +} + +func (g *Game) loadAssets() error { + err := g.loadBitmaps(map[string]string{ + "basic_tile.png": "basic_tile", + "water_tile.png": "water_tile", + + "main_character.png": "main_character", + "villain_character.png": "villain_character", + + "brick.png": "brick", + "crate.png": "crate", + }) + if err != nil { + return err + } + + err = g.loadLevels("1") + if err != nil { + return err + } + return nil +} + +func (g *Game) PlayScene() *playScene { + return g.scene.Proxy.(*playScene) +} + +func (g *Game) Destroy() { + g.ui.Destroy() + g.ctx.Destroy() +} + +func (g *Game) Init(disp *allg5.Display, res vfs.CopyDir, fps *FPS) error { + g.ctx = &Context{Resources: res, Bitmaps: map[string]*allg5.Bitmap{}} + if err := g.initUI(disp, fps); err != nil { + return err + } + if err := g.loadAssets(); err != nil { + return err + } + scene := g.PlayScene() + scene.init(g.ctx) + scene.loadLevel("1") + return nil +} + +func (g *Game) Handle(e allg5.Event) { + switch e := e.(type) { + case *allg5.KeyDownEvent: + if e.KeyCode == allg5.KeyF3 { + g.info.Visible = !g.info.Visible + } + } + g.ui.Handle(e) +} + +func (g *Game) Render() { + g.ui.Render() +} diff --git a/cmd/krampus19/info.go b/cmd/krampus19/info.go new file mode 100644 index 0000000..571ca26 --- /dev/null +++ b/cmd/krampus19/info.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + + "opslag.de/schobers/allg5" + "opslag.de/schobers/geom" + "opslag.de/schobers/krampus19/alui" +) + +type info struct { + alui.ControlBase + + fps *FPS +} + +func (i *info) Render(ctx *alui.Context, bounds geom.RectangleF32) { + ctx.Fonts.Get("default").Draw(4, 4, allg5.NewColor(0xff, 0xff, 0xff), allg5.AlignLeft, fmt.Sprintf("FPS: %d", i.fps.Current())) +} diff --git a/cmd/krampus19/krampus19.go b/cmd/krampus19/krampus19.go new file mode 100644 index 0000000..ab6de05 --- /dev/null +++ b/cmd/krampus19/krampus19.go @@ -0,0 +1,87 @@ +package main + +import ( + "log" + + rice "github.com/GeertJohan/go.rice" + "github.com/spf13/afero" + "opslag.de/schobers/allg5" + "opslag.de/schobers/fs/ricefs" + "opslag.de/schobers/fs/vfs" +) + +//go:generate rice embed-syso + +func main() { + err := run() + if err != nil { + log.Fatal(err) + } +} + +func resources() (vfs.CopyDir, error) { + var embeddedFs = ricefs.NewFs(rice.MustFindBox("res")) + var osFs = afero.NewBasePathFs(afero.NewOsFs(), "./res") + return vfs.NewCopyDir(vfs.NewFallbackFs(osFs, embeddedFs)) +} + +func run() error { + err := allg5.Init(allg5.InitAll) + if err != nil { + return err + } + + disp, err := allg5.NewDisplay(1440, 900, allg5.NewDisplayOptions{Maximized: false, Windowed: true, Resizable: true, Vsync: true}) + if err != nil { + return err + } + defer disp.Destroy() + + eq, err := allg5.NewEventQueue() + if err != nil { + return err + } + defer eq.Destroy() + + eq.RegisterDisplay(disp) + eq.RegisterKeyboard() + eq.RegisterMouse() + + res, err := resources() + if err != nil { + return err + } + defer res.Destroy() + + fps := NewFPS() + defer fps.Destroy() + + game := &Game{} + err = game.Init(disp, res, fps) + if err != nil { + return err + } + defer game.Destroy() + + back := allg5.NewColor(0x21, 0x21, 0x21) + for { + allg5.ClearToColor(back) + game.Render() + disp.Flip() + fps.Count() + + e := eq.Get() + for e != nil { + switch e := e.(type) { + case *allg5.DisplayCloseEvent: + return nil + case *allg5.KeyDownEvent: + if e.KeyCode == allg5.KeyEscape { + return nil + } + } + game.Handle(e) + e = eq.Get() + } + } +} diff --git a/cmd/krampus19/level.go b/cmd/krampus19/level.go new file mode 100644 index 0000000..8a80f32 --- /dev/null +++ b/cmd/krampus19/level.go @@ -0,0 +1,142 @@ +package main + +import ( + "errors" + "fmt" + "io" +) + +type entity byte +type tile byte + +const ( + entityInvalid entity = entity(0) + entityNone = '_' + entityCharacter = '@' + entityVillain = 'X' + entityBrick = 'B' + entityCrate = 'C' +) + +func (e entity) IsValid() bool { + switch e { + case entityNone: + case entityCharacter: + case entityVillain: + case entityBrick: + case entityCrate: + default: + return false + } + return true +} + +const ( + tileInvalid tile = tile(0) + tileNothing = '.' + tileBasic = '#' + tileWater = '~' +) + +func (t tile) IsValid() bool { + switch t { + case tileNothing: + case tileBasic: + case tileWater: + default: + return false + } + return true +} + +type level struct { + width int + height int + tiles []tile + entities []entity +} + +func loadLevelAsset(r io.Reader) (level, error) { + var l level + ctx := levelContext{&l, nil} + err := parseLines(r, ctx.parse) + if err != nil { + return level{}, err + } + if ctx.err != nil { + return level{}, ctx.err + } + return l, nil +} + +type levelContext struct { + level *level + err error +} + +func (c *levelContext) emitErr(err error) parseLineFn { + c.err = err + return nil +} + +func (c *levelContext) parse(p *lineParser) parseLineFn { + if p.eof() { + return nil + } + switch p.peek() { + case "level:": + return c.parseContent + case "": + p.next() // skip + return c.parse + default: + return nil + } +} + +func (c *levelContext) parseContent(p *lineParser) parseLineFn { + if p.next() != "level:" { + return c.emitErr(errors.New("expected level start")) + } + return c.parseRow +} + +func (c *levelContext) parseRow(p *lineParser) parseLineFn { + if p.eof() { + return c.emitErr(errors.New("unexpected end of file")) + } + line := p.next() + if line == ":level" { + return c.parse + } + if c.level.height == 0 { + c.level.width = len(line) / 2 + } + return c.addRow(line) +} + +func (c *levelContext) addRow(line string) parseLineFn { + var tiles []tile + var entities []entity + for i := 0; i < len(line); i += 2 { + tiles = append(tiles, tile(line[i])) + entities = append(entities, entity(line[i+1])) + } + + for i, t := range tiles { + if !t.IsValid() { + return c.emitErr(fmt.Errorf("level contains invalid tile at (%d, %d)", i, c.level.height)) + } + } + for i, e := range entities { + if !e.IsValid() { + return c.emitErr(fmt.Errorf("level contains invalid entity at (%d, %d)", i, c.level.height)) + } + } + + c.level.height++ + c.level.tiles = append(c.level.tiles, tiles...) + c.level.entities = append(c.level.entities, entities...) + + return c.parseRow +} diff --git a/cmd/krampus19/lineparser.go b/cmd/krampus19/lineparser.go new file mode 100644 index 0000000..d75cb39 --- /dev/null +++ b/cmd/krampus19/lineparser.go @@ -0,0 +1,39 @@ +package main + +import ( + "io" + "io/ioutil" + "strings" +) + +type lineParser struct { + lines []string + i int +} + +func (p *lineParser) eof() bool { return p.i == len(p.lines) } +func (p *lineParser) peek() string { return p.lines[p.i] } +func (p *lineParser) next() string { + i := p.i + p.i++ + return p.lines[i] +} + +type parseLineFn func(p *lineParser) parseLineFn + +func parseLines(r io.Reader, fn parseLineFn) error { + content, err := ioutil.ReadAll(r) + if err != nil { + return err + } + lines := strings.Split(string(content), "\n") + for i, line := range lines { + lines[i] = strings.TrimRight(line, "\r\n") + } + + parser := &lineParser{lines: lines} + for fn != nil { + fn = fn(parser) + } + return nil +} diff --git a/cmd/krampus19/mainmenu.go b/cmd/krampus19/mainmenu.go new file mode 100644 index 0000000..41c5a68 --- /dev/null +++ b/cmd/krampus19/mainmenu.go @@ -0,0 +1,3 @@ +package main + +type mainMenu struct{} diff --git a/cmd/krampus19/playscene.go b/cmd/krampus19/playscene.go new file mode 100644 index 0000000..327cc9e --- /dev/null +++ b/cmd/krampus19/playscene.go @@ -0,0 +1,122 @@ +package main + +import ( + "opslag.de/schobers/allg5" + "opslag.de/schobers/geom" + "opslag.de/schobers/krampus19/alui" +) + +type playScene struct { + alui.ControlBase + + ctx *Context + level level + + offset geom.PointF32 + scale float32 +} + +func (s *playScene) init(ctx *Context) { + s.ctx = ctx +} + +func (s *playScene) loadLevel(name string) { + s.level = s.ctx.Levels[name] +} + +func (s *playScene) toScreenPos(p geom.Point) geom.PointF32 { + pos := geom.PtF32(float32(p.X)+.5, float32(p.Y)+.5) + pos = geom.PtF32(pos.X*148-pos.Y*46, pos.Y*82) + pos = geom.PtF32(pos.X*s.scale, pos.Y*s.scale) + return pos.Add(s.offset) +} + +// <- 168-> +// <-128-> +// +// /----/| ^ ^ +// / / / | 72 +// /----/ / 92 v ^ +// |----|/ v v 20 +// +// Gap: 20 +// Center: 84,46 +// Offset between horizontal tiles: 148,0 +// Offset between vertical tiles: -46,82 + +func (s *playScene) Layout(ctx *alui.Context, bounds geom.RectangleF32) { + s.scale = bounds.Dy() / 1080 + + tilesCenter := geom.PtF32(.5*float32(s.level.width), .5*float32(s.level.height)) + tilesCenter = geom.PtF32(tilesCenter.X*148-tilesCenter.Y*46, tilesCenter.Y*82) + tilesCenter = geom.PtF32(tilesCenter.X*s.scale, tilesCenter.Y*s.scale) + center := bounds.Center() + s.offset = geom.PtF32(center.X-tilesCenter.X, center.Y-tilesCenter.Y) +} + +func (s *playScene) Render(ctx *alui.Context, bounds geom.RectangleF32) { + basicTile := s.ctx.Bitmaps["basic_tile"] + waterTile := s.ctx.Bitmaps["water_tile"] + tileBmp := func(t tile) *allg5.Bitmap { + switch t { + case tileBasic: + return basicTile + case tileWater: + return waterTile + default: + return nil + } + } + + character := s.ctx.Bitmaps["main_character"] + villain := s.ctx.Bitmaps["villain_character"] + brick := s.ctx.Bitmaps["brick"] + crate := s.ctx.Bitmaps["crate"] + entityBmp := func(e entity) *allg5.Bitmap { + switch e { + case entityCharacter: + return character + case entityVillain: + return villain + case entityBrick: + return brick + case entityCrate: + return crate + default: + return nil + } + } + + scale := 168 / float32(basicTile.Width()) + + // center := disp.Center() + + level := s.level + for i, t := range level.tiles { + tile := tileBmp(t) + if tile == nil { + continue + } + pos := geom.Pt(i%level.width, i/level.width) + screenPos := s.toScreenPos(pos) + if t == tileWater { + screenPos.Y += 8 * s.scale + } + tile.DrawOptions(screenPos.X, screenPos.Y, allg5.DrawOptions{Center: true, Scale: allg5.NewUniformScale(s.scale * scale)}) + } + + for i, e := range level.entities { + bmp := entityBmp(e) + if bmp == nil { + continue + } + pos := geom.Pt(i%level.width, i/level.width) + screenPos := s.toScreenPos(pos) + screenPos.Y -= 48 * s.scale + scale := scale + if e == entityCharacter { + scale *= .4 + } + bmp.DrawOptions(screenPos.X, screenPos.Y, allg5.DrawOptions{Center: true, Scale: allg5.NewUniformScale(s.scale * scale)}) + } +} diff --git a/cmd/krampus19/res/OpenSans-Regular.ttf b/cmd/krampus19/res/OpenSans-Regular.ttf new file mode 100644 index 0000000..2e31d02 Binary files /dev/null and b/cmd/krampus19/res/OpenSans-Regular.ttf differ diff --git a/cmd/krampus19/res/basic_tile.png b/cmd/krampus19/res/basic_tile.png new file mode 100644 index 0000000..26c4be4 Binary files /dev/null and b/cmd/krampus19/res/basic_tile.png differ diff --git a/cmd/krampus19/res/brick.png b/cmd/krampus19/res/brick.png new file mode 100644 index 0000000..d6d4a3d Binary files /dev/null and b/cmd/krampus19/res/brick.png differ diff --git a/cmd/krampus19/res/crate.png b/cmd/krampus19/res/crate.png new file mode 100644 index 0000000..f38e4f5 Binary files /dev/null and b/cmd/krampus19/res/crate.png differ diff --git a/cmd/krampus19/res/levels/level1.txt b/cmd/krampus19/res/levels/level1.txt new file mode 100644 index 0000000..e88f242 --- /dev/null +++ b/cmd/krampus19/res/levels/level1.txt @@ -0,0 +1,11 @@ +level: +._._._._._._._._._._ +._._#_#_#_._._._._._ +._._#_._#_._._._._._ +._._#_#_#_._._._._._ +._._#_._#B._._._._._ +._#@#_#_#_~_~_#_#X._ +._._._._~_~_~_#_._._ +._._._#_#_#_#_#_._._ +._._._._._._._._._._ +:level \ No newline at end of file diff --git a/cmd/krampus19/res/main_character.png b/cmd/krampus19/res/main_character.png new file mode 100644 index 0000000..1ba2b59 Binary files /dev/null and b/cmd/krampus19/res/main_character.png differ diff --git a/cmd/krampus19/res/villain_character.png b/cmd/krampus19/res/villain_character.png new file mode 100644 index 0000000..4e6a836 Binary files /dev/null and b/cmd/krampus19/res/villain_character.png differ diff --git a/cmd/krampus19/res/water_tile.png b/cmd/krampus19/res/water_tile.png new file mode 100644 index 0000000..6a16faa Binary files /dev/null and b/cmd/krampus19/res/water_tile.png differ diff --git a/cmd/krampus19/splash.go b/cmd/krampus19/splash.go new file mode 100644 index 0000000..ecbabe6 --- /dev/null +++ b/cmd/krampus19/splash.go @@ -0,0 +1,7 @@ +package main + +import "opslag.de/schobers/krampus19/alui" + +type splash struct { + alui.Container +}