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"
|
||||
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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user