From ecaa8c6cef77a15c81e28b4cc0e631ad6c3047e6 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sun, 9 Oct 2022 21:39:43 -0700 Subject: [PATCH] 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. --- assets/sprites/attr-fire.png | Bin 0 -> 997 bytes assets/sprites/attr-semisolid.png | Bin 0 -> 645 bytes assets/sprites/attr-slippery.png | Bin 0 -> 690 bytes assets/sprites/attr-solid.png | Bin 0 -> 792 bytes assets/sprites/attr-water.png | Bin 0 -> 962 bytes pkg/balance/theme.go | 7 ++++ pkg/collision/collide_level.go | 42 +++++++++++-------- pkg/level/swatch.go | 8 ++-- pkg/pattern/pattern.go | 47 +++++++++++++++++++++ pkg/uix/actor.go | 5 ++- pkg/windows/palette_editor.go | 67 +++++++++++++++++++++++------- 11 files changed, 139 insertions(+), 37 deletions(-) create mode 100644 assets/sprites/attr-fire.png create mode 100644 assets/sprites/attr-semisolid.png create mode 100644 assets/sprites/attr-slippery.png create mode 100644 assets/sprites/attr-solid.png create mode 100644 assets/sprites/attr-water.png diff --git a/assets/sprites/attr-fire.png b/assets/sprites/attr-fire.png new file mode 100644 index 0000000000000000000000000000000000000000..f8b43ba0e308e764b6664db758e392fe29c6edad GIT binary patch literal 997 zcmVEX>4Tx04R}tkv&MmKpe$iQ$;Nm2aAX}WT;LSL`5963Pq?8YK2xEOfLO`CJjl7 zi=*ILaPVWX>fqw6tAnc`2!4P#Iyou2NQwVT3N2zhIPS;0dyl(!fKV?p&FUBjG~G5+ ziMW`_u8Li+2%!f92t$;aWz0!Z629Z>9s$1I#dwzgxj#p*nzI-X5Q%4*VcNtS#M7I$ z!FiuJ!ius=d`>)O(glehxvqHp#<}3Kz%wIeIyFxmAr=d5th6yJni}yGaa7fG$`>*o ztDLtuYvn3y-jlyDoYPm9xlVH!2`pj>5=1DdqJ%PR#Aww?v5=zuxQ~Cx^-JVZ$W;O( z#{w$QAiI9>Klt6Pm7kpOlEQJI^TlyKMu4tepiy(2?_(i|qi@Or{kK49&FihXkJASrOL!@$)JQ<8600006VoOIv0RI600RN!9r;`8x010qNS#tmY zE+YT{E+YYWr9XB6000McNliru<_Zb~Aqm@#uVDZH02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00F&8L_t(I%cay$Z%knn$MMgd8LCZ2MNR$NAtKm_#DbND z1r3reEd+^p0TxzXfrXt!NV@Y9u#gNk?8ZhW(jo{kkqV*1MCw?~Gj%)dOoyK2$<4j_ z{l51+&pGE|zzDJ+TS-?M05pmU*5M_7_}>UB!PVe-@G!v#G7beBmic};_!6{&Ye8`t zO)>d1D5URWxELJC3CA#j_1d8Q818~?#X0PfKi{;qo5ADYSo-T^@Fs|~J_qN!fER*~ z!9*v+PEqF3!eLxT9Xqil=O4wDU?ZMk9A~i^C-4-rf;GXZ;B79OUh8)-8@veK1wV7y zp9zKpdxClr$0}Ae*i_Iy*c}FF+7)6S))w#zzpOrnkrh%g-uIPO1_FM-d?^8I*u4mF z5OZlGdvO={`vPldmC(dJoWuzDq}}4 z!CW8ET+&P?&q5Dp%Uxw@K9Ax!4q#iZHEO8% TkxL-z00000NkvXXu0mjfNmI85 literal 0 HcmV?d00001 diff --git a/assets/sprites/attr-semisolid.png b/assets/sprites/attr-semisolid.png new file mode 100644 index 0000000000000000000000000000000000000000..8c4c853bb9f455adb3510ca03abf7509a7a75e06 GIT binary patch literal 645 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 literal 0 HcmV?d00001 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) }