SemiSolid Pixels + Icons

* Add new pixel attributes: SemiSolid and Slippery (the latter is WIP)
* SemiSolid pixels are only solid below the player character. You can walk on
  them and up and down SemiSolid slopes, but can freely pass through from the
  sides or jump through from below.
* Update the Palette Editor UI to replace the Attributes buttons: instead of
  text labels they now have smaller icons (w/ tooltips) for the Solid,
  SemiSolid, Fire, Water and Slippery attributes.
* Bugfix in Palette Editor: use cropped (24x24) images for the Tex buttons so
  that the large Bubbles texture stays within its designated space!
* uix.Actor.SetGrounded() to also set the Y velocity to zero when an actor
  becomes grounded. This fixes a minor bug where the player's Y velocity (due
  to gravity) was not updated while they were grounded, which may eventually
  become useful to allow them to jump down thru a SemiSolid floor. Warp Doors
  needed a fix to work around the bug, to set the player's Grounded(false) or
  else they would hover a few pixels above the ground at their destination,
  since Grounded status paused gravity calculations.
pull/84/head
Noah 2022-10-09 21:39:43 -07:00
parent 701073cecc
commit ecaa8c6cef
11 changed files with 139 additions and 37 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 997 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 792 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

View File

@ -21,6 +21,13 @@ var (
PencilIcon = "assets/sprites/pencil.png" PencilIcon = "assets/sprites/pencil.png"
FloodCursor = "assets/sprites/flood-cursor.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 // Title Screen Font
TitleScreenFont = render.Text{ TitleScreenFont = render.Text{
Size: 46, Size: 46,

View File

@ -94,10 +94,10 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli
ceiling = true ceiling = true
P.Y++ P.Y++
} }
if result.Left { if result.Left && !result.LeftPixel.SemiSolid {
P.X++ P.X++
} }
if result.Right { if result.Right && !result.RightPixel.SemiSolid {
P.X-- P.X--
} }
} }
@ -134,7 +134,11 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli
target.X++ // push along to the right target.X++ // push along to the right
} }
} else { } 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. // 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 hitLeft = true
capLeft = result.LeftPoint.X // + 1 capLeft = result.LeftPoint.X
// 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).
} }
if result.Right && !hitRight { if result.Right && !hitRight && !result.RightPixel.SemiSolid {
hitRight = true hitRight = true
capRight = result.RightPoint.X - S.W 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.Bottom = true
result.MoveTo.Y = capFloor result.MoveTo.Y = capFloor
} }
if hitLeft { if hitLeft && !result.LeftPixel.SemiSolid {
result.Left = true result.Left = true
result.MoveTo.X = capLeft result.MoveTo.X = capLeft
} }
if hitRight { if hitRight && !result.RightPixel.SemiSolid {
result.Right = true result.Right = true
result.MoveTo.X = capRight 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. // IsColliding returns whether any sort of collision has occurred.
func (c *Collide) IsColliding() bool { 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 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. // 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 continue
} }

View File

@ -14,9 +14,11 @@ type Swatch struct {
Pattern string `json:"pattern"` // like "noise.png" Pattern string `json:"pattern"` // like "noise.png"
// Optional attributes. // Optional attributes.
Solid bool `json:"solid,omitempty"` Solid bool `json:"solid,omitempty"`
Fire bool `json:"fire,omitempty"` SemiSolid bool `json:"semisolid,omitempty"`
Water bool `json:"water,omitempty"` Fire bool `json:"fire,omitempty"`
Water bool `json:"water,omitempty"`
Slippery bool `json:"slippery,omitempty"`
// Private runtime attributes. // Private runtime attributes.
index int // position in the Palette, for reverse of `Palette.byName` index int // position in the Palette, for reverse of `Palette.byName`

View File

@ -4,6 +4,7 @@ package pattern
import ( import (
"errors" "errors"
"fmt" "fmt"
"image"
"git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/sprites" "git.kirsle.net/SketchyMaze/doodle/pkg/sprites"
@ -63,10 +64,14 @@ var Builtins = []Pattern{
// after LoadBuiltins had been called. // after LoadBuiltins had been called.
var images map[string]*ui.Image 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 // LoadBuiltins loads all of the PNG textures of built-in patterns
// into ui.Image widgets. // into ui.Image widgets.
func LoadBuiltins(e render.Engine) { func LoadBuiltins(e render.Engine) {
images = map[string]*ui.Image{} images = map[string]*ui.Image{}
croppedImages = map[string]map[render.Rect]*ui.Image{}
for _, pat := range Builtins { for _, pat := range Builtins {
if pat.Filename == "" { 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) 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. // 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 { func SampleColor(filename string, color render.Color, point render.Point) render.Color {
if filename == "" { if filename == "" {
@ -236,3 +269,17 @@ func OverlayFilter(a, b render.Color) render.Color {
return render.RGBA(deltaR, deltaG, deltaB, a.Alpha) 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))
}

View File

@ -197,9 +197,12 @@ func (a *Actor) Grounded() bool {
return a.grounded 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) { func (a *Actor) SetGrounded(v bool) {
a.grounded = v a.grounded = v
if v {
a.velocity.Y = 0
}
} }
// Hide makes the actor invisible. // Hide makes the actor invisible.

View File

@ -10,6 +10,7 @@ import (
"git.kirsle.net/SketchyMaze/doodle/pkg/modal" "git.kirsle.net/SketchyMaze/doodle/pkg/modal"
"git.kirsle.net/SketchyMaze/doodle/pkg/pattern" "git.kirsle.net/SketchyMaze/doodle/pkg/pattern"
"git.kirsle.net/SketchyMaze/doodle/pkg/shmem" "git.kirsle.net/SketchyMaze/doodle/pkg/shmem"
"git.kirsle.net/SketchyMaze/doodle/pkg/sprites"
"git.kirsle.net/SketchyMaze/doodle/pkg/usercfg" "git.kirsle.net/SketchyMaze/doodle/pkg/usercfg"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"git.kirsle.net/go/ui" "git.kirsle.net/go/ui"
@ -119,10 +120,12 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window {
// Draw the main table of Palette rows. // Draw the main table of Palette rows.
if pal := config.EditPalette; pal != nil { if pal := config.EditPalette; pal != nil {
for i, swatch := range pal.Swatches { for i, swatch := range pal.Swatches {
i := i var (
i = i
swatch = swatch
)
var idStr = fmt.Sprintf("%d", i) var idStr = fmt.Sprintf("%d", i)
swatch := swatch
row := ui.NewFrame("Swatch " + idStr) row := ui.NewFrame("Swatch " + idStr)
rows = append(rows, row) rows = append(rows, row)
@ -269,20 +272,34 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window {
Height: 24, Height: 24,
}) })
attributes := []struct { attributes := []struct {
Label string Label string
Var *bool Sprite string
Var *bool
}{ }{
{ {
Label: "Solid", Label: "Solid",
Var: &swatch.Solid, Sprite: balance.AttrSolid,
Var: &swatch.Solid,
}, },
{ {
Label: "Fire", Label: "Semi-Solid",
Var: &swatch.Fire, Sprite: balance.AttrSemiSolid,
Var: &swatch.SemiSolid,
}, },
{ {
Label: "Water", Label: "Fire",
Var: &swatch.Water, 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 { if !config.IsDoodad {
for _, attr := range attributes { for _, attr := range attributes {
attr := attr attr := attr
btn := ui.NewCheckButton(attr.Label, attr.Var, ui.NewLabel(ui.Label{ var child ui.Widget
Text: attr.Label,
Font: balance.MenuFont, 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 { btn.Handle(ui.Click, func(ed ui.EventData) error {
if config.OnChange != nil { if config.OnChange != nil {
config.OnChange() config.OnChange()
@ -301,8 +328,16 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window {
return nil return nil
}) })
config.Supervisor.Add(btn) 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{ attrFrame.Pack(btn, ui.Pack{
Side: ui.W, 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 // show the image by its filename... for both onChange and
// initial render needs. // initial render needs.
func setImageOnSelect(sel *ui.SelectBox, filename string) { func setImageOnSelect(sel *ui.SelectBox, filename string) {
if image, err := pattern.GetImage(filename); err == nil { if img, err := pattern.GetImageCropped(filename, render.NewRect(24, 24)); err == nil {
sel.SetImage(image) sel.SetImage(img)
} else { } else {
sel.SetImage(nil) sel.SetImage(nil)
} }