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.
This commit is contained in:
parent
701073cecc
commit
ecaa8c6cef
BIN
assets/sprites/attr-fire.png
Normal file
BIN
assets/sprites/attr-fire.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 997 B |
BIN
assets/sprites/attr-semisolid.png
Normal file
BIN
assets/sprites/attr-semisolid.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 645 B |
BIN
assets/sprites/attr-slippery.png
Normal file
BIN
assets/sprites/attr-slippery.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 690 B |
BIN
assets/sprites/attr-solid.png
Normal file
BIN
assets/sprites/attr-solid.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 792 B |
BIN
assets/sprites/attr-water.png
Normal file
BIN
assets/sprites/attr-water.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 962 B |
|
@ -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,
|
||||||
|
|
|
@ -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,9 +134,13 @@ 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 {
|
||||||
|
// 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
|
target.X = P.X
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cap our vertical movement if we're touching ceilings.
|
// Cap our vertical movement if we're touching ceilings.
|
||||||
if ceiling {
|
if ceiling {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,10 @@ type Swatch struct {
|
||||||
|
|
||||||
// Optional attributes.
|
// Optional attributes.
|
||||||
Solid bool `json:"solid,omitempty"`
|
Solid bool `json:"solid,omitempty"`
|
||||||
|
SemiSolid bool `json:"semisolid,omitempty"`
|
||||||
Fire bool `json:"fire,omitempty"`
|
Fire bool `json:"fire,omitempty"`
|
||||||
Water bool `json:"water,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`
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -270,30 +273,54 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window {
|
||||||
})
|
})
|
||||||
attributes := []struct {
|
attributes := []struct {
|
||||||
Label string
|
Label string
|
||||||
|
Sprite string
|
||||||
Var *bool
|
Var *bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Label: "Solid",
|
Label: "Solid",
|
||||||
|
Sprite: balance.AttrSolid,
|
||||||
Var: &swatch.Solid,
|
Var: &swatch.Solid,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Label: "Semi-Solid",
|
||||||
|
Sprite: balance.AttrSemiSolid,
|
||||||
|
Var: &swatch.SemiSolid,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Label: "Fire",
|
Label: "Fire",
|
||||||
|
Sprite: balance.AttrFire,
|
||||||
Var: &swatch.Fire,
|
Var: &swatch.Fire,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Label: "Water",
|
Label: "Water",
|
||||||
|
Sprite: balance.AttrWater,
|
||||||
Var: &swatch.Water,
|
Var: &swatch.Water,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Label: "Slippery",
|
||||||
|
Sprite: balance.AttrSlippery,
|
||||||
|
Var: &swatch.Slippery,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not show in Doodad editing mode.
|
// Do not show in Doodad editing mode.
|
||||||
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
|
||||||
|
|
||||||
|
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,
|
Text: attr.Label,
|
||||||
Font: balance.MenuFont,
|
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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user