diff --git a/assets/sprites/attr-fire.png b/assets/sprites/attr-fire.png new file mode 100644 index 0000000..f8b43ba Binary files /dev/null and b/assets/sprites/attr-fire.png differ diff --git a/assets/sprites/attr-semisolid.png b/assets/sprites/attr-semisolid.png new file mode 100644 index 0000000..8c4c853 Binary files /dev/null and b/assets/sprites/attr-semisolid.png differ diff --git a/assets/sprites/attr-slippery.png b/assets/sprites/attr-slippery.png new file mode 100644 index 0000000..02be794 Binary files /dev/null and b/assets/sprites/attr-slippery.png differ diff --git a/assets/sprites/attr-solid.png b/assets/sprites/attr-solid.png new file mode 100644 index 0000000..4aa8c64 Binary files /dev/null and b/assets/sprites/attr-solid.png differ diff --git a/assets/sprites/attr-water.png b/assets/sprites/attr-water.png new file mode 100644 index 0000000..e62b4e3 Binary files /dev/null and b/assets/sprites/attr-water.png differ diff --git a/pkg/balance/theme.go b/pkg/balance/theme.go index a39bf94..0239cbe 100644 --- a/pkg/balance/theme.go +++ b/pkg/balance/theme.go @@ -21,6 +21,13 @@ var ( PencilIcon = "assets/sprites/pencil.png" FloodCursor = "assets/sprites/flood-cursor.png" + // Pixel attributes + AttrSolid = "assets/sprites/attr-solid.png" + AttrFire = "assets/sprites/attr-fire.png" + AttrWater = "assets/sprites/attr-water.png" + AttrSemiSolid = "assets/sprites/attr-semisolid.png" + AttrSlippery = "assets/sprites/attr-slippery.png" + // Title Screen Font TitleScreenFont = render.Text{ Size: 46, diff --git a/pkg/collision/collide_level.go b/pkg/collision/collide_level.go index b06a503..c26b1e7 100644 --- a/pkg/collision/collide_level.go +++ b/pkg/collision/collide_level.go @@ -94,10 +94,10 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli ceiling = true P.Y++ } - if result.Left { + if result.Left && !result.LeftPixel.SemiSolid { P.X++ } - if result.Right { + if result.Right && !result.RightPixel.SemiSolid { P.X-- } } @@ -134,7 +134,11 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli target.X++ // push along to the right } } else { - target.X = P.X + // Not a slope.. may be a solid wall. If the wall is a SemiSolid though, + // do not cap our direction just yet. + if !(result.Left && result.LeftPixel.SemiSolid) && !(result.Right && result.RightPixel.SemiSolid) { + target.X = P.X + } } } @@ -188,18 +192,16 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli // Similar to the "+ 1" on the left side, below. } - if result.Left && !hitLeft { + // TODO: this block of code is interesting. For SemiSolid slopes, the character + // walks up the slopes FAST (full speed) which is nice; would like to do this + // for regular solid slopes too. But if this block of code is dummied out for + // solid walls, the player is able to clip thru thin walls (couple px thick); the + // capLeft/capRight behavior is good at stopping the player here. + if result.Left && !hitLeft && !result.LeftPixel.SemiSolid { hitLeft = true - capLeft = result.LeftPoint.X // + 1 - - // TODO: there was a clipping bug where the player could clip - // thru a left wall if they jumped slightly while pressing into - // it. (90 degree angle between floor and left wall). The bug - // does NOT repro on right walls, only left. The "+ 1" added to - // capLeft works around it, BUT breaks walking up leftward slopes - // (walking up rightward slopes still works). + capLeft = result.LeftPoint.X } - if result.Right && !hitRight { + if result.Right && !hitRight && !result.RightPixel.SemiSolid { hitRight = true capRight = result.RightPoint.X - S.W } @@ -219,11 +221,11 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli result.Bottom = true result.MoveTo.Y = capFloor } - if hitLeft { + if hitLeft && !result.LeftPixel.SemiSolid { result.Left = true result.MoveTo.X = capLeft } - if hitRight { + if hitRight && !result.RightPixel.SemiSolid { result.Right = true result.MoveTo.X = capRight } @@ -233,7 +235,7 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli // IsColliding returns whether any sort of collision has occurred. func (c *Collide) IsColliding() bool { - return c.Top || c.Bottom || c.Left || c.Right || + return c.Top || c.Bottom || (c.Left && !c.LeftPixel.SemiSolid) || (c.Right && !c.RightPixel.SemiSolid) || c.InFire != "" || c.InWater } @@ -295,7 +297,13 @@ func (c *Collide) ScanGridLine(p1, p2 render.Point, grid *level.Chunker, side Si } // Non-solid swatches don't collide so don't pay them attention. - if !swatch.Solid { + if !swatch.Solid && !swatch.SemiSolid { + continue + } + + // A semisolid only has collision on the bottom (and a little on the + // sides, for slope walking only) + if swatch.SemiSolid && side == Top { continue } diff --git a/pkg/level/swatch.go b/pkg/level/swatch.go index 5640a0e..ed6077a 100644 --- a/pkg/level/swatch.go +++ b/pkg/level/swatch.go @@ -14,9 +14,11 @@ type Swatch struct { Pattern string `json:"pattern"` // like "noise.png" // Optional attributes. - Solid bool `json:"solid,omitempty"` - Fire bool `json:"fire,omitempty"` - Water bool `json:"water,omitempty"` + Solid bool `json:"solid,omitempty"` + SemiSolid bool `json:"semisolid,omitempty"` + Fire bool `json:"fire,omitempty"` + Water bool `json:"water,omitempty"` + Slippery bool `json:"slippery,omitempty"` // Private runtime attributes. index int // position in the Palette, for reverse of `Palette.byName` diff --git a/pkg/pattern/pattern.go b/pkg/pattern/pattern.go index 34ecbbc..026da59 100644 --- a/pkg/pattern/pattern.go +++ b/pkg/pattern/pattern.go @@ -4,6 +4,7 @@ package pattern import ( "errors" "fmt" + "image" "git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/SketchyMaze/doodle/pkg/sprites" @@ -63,10 +64,14 @@ var Builtins = []Pattern{ // after LoadBuiltins had been called. var images map[string]*ui.Image +// Cache of cropped images (e.g. 24x24 icons for palette editor) +var croppedImages map[string]map[render.Rect]*ui.Image + // LoadBuiltins loads all of the PNG textures of built-in patterns // into ui.Image widgets. func LoadBuiltins(e render.Engine) { images = map[string]*ui.Image{} + croppedImages = map[string]map[render.Rect]*ui.Image{} for _, pat := range Builtins { if pat.Filename == "" { @@ -93,6 +98,34 @@ func GetImage(filename string) (*ui.Image, error) { return nil, fmt.Errorf("pattern.GetImage: filename %s not found", filename) } +// GetImageCropped gets a cropped ui.Image for a builtin pattern. +func GetImageCropped(filename string, crop render.Rect) (*ui.Image, error) { + // Have it cached already? + if sizes, ok := croppedImages[filename]; ok { + if cached, ok := sizes[crop]; ok { + return cached, nil + } + } + + uiImage, err := GetImage(filename) + if err != nil { + return nil, err + } + + cropped, err := CropImage(uiImage.Image, image.Rect(0, 0, crop.W, crop.H)) + if err != nil { + return nil, err + } + + // Cache it for the future so we don't leak textures every time the palette editor asks. + if _, ok := croppedImages[filename]; !ok { + croppedImages[filename] = map[render.Rect]*ui.Image{} + } + croppedImages[filename][crop] = cropped + + return cropped, nil +} + // SampleColor samples a color with the pattern for a given coordinate in infinite space. func SampleColor(filename string, color render.Color, point render.Point) render.Color { if filename == "" { @@ -236,3 +269,17 @@ func OverlayFilter(a, b render.Color) render.Color { return render.RGBA(deltaR, deltaG, deltaB, a.Alpha) } + +// CropImage crops an image to a smaller size (such as the 24x24 selectbox button in the Palette Editor UI) +func CropImage(img image.Image, crop image.Rectangle) (*ui.Image, error) { + type subImager interface { + SubImage(r image.Rectangle) image.Image + } + + simg, ok := img.(subImager) + if !ok { + return nil, fmt.Errorf("image does not support cropping") + } + + return ui.ImageFromImage(simg.SubImage(crop)) +} diff --git a/pkg/uix/actor.go b/pkg/uix/actor.go index 86d9829..c503b8e 100644 --- a/pkg/uix/actor.go +++ b/pkg/uix/actor.go @@ -197,9 +197,12 @@ func (a *Actor) Grounded() bool { return a.grounded } -// SetGrounded sets the actor's grounded value. +// SetGrounded sets the actor's grounded value. If true, also sets their Y velocity to zero. func (a *Actor) SetGrounded(v bool) { a.grounded = v + if v { + a.velocity.Y = 0 + } } // Hide makes the actor invisible. diff --git a/pkg/windows/palette_editor.go b/pkg/windows/palette_editor.go index dc40ecf..53c0961 100644 --- a/pkg/windows/palette_editor.go +++ b/pkg/windows/palette_editor.go @@ -10,6 +10,7 @@ import ( "git.kirsle.net/SketchyMaze/doodle/pkg/modal" "git.kirsle.net/SketchyMaze/doodle/pkg/pattern" "git.kirsle.net/SketchyMaze/doodle/pkg/shmem" + "git.kirsle.net/SketchyMaze/doodle/pkg/sprites" "git.kirsle.net/SketchyMaze/doodle/pkg/usercfg" "git.kirsle.net/go/render" "git.kirsle.net/go/ui" @@ -119,10 +120,12 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window { // Draw the main table of Palette rows. if pal := config.EditPalette; pal != nil { for i, swatch := range pal.Swatches { - i := i + var ( + i = i + swatch = swatch + ) var idStr = fmt.Sprintf("%d", i) - swatch := swatch row := ui.NewFrame("Swatch " + idStr) rows = append(rows, row) @@ -269,20 +272,34 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window { Height: 24, }) attributes := []struct { - Label string - Var *bool + Label string + Sprite string + Var *bool }{ { - Label: "Solid", - Var: &swatch.Solid, + Label: "Solid", + Sprite: balance.AttrSolid, + Var: &swatch.Solid, }, { - Label: "Fire", - Var: &swatch.Fire, + Label: "Semi-Solid", + Sprite: balance.AttrSemiSolid, + Var: &swatch.SemiSolid, }, { - Label: "Water", - Var: &swatch.Water, + Label: "Fire", + Sprite: balance.AttrFire, + Var: &swatch.Fire, + }, + { + Label: "Water", + Sprite: balance.AttrWater, + Var: &swatch.Water, + }, + { + Label: "Slippery", + Sprite: balance.AttrSlippery, + Var: &swatch.Slippery, }, } @@ -290,10 +307,20 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window { if !config.IsDoodad { for _, attr := range attributes { attr := attr - btn := ui.NewCheckButton(attr.Label, attr.Var, ui.NewLabel(ui.Label{ - Text: attr.Label, - Font: balance.MenuFont, - })) + var child ui.Widget + + icon, err := sprites.LoadImage(config.Engine, attr.Sprite) + if err != nil { + log.Error("Sprite loading error: %s", err) + child = ui.NewLabel(ui.Label{ + Text: attr.Label, + Font: balance.MenuFont, + }) + } else { + child = icon + } + + btn := ui.NewCheckButton(attr.Label, attr.Var, child) btn.Handle(ui.Click, func(ed ui.EventData) error { if config.OnChange != nil { config.OnChange() @@ -301,8 +328,16 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window { return nil }) config.Supervisor.Add(btn) + + tt := ui.NewTooltip(btn, ui.Tooltip{ + Text: attr.Label, + Edge: ui.Bottom, + }) + tt.Supervise(config.Supervisor) + attrFrame.Pack(btn, ui.Pack{ Side: ui.W, + PadX: 1, }) } } @@ -440,8 +475,8 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window { // show the image by its filename... for both onChange and // initial render needs. func setImageOnSelect(sel *ui.SelectBox, filename string) { - if image, err := pattern.GetImage(filename); err == nil { - sel.SetImage(image) + if img, err := pattern.GetImageCropped(filename, render.NewRect(24, 24)); err == nil { + sel.SetImage(img) } else { sel.SetImage(nil) }