diff --git a/assets/pattern/bubbles.png b/assets/pattern/bubbles.png new file mode 100644 index 0000000..647fe0a Binary files /dev/null and b/assets/pattern/bubbles.png differ diff --git a/dev-assets/doodads/azulian/azulian.js b/dev-assets/doodads/azulian/azulian.js index 2c77b6d..73e0943 100644 --- a/dev-assets/doodads/azulian/azulian.js +++ b/dev-assets/doodads/azulian/azulian.js @@ -2,9 +2,11 @@ const color = Self.GetTag("color"); var playerSpeed = color === 'blue' ? 2 : 4, + swimSpeed = playerSpeed * 0.4, aggroX = 250, // X/Y distance sensitivity from player aggroY = color === 'blue' ? 100 : 200, jumpSpeed = color === 'blue' ? 14 : 18, + swimJumpSpeed = jumpSpeed * 0.4, animating = false, direction = "right", lastDirection = "right"; @@ -14,7 +16,9 @@ if (color === 'white') { aggroX = 1000; aggroY = 400; playerSpeed = 8; + swimSpeed = playerSpeed * 0.4; jumpSpeed = 20; + swimJumpSpeed = jumpSpeed * 0.4; } function setupAnimations(color) { @@ -30,6 +34,9 @@ function setupAnimations(color) { function main() { playerSpeed = color === 'blue' ? 2 : 4; + let swimJumpCooldownTick = 0, // minimum Game Tick before we can jump while swimming + swimJumpCooldown = 10; // CONFIG: delta of ticks between jumps while swimming + Self.SetMobile(true); Self.SetGravity(true); Self.SetInventory(true); @@ -95,8 +102,25 @@ function main() { sampleTick++; } - let Vx = parseFloat(playerSpeed * (direction === "left" ? -1 : 1)), - Vy = jump && Self.Grounded() ? parseFloat(-jumpSpeed) : Self.GetVelocity().Y; + // Handle being underwater. + let canJump = Self.Grounded(); + if (Self.IsWet()) { + let tick = GetTick(); + if (tick > swimJumpCooldownTick) { + canJump = true; + swimJumpCooldownTick = tick + swimJumpCooldown; + } + } + + // How speedy would our movement and jump be? + let xspeed = playerSpeed, yspeed = jumpSpeed; + if (Self.IsWet()) { + xspeed = swimSpeed; + yspeed = swimJumpSpeed; + } + + let Vx = parseFloat(xspeed * (direction === "left" ? -1 : 1)), + Vy = jump && canJump ? parseFloat(-yspeed) : Self.GetVelocity().Y; Self.SetVelocity(Vector(Vx, Vy)); // If we changed directions, stop animating now so we can diff --git a/pkg/balance/numbers.go b/pkg/balance/numbers.go index 15bb536..3407103 100644 --- a/pkg/balance/numbers.go +++ b/pkg/balance/numbers.go @@ -41,7 +41,10 @@ var ( PlayerAcceleration float64 = 0.12 Gravity float64 = 7 GravityAcceleration float64 = 0.1 - SlopeMaxHeight = 8 // max pixel height for player to walk up a slope + SwimGravity float64 = 3 + SwimJumpVelocity float64 = -12 + SwimJumpCooldown uint64 = 24 // number of frames of cooldown between swim-jumps + SlopeMaxHeight = 8 // max pixel height for player to walk up a slope // Default chunk size for canvases. ChunkSize = 128 diff --git a/pkg/collision/collide_level.go b/pkg/collision/collide_level.go index 33d7ae0..3350f51 100644 --- a/pkg/collision/collide_level.go +++ b/pkg/collision/collide_level.go @@ -35,6 +35,7 @@ func (c *Collide) Reset() { c.Left = false c.Right = false c.Bottom = false + c.InWater = false } // Side of the collision box (top, bottom, left, right) @@ -85,8 +86,6 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli if result.Bottom { if !d.Grounded() { d.SetGrounded(true) - } else { - // result.Bottom = false } } else { d.SetGrounded(false) @@ -250,10 +249,10 @@ func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Chunker) bool { side Side } jobs := []jobSide{ // We'll scan each side of the bounding box in parallel - jobSide{col.Top[0], col.Top[1], Top}, - jobSide{col.Bottom[0], col.Bottom[1], Bottom}, - jobSide{col.Left[0], col.Left[1], Left}, - jobSide{col.Right[0], col.Right[1], Right}, + {col.Top[0], col.Top[1], Top}, + {col.Bottom[0], col.Bottom[1], Bottom}, + {col.Left[0], col.Left[1], Left}, + {col.Right[0], col.Right[1], Right}, } var wg sync.WaitGroup diff --git a/pkg/editor_scene.go b/pkg/editor_scene.go index 80daa11..4e86b49 100644 --- a/pkg/editor_scene.go +++ b/pkg/editor_scene.go @@ -225,7 +225,11 @@ func (s *EditorScene) setupAsync(d *Doodle) error { // Scroll the level to the remembered position from when we went // to Play Mode and back. If no remembered position, this is zero // anyway. - s.UI.Canvas.ScrollTo(s.RememberScrollPosition) + if s.RememberScrollPosition.IsZero() && s.Level != nil { + s.UI.Canvas.ScrollTo(s.Level.ScrollPosition) + } else { + s.UI.Canvas.ScrollTo(s.RememberScrollPosition) + } d.Flash("Editor Mode.") if s.DrawingType == enum.LevelDrawing { @@ -514,6 +518,9 @@ func (s *EditorScene) SaveLevel(filename string) error { m.Palette = s.UI.Canvas.Palette m.Chunker = s.UI.Canvas.Chunker() + // Store the scroll position. + m.ScrollPosition = s.UI.Canvas.Scroll + // Clear the modified flag on the level. s.UI.Canvas.SetModified(false) diff --git a/pkg/level/palette_defaults.go b/pkg/level/palette_defaults.go index f913501..aa2bce0 100644 --- a/pkg/level/palette_defaults.go +++ b/pkg/level/palette_defaults.go @@ -37,7 +37,7 @@ var ( Name: "water", Color: render.MustHexColor("#09F"), Water: true, - Pattern: "ink.png", + Pattern: "bubbles.png", }, { Name: "hint", @@ -89,7 +89,7 @@ var ( Name: "water", Color: render.RGBA(0, 153, 255, 255), Water: true, - Pattern: "ink.png", + Pattern: "bubbles.png", }, { Name: "hint", @@ -126,7 +126,7 @@ var ( { Name: "water", Color: render.MustHexColor("#09F"), - Pattern: "ink.png", + Pattern: "bubbles.png", }, { Name: "hint", @@ -159,7 +159,7 @@ var ( Name: "water", Color: render.RGBA(0, 153, 255, 255), Water: true, - Pattern: "ink.png", + Pattern: "bubbles.png", }, { Name: "electric", diff --git a/pkg/level/types.go b/pkg/level/types.go index a0de0f4..7ff8639 100644 --- a/pkg/level/types.go +++ b/pkg/level/types.go @@ -57,6 +57,9 @@ type Level struct { MaxHeight int64 `json:"boundedHeight"` Wallpaper string `json:"wallpaper"` + // The last scrolled position in the editor. + ScrollPosition render.Point `json:"scroll"` + // Actors keep a list of the doodad instances in this map. Actors ActorMap `json:"actors"` diff --git a/pkg/pattern/pattern.go b/pkg/pattern/pattern.go index c8cadbc..b33c284 100644 --- a/pkg/pattern/pattern.go +++ b/pkg/pattern/pattern.go @@ -42,6 +42,10 @@ var Builtins = []Pattern{ }, { Name: "Bubbles", + Filename: "bubbles.png", + }, + { + Name: "Circles", Filename: "circles.png", }, { diff --git a/pkg/play_scene.go b/pkg/play_scene.go index 58ff74b..5d531f3 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -74,6 +74,7 @@ type PlayScene struct { godMode bool // Cheat: player can't die godModeUntil time.Time // Invulnerability timer at respawn. playerJumpCounter int // limit jump length + jumpCooldownUntil uint64 // future game tick for jump cooldown (swimming esp.) // Inventory HUD. Impl. in play_inventory.go invenFrame *ui.Frame @@ -211,6 +212,8 @@ func (s *PlayScene) setupAsync(d *Doodle) error { // Handler when an actor touches water or fire. s.drawing.OnLevelCollision = func(a *uix.Actor, col *collision.Collide) { + a.SetWet(col.InWater) + if col.InFire != "" { a.Canvas.MaskColor = render.Black if a.ID() == "PLAYER" { // only the player dies in fire. @@ -472,8 +475,7 @@ func (s *PlayScene) installPlayerDoodad(filename string, spawn render.Point, cen // Set up the movement physics for the player. s.playerPhysics = &physics.Mover{ - MaxSpeed: physics.NewVector(balance.PlayerMaxVelocity, balance.PlayerMaxVelocity), - // Gravity: physics.NewVector(balance.Gravity, balance.Gravity), + MaxSpeed: physics.NewVector(balance.PlayerMaxVelocity, balance.PlayerMaxVelocity), Acceleration: 0.025, Friction: 0.1, } @@ -804,111 +806,6 @@ func (s *PlayScene) Draw(d *Doodle) error { return nil } -// movePlayer updates the player's X,Y coordinate based on key pressed. -func (s *PlayScene) movePlayer(ev *event.State) { - var ( - playerSpeed = float64(balance.PlayerMaxVelocity) - velocity = s.Player.Velocity() - direction float64 - jumping bool - ) - - // Antigravity: player can move anywhere with arrow keys. - if s.antigravity || !s.Player.HasGravity() { - velocity.X = 0 - velocity.Y = 0 - - // Shift to slow your roll to 1 pixel per tick. - if keybind.Shift(ev) { - playerSpeed = 1 - } - - if keybind.Left(ev) { - velocity.X = -playerSpeed - } else if keybind.Right(ev) { - velocity.X = playerSpeed - } - if keybind.Up(ev) { - velocity.Y = -playerSpeed - } else if keybind.Down(ev) { - velocity.Y = playerSpeed - } - } else { - // Moving left or right. - if keybind.Left(ev) { - direction = -1 - } else if keybind.Right(ev) { - direction = 1 - } - - // Up button to signal they want to jump. - if keybind.Up(ev) { - if s.Player.Grounded() { - velocity.Y = balance.PlayerJumpVelocity - } - } else if velocity.Y < 0 { - velocity.Y = 0 - } - // if keybind.Up(ev) && (s.Player.Grounded() || s.playerJumpCounter >= 0) { - // jumping = true - - // if s.Player.Grounded() { - // // Allow them to sustain the jump this many ticks. - // s.playerJumpCounter = 32 - // } - // } - - // Moving left or right? Interpolate their velocity by acceleration. - if direction != 0 { - if s.playerLastDirection != direction { - velocity.X = 0 - } - - // TODO: fast turn-around if they change directions so they don't - // slip and slide while their velocity updates. - velocity.X = physics.Lerp( - velocity.X, - direction*s.playerPhysics.MaxSpeed.X, - s.playerPhysics.Acceleration, - ) - } else { - // Slow them back to zero using friction. - velocity.X = physics.Lerp( - velocity.X, - 0, - s.playerPhysics.Friction, - ) - } - - // Moving upwards (jumping): give them full acceleration upwards. - if jumping { - velocity.Y = -playerSpeed - } - - // While in the air, count down their jump counter; when zero they - // cannot jump again until they touch ground. - if !s.Player.Grounded() { - s.playerJumpCounter-- - } - } - - s.playerLastDirection = direction - - // Move the player unless frozen. - // TODO: if Y=0 then gravity fails, but not doing this allows the - // player to jump while frozen. Not a HUGE deal right now as only Warp Doors - // freeze the player currently but do address this later. - if s.Player.IsFrozen() { - velocity.X = 0 - } - s.Player.SetVelocity(velocity) - - // If the "Use" key is pressed, set an actor flag on the player. - s.Player.SetUsing(keybind.Use(ev)) - - s.scripting.To(s.Player.ID()).Events.RunKeypress(keybind.FromEvent(ev)) -} - // Drawing returns the private world drawing, for debugging with the console. func (s *PlayScene) Drawing() *uix.Canvas { return s.drawing diff --git a/pkg/player_physics.go b/pkg/player_physics.go new file mode 100644 index 0000000..561d375 --- /dev/null +++ b/pkg/player_physics.go @@ -0,0 +1,119 @@ +package doodle + +// Subset of the PlayScene that is responsible for movement of the player character. + +import ( + "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/keybind" + "git.kirsle.net/apps/doodle/pkg/physics" + "git.kirsle.net/apps/doodle/pkg/shmem" + "git.kirsle.net/go/render/event" +) + +// movePlayer updates the player's X,Y coordinate based on key pressed. +func (s *PlayScene) movePlayer(ev *event.State) { + var ( + playerSpeed = float64(balance.PlayerMaxVelocity) + velocity = s.Player.Velocity() + direction float64 + jumping bool + // holdingJump bool // holding down the jump button vs. tapping it + ) + + // Antigravity: player can move anywhere with arrow keys. + if s.antigravity || !s.Player.HasGravity() { + velocity.X = 0 + velocity.Y = 0 + + // Shift to slow your roll to 1 pixel per tick. + if keybind.Shift(ev) { + playerSpeed = 1 + } + + if keybind.Left(ev) { + velocity.X = -playerSpeed + } else if keybind.Right(ev) { + velocity.X = playerSpeed + } + if keybind.Up(ev) { + velocity.Y = -playerSpeed + } else if keybind.Down(ev) { + velocity.Y = playerSpeed + } + } else { + // Moving left or right. + if keybind.Left(ev) { + direction = -1 + } else if keybind.Right(ev) { + direction = 1 + } + + // Up button to signal they want to jump. + if keybind.Up(ev) { + if s.Player.IsWet() { + // If they are holding Up put a cooldown in how fast they can swim + // to the surface. Tapping the Jump button allows a faster ascent. + if shmem.Tick > s.jumpCooldownUntil { + s.jumpCooldownUntil = shmem.Tick + balance.SwimJumpCooldown + velocity.Y = balance.SwimJumpVelocity + } + } else if s.Player.Grounded() { + velocity.Y = balance.PlayerJumpVelocity + } + } else { + s.jumpCooldownUntil = 0 + if velocity.Y < 0 { + velocity.Y = 0 + } + } + + // Moving left or right? Interpolate their velocity by acceleration. + if direction != 0 { + if s.playerLastDirection != direction { + velocity.X = 0 + } + + // TODO: fast turn-around if they change directions so they don't + // slip and slide while their velocity updates. + velocity.X = physics.Lerp( + velocity.X, + direction*s.playerPhysics.MaxSpeed.X, + s.playerPhysics.Acceleration, + ) + } else { + // Slow them back to zero using friction. + velocity.X = physics.Lerp( + velocity.X, + 0, + s.playerPhysics.Friction, + ) + } + + // Moving upwards (jumping): give them full acceleration upwards. + if jumping { + velocity.Y = -playerSpeed + } + + // While in the air, count down their jump counter; when zero they + // cannot jump again until they touch ground. + if !s.Player.Grounded() { + s.playerJumpCounter-- + } + } + + s.playerLastDirection = direction + + // Move the player unless frozen. + // TODO: if Y=0 then gravity fails, but not doing this allows the + // player to jump while frozen. Not a HUGE deal right now as only Warp Doors + // freeze the player currently but do address this later. + if s.Player.IsFrozen() { + velocity.X = 0 + } + s.Player.SetVelocity(velocity) + + // If the "Use" key is pressed, set an actor flag on the player. + s.Player.SetUsing(keybind.Use(ev)) + + s.scripting.To(s.Player.ID()).Events.RunKeypress(keybind.FromEvent(ev)) +} diff --git a/pkg/uix/actor.go b/pkg/uix/actor.go index 6c13ecc..8d49af9 100644 --- a/pkg/uix/actor.go +++ b/pkg/uix/actor.go @@ -36,6 +36,7 @@ type Actor struct { // Actor runtime variables. hasGravity bool hasInventory bool + wet bool isMobile bool // Mobile character, such as the player or an enemy noclip bool // Disable collision detection hidden bool // invisible, via Hide() and Show() @@ -152,6 +153,16 @@ func (a *Actor) SetInvulnerable(v bool) { a.immortal = v } +// Wet returns whether the actor is in contact with water pixels in a level. +func (a *Actor) IsWet() bool { + return a.wet +} + +// SetWet updates the state of the actor's wet-ness. +func (a *Actor) SetWet(v bool) { + a.wet = v +} + // Size returns the size of the actor, from the underlying doodads.Drawing. func (a *Actor) Size() render.Rect { return a.Drawing.Size() diff --git a/pkg/uix/actor_collision.go b/pkg/uix/actor_collision.go index 5348f67..22c1ca8 100644 --- a/pkg/uix/actor_collision.go +++ b/pkg/uix/actor_collision.go @@ -73,9 +73,13 @@ func (w *Canvas) loopActorCollision() error { // Apply gravity to the actor's velocity. if a.hasGravity && !a.Grounded() { //v.Y >= 0 { if !a.Grounded() { + var gravity = balance.Gravity + if a.IsWet() { + gravity = balance.SwimGravity + } v.Y = physics.Lerp( - v.Y, // current speed - balance.Gravity, // target max gravity falling downwards + v.Y, // current speed + gravity, // target max gravity falling downwards balance.GravityAcceleration, ) } else { @@ -98,12 +102,11 @@ func (w *Canvas) loopActorCollision() error { // Check collision with level geometry. chkPoint := delta.ToPoint() - info, ok := collision.CollidesWithGrid(a, w.chunks, chkPoint) - if ok { - // Collision happened with world. - if w.OnLevelCollision != nil { - w.OnLevelCollision(a, info) - } + info, _ := collision.CollidesWithGrid(a, w.chunks, chkPoint) + + // Inform the caller about the collision state every tick + if w.OnLevelCollision != nil { + w.OnLevelCollision(a, info) } // Move us back where the collision check put us diff --git a/pkg/uix/scripting.go b/pkg/uix/scripting.go index 1ecddb7..2b33a2d 100644 --- a/pkg/uix/scripting.go +++ b/pkg/uix/scripting.go @@ -122,6 +122,7 @@ func (w *Canvas) MakeSelfAPI(actor *Actor) map[string]interface{} { "Destroy": actor.Destroy, "Freeze": actor.Freeze, "Unfreeze": actor.Unfreeze, + "IsWet": actor.IsWet, "Hide": actor.Hide, "Show": actor.Show, "GetLinks": func() []map[string]interface{} {