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"
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,

View File

@ -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
}

View File

@ -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`

View File

@ -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))
}

View File

@ -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.

View File

@ -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)
}