2018-06-21 02:00:46 +00:00
|
|
|
package doodle
|
|
|
|
|
|
|
|
import (
|
2018-09-25 16:40:34 +00:00
|
|
|
"fmt"
|
2021-10-05 02:51:31 +00:00
|
|
|
"time"
|
2018-09-25 16:40:34 +00:00
|
|
|
|
2019-04-10 00:35:44 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
Add Switches, Fire/Water Collision and Play Menu
* New doodads: Switches.
* They come in four varieties: wall switch (background element, with
"ON/OFF" text) and three side-profile switches for the floor, left
or right walls.
* On collision with the player, they flip their state from "OFF" to
"ON" or vice versa. If the player walks away and then collides
again, the switch flips again.
* Can be used to open/close Electric Doors when turned on/off. Their
default state is "off"
* If a switch receives a power signal from another linked switch, it
sets its own state to match. So, two "on/off" switches that are
connected to a door AND to each other will both flip on/off when one
of them flips.
* Update the Level Collision logic to support Decoration, Fire and Water
pixel collisions.
* Previously, ALL pixels in the level were acting as though solid.
* Non-solid pixels don't count for collision detection, but their
attributes (fire and water) are collected and returned.
* Updated the MenuScene to support loading a map file in Play Mode
instead of Edit Mode. Updated the title screen menu to add a button
for playing levels instead of editing them.
* Wrote some documentation.
2019-07-07 01:30:03 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/collision"
|
2019-05-02 01:30:30 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/doodads"
|
2022-02-20 02:25:36 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/gamepad"
|
2020-11-18 02:22:48 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/keybind"
|
2019-04-10 00:35:44 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/level"
|
2021-12-27 04:48:29 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/levelpack"
|
2019-04-10 00:35:44 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/log"
|
2021-08-16 03:17:53 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/modal"
|
2021-07-19 03:04:24 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/modal/loadscreen"
|
2020-04-05 04:00:32 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/physics"
|
2022-01-03 00:28:43 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/savegame"
|
2019-04-16 06:07:15 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/scripting"
|
2022-01-03 00:28:43 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/sprites"
|
2019-04-10 00:35:44 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/uix"
|
2019-12-28 03:16:34 +00:00
|
|
|
"git.kirsle.net/go/render"
|
|
|
|
"git.kirsle.net/go/render/event"
|
|
|
|
"git.kirsle.net/go/ui"
|
2018-06-21 02:00:46 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// PlayScene manages the "Edit Level" game mode.
|
|
|
|
type PlayScene struct {
|
2018-07-24 03:10:53 +00:00
|
|
|
// Configuration attributes.
|
2021-10-05 03:49:11 +00:00
|
|
|
Filename string
|
|
|
|
Level *level.Level
|
|
|
|
CanEdit bool // i.e. you came from the Editor Mode
|
|
|
|
HasNext bool // has a next level to load next
|
|
|
|
RememberScrollPosition render.Point // for the Editor quality of life
|
2021-10-05 05:02:00 +00:00
|
|
|
SpawnPoint render.Point // if not zero, overrides Start Flag
|
2018-07-24 03:10:53 +00:00
|
|
|
|
2021-12-27 04:48:29 +00:00
|
|
|
// If this level was part of a levelpack. The Play Scene will read it
|
|
|
|
// from the levelpack ZIP file in priority over any other location.
|
|
|
|
LevelPack *levelpack.LevelPack
|
|
|
|
|
2018-07-24 03:10:53 +00:00
|
|
|
// Private variables.
|
2021-08-16 00:01:18 +00:00
|
|
|
d *Doodle
|
|
|
|
drawing *uix.Canvas
|
|
|
|
scripting *scripting.Supervisor
|
|
|
|
running bool
|
|
|
|
deathBarrier int // Y position of death barrier in case of falling OOB.
|
2018-06-21 02:00:46 +00:00
|
|
|
|
2022-01-03 00:28:43 +00:00
|
|
|
// Score variables.
|
|
|
|
startTime time.Time // wallclock time when level begins
|
|
|
|
perfectRun bool // set false on first respawn
|
|
|
|
cheated bool // user has entered a cheat code while playing
|
|
|
|
|
2019-06-26 01:36:53 +00:00
|
|
|
// UI widgets.
|
2022-01-03 06:36:32 +00:00
|
|
|
supervisor *ui.Supervisor
|
|
|
|
screen *ui.Frame // A window sized invisible frame to position UI elements.
|
|
|
|
menubar *ui.MenuBar
|
|
|
|
editButton *ui.Button
|
|
|
|
winLevelPacks *ui.Window
|
2019-06-26 01:36:53 +00:00
|
|
|
|
2019-04-10 01:28:08 +00:00
|
|
|
// Custom debug labels.
|
|
|
|
debPosition *string
|
|
|
|
debViewport *string
|
|
|
|
debScroll *string
|
|
|
|
debWorldIndex *string
|
|
|
|
|
2018-07-24 03:10:53 +00:00
|
|
|
// Player character
|
2021-10-08 01:24:18 +00:00
|
|
|
Player *uix.Actor
|
|
|
|
playerPhysics *physics.Mover
|
|
|
|
lastCheckpoint render.Point
|
2022-01-19 02:32:15 +00:00
|
|
|
playerLastDirection float64 // player's heading last tick
|
|
|
|
antigravity bool // Cheat: disable player gravity
|
|
|
|
noclip bool // Cheat: disable player clipping
|
|
|
|
godMode bool // Cheat: player can't die
|
|
|
|
godModeUntil time.Time // Invulnerability timer at respawn.
|
|
|
|
playerJumpCounter int // limit jump length
|
2020-04-03 06:09:46 +00:00
|
|
|
|
|
|
|
// Inventory HUD. Impl. in play_inventory.go
|
|
|
|
invenFrame *ui.Frame
|
|
|
|
invenItems []string // item list
|
|
|
|
invenDoodads map[string]*uix.Canvas
|
2021-10-05 02:51:31 +00:00
|
|
|
|
2022-01-03 00:28:43 +00:00
|
|
|
// Elapsed Time frame.
|
|
|
|
timerFrame *ui.Frame
|
|
|
|
timerPerfectImage *ui.Image
|
|
|
|
timerImperfectImage *ui.Image
|
|
|
|
timerLabel *ui.Label
|
|
|
|
|
2021-10-05 02:51:31 +00:00
|
|
|
// Touchscreen controls state.
|
|
|
|
isTouching bool
|
|
|
|
playerIsIdle bool // LoopTouchable watches for inactivity on input controls.
|
|
|
|
idleLastStart time.Time
|
|
|
|
idleHelpAlpha int // fade in UI hints
|
2018-06-21 02:00:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Name of the scene.
|
|
|
|
func (s *PlayScene) Name() string {
|
|
|
|
return "Play"
|
|
|
|
}
|
|
|
|
|
|
|
|
// Setup the play scene.
|
|
|
|
func (s *PlayScene) Setup(d *Doodle) error {
|
2018-10-28 05:22:13 +00:00
|
|
|
s.d = d
|
2019-04-16 06:07:15 +00:00
|
|
|
s.scripting = scripting.NewSupervisor()
|
2019-06-26 01:36:53 +00:00
|
|
|
s.supervisor = ui.NewSupervisor()
|
2019-04-10 01:28:08 +00:00
|
|
|
|
2021-07-19 03:04:24 +00:00
|
|
|
// Show the loading screen.
|
|
|
|
loadscreen.ShowWithProgress()
|
|
|
|
go func() {
|
|
|
|
if err := s.setupAsync(d); err != nil {
|
|
|
|
log.Error("PlayScene.setupAsync: %s", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
loadscreen.Hide()
|
|
|
|
}()
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// setupAsync initializes the play screen in the background, underneath
|
|
|
|
// a Loading screen.
|
|
|
|
func (s *PlayScene) setupAsync(d *Doodle) error {
|
2020-04-03 06:09:46 +00:00
|
|
|
// Create an invisible 'screen' frame for UI elements to use for positioning.
|
|
|
|
s.screen = ui.NewFrame("Screen")
|
|
|
|
s.screen.Resize(render.NewRect(d.width, d.height))
|
|
|
|
|
2022-01-03 06:36:32 +00:00
|
|
|
// Menu Bar
|
|
|
|
s.menubar = s.setupMenuBar(d)
|
|
|
|
s.screen.Pack(s.menubar, ui.Pack{
|
|
|
|
Side: ui.N,
|
|
|
|
FillX: true,
|
|
|
|
})
|
|
|
|
|
2019-07-02 22:24:46 +00:00
|
|
|
// Level Exit handler.
|
2021-08-16 03:17:53 +00:00
|
|
|
s.scripting.OnLevelExit(s.BeatLevel)
|
|
|
|
s.scripting.OnLevelFail(s.FailLevel)
|
|
|
|
s.scripting.OnSetCheckpoint(s.SetCheckpoint)
|
2019-07-02 22:24:46 +00:00
|
|
|
|
2019-04-10 01:28:08 +00:00
|
|
|
// Initialize debug overlay values.
|
|
|
|
s.debPosition = new(string)
|
|
|
|
s.debViewport = new(string)
|
|
|
|
s.debScroll = new(string)
|
|
|
|
s.debWorldIndex = new(string)
|
|
|
|
customDebugLabels = []debugLabel{
|
|
|
|
{"Pixel:", s.debWorldIndex},
|
|
|
|
{"Player:", s.debPosition},
|
|
|
|
{"Viewport:", s.debViewport},
|
|
|
|
{"Scroll:", s.debScroll},
|
|
|
|
}
|
|
|
|
|
2019-06-26 01:36:53 +00:00
|
|
|
// Initialize the "Edit Map" button.
|
|
|
|
s.editButton = ui.NewButton("Edit", ui.NewLabel(ui.Label{
|
|
|
|
Text: "Edit (E)",
|
|
|
|
Font: balance.PlayButtonFont,
|
|
|
|
}))
|
2020-04-07 06:21:17 +00:00
|
|
|
s.editButton.Handle(ui.Click, func(ed ui.EventData) error {
|
2019-06-26 01:36:53 +00:00
|
|
|
s.EditLevel()
|
2020-04-07 06:21:17 +00:00
|
|
|
return nil
|
2019-06-26 01:36:53 +00:00
|
|
|
})
|
|
|
|
s.supervisor.Add(s.editButton)
|
|
|
|
|
2020-04-03 06:09:46 +00:00
|
|
|
// Set up the inventory HUD.
|
|
|
|
s.setupInventoryHud()
|
|
|
|
|
2022-01-03 00:28:43 +00:00
|
|
|
// Set up the elapsed time frame.
|
|
|
|
{
|
|
|
|
s.timerFrame = ui.NewFrame("Elapsed Timer")
|
|
|
|
|
|
|
|
// Set the gold and silver images.
|
|
|
|
gold, _ := sprites.LoadImage(s.d.Engine, balance.GoldCoin)
|
|
|
|
silver, _ := sprites.LoadImage(s.d.Engine, balance.SilverCoin)
|
|
|
|
s.timerPerfectImage = gold
|
|
|
|
s.timerImperfectImage = silver
|
|
|
|
s.timerLabel = ui.NewLabel(ui.Label{
|
|
|
|
Text: "00:00",
|
|
|
|
Font: balance.TimerFont,
|
|
|
|
})
|
|
|
|
|
|
|
|
if s.timerPerfectImage != nil {
|
|
|
|
s.timerFrame.Pack(s.timerPerfectImage, ui.Pack{
|
|
|
|
Side: ui.W,
|
|
|
|
PadX: 2,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if s.timerImperfectImage != nil {
|
|
|
|
s.timerFrame.Pack(s.timerImperfectImage, ui.Pack{
|
|
|
|
Side: ui.W,
|
|
|
|
PadX: 2,
|
|
|
|
})
|
|
|
|
s.timerImperfectImage.Hide()
|
|
|
|
}
|
|
|
|
|
|
|
|
s.timerFrame.Pack(s.timerLabel, ui.Pack{
|
|
|
|
Side: ui.W,
|
|
|
|
PadX: 2,
|
|
|
|
})
|
|
|
|
|
|
|
|
s.screen.Place(s.timerFrame, ui.Place{
|
|
|
|
Top: 40,
|
|
|
|
Left: 40,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-04-10 01:28:08 +00:00
|
|
|
// Initialize the drawing canvas.
|
2018-09-26 17:04:46 +00:00
|
|
|
s.drawing = uix.NewCanvas(balance.ChunkSize, false)
|
2019-04-14 22:25:03 +00:00
|
|
|
s.drawing.Name = "play-canvas"
|
2018-09-25 16:40:34 +00:00
|
|
|
s.drawing.MoveTo(render.Origin)
|
2019-12-28 03:16:34 +00:00
|
|
|
s.drawing.Resize(render.NewRect(d.width, d.height))
|
2018-09-25 16:40:34 +00:00
|
|
|
s.drawing.Compute(d.Engine)
|
2018-07-24 03:10:53 +00:00
|
|
|
|
Add Switches, Fire/Water Collision and Play Menu
* New doodads: Switches.
* They come in four varieties: wall switch (background element, with
"ON/OFF" text) and three side-profile switches for the floor, left
or right walls.
* On collision with the player, they flip their state from "OFF" to
"ON" or vice versa. If the player walks away and then collides
again, the switch flips again.
* Can be used to open/close Electric Doors when turned on/off. Their
default state is "off"
* If a switch receives a power signal from another linked switch, it
sets its own state to match. So, two "on/off" switches that are
connected to a door AND to each other will both flip on/off when one
of them flips.
* Update the Level Collision logic to support Decoration, Fire and Water
pixel collisions.
* Previously, ALL pixels in the level were acting as though solid.
* Non-solid pixels don't count for collision detection, but their
attributes (fire and water) are collected and returned.
* Updated the MenuScene to support loading a map file in Play Mode
instead of Edit Mode. Updated the title screen menu to add a button
for playing levels instead of editing them.
* Wrote some documentation.
2019-07-07 01:30:03 +00:00
|
|
|
// Handler when an actor touches water or fire.
|
|
|
|
s.drawing.OnLevelCollision = func(a *uix.Actor, col *collision.Collide) {
|
2021-03-31 06:33:25 +00:00
|
|
|
if col.InFire != "" {
|
Add Switches, Fire/Water Collision and Play Menu
* New doodads: Switches.
* They come in four varieties: wall switch (background element, with
"ON/OFF" text) and three side-profile switches for the floor, left
or right walls.
* On collision with the player, they flip their state from "OFF" to
"ON" or vice versa. If the player walks away and then collides
again, the switch flips again.
* Can be used to open/close Electric Doors when turned on/off. Their
default state is "off"
* If a switch receives a power signal from another linked switch, it
sets its own state to match. So, two "on/off" switches that are
connected to a door AND to each other will both flip on/off when one
of them flips.
* Update the Level Collision logic to support Decoration, Fire and Water
pixel collisions.
* Previously, ALL pixels in the level were acting as though solid.
* Non-solid pixels don't count for collision detection, but their
attributes (fire and water) are collected and returned.
* Updated the MenuScene to support loading a map file in Play Mode
instead of Edit Mode. Updated the title screen menu to add a button
for playing levels instead of editing them.
* Wrote some documentation.
2019-07-07 01:30:03 +00:00
|
|
|
a.Canvas.MaskColor = render.Black
|
2021-03-31 06:33:25 +00:00
|
|
|
if a.ID() == "PLAYER" { // only the player dies in fire.
|
|
|
|
s.DieByFire(col.InFire)
|
|
|
|
}
|
Add Switches, Fire/Water Collision and Play Menu
* New doodads: Switches.
* They come in four varieties: wall switch (background element, with
"ON/OFF" text) and three side-profile switches for the floor, left
or right walls.
* On collision with the player, they flip their state from "OFF" to
"ON" or vice versa. If the player walks away and then collides
again, the switch flips again.
* Can be used to open/close Electric Doors when turned on/off. Their
default state is "off"
* If a switch receives a power signal from another linked switch, it
sets its own state to match. So, two "on/off" switches that are
connected to a door AND to each other will both flip on/off when one
of them flips.
* Update the Level Collision logic to support Decoration, Fire and Water
pixel collisions.
* Previously, ALL pixels in the level were acting as though solid.
* Non-solid pixels don't count for collision detection, but their
attributes (fire and water) are collected and returned.
* Updated the MenuScene to support loading a map file in Play Mode
instead of Edit Mode. Updated the title screen menu to add a button
for playing levels instead of editing them.
* Wrote some documentation.
2019-07-07 01:30:03 +00:00
|
|
|
} else if col.InWater {
|
|
|
|
a.Canvas.MaskColor = render.DarkBlue
|
|
|
|
} else {
|
|
|
|
a.Canvas.MaskColor = render.Invisible
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-19 05:24:36 +00:00
|
|
|
// Handle a doodad changing the player character.
|
|
|
|
s.drawing.OnSetPlayerCharacter = s.SetPlayerCharacter
|
|
|
|
|
2018-09-25 16:40:34 +00:00
|
|
|
// Given a filename or map data to play?
|
|
|
|
if s.Level != nil {
|
|
|
|
log.Debug("PlayScene.Setup: received level from scene caller")
|
2021-07-20 00:14:00 +00:00
|
|
|
s.drawing.LoadLevel(s.Level)
|
2019-04-10 02:17:56 +00:00
|
|
|
s.drawing.InstallActors(s.Level.Actors)
|
2018-07-24 03:10:53 +00:00
|
|
|
} else if s.Filename != "" {
|
2021-07-19 03:04:24 +00:00
|
|
|
loadscreen.SetSubtitle("Opening: " + s.Filename)
|
2018-07-24 03:10:53 +00:00
|
|
|
log.Debug("PlayScene.Setup: loading map from file %s", s.Filename)
|
2019-04-16 06:07:15 +00:00
|
|
|
// NOTE: s.LoadLevel also calls s.drawing.InstallActors
|
2018-07-24 03:10:53 +00:00
|
|
|
s.LoadLevel(s.Filename)
|
|
|
|
}
|
|
|
|
|
2018-09-25 16:40:34 +00:00
|
|
|
if s.Level == nil {
|
2018-07-24 03:10:53 +00:00
|
|
|
log.Debug("PlayScene.Setup: no grid given, initializing empty grid")
|
2018-09-25 16:40:34 +00:00
|
|
|
s.Level = level.New()
|
2021-07-20 00:14:00 +00:00
|
|
|
s.drawing.LoadLevel(s.Level)
|
2019-04-10 02:17:56 +00:00
|
|
|
s.drawing.InstallActors(s.Level.Actors)
|
2018-06-21 02:00:46 +00:00
|
|
|
}
|
2018-07-24 03:10:53 +00:00
|
|
|
|
2021-08-16 00:01:18 +00:00
|
|
|
// Choose a death barrier in case the user falls off the map,
|
|
|
|
// so they don't fall forever.
|
|
|
|
worldSize := s.Level.Chunker.WorldSize()
|
|
|
|
s.deathBarrier = worldSize.H + 1000
|
|
|
|
log.Debug("Death barrier at %d", s.deathBarrier)
|
|
|
|
|
2021-07-19 03:04:24 +00:00
|
|
|
// Set the loading screen text with the level metadata.
|
|
|
|
loadscreen.SetSubtitle(
|
|
|
|
s.Level.Title,
|
|
|
|
"by "+s.Level.Author,
|
|
|
|
)
|
|
|
|
|
2019-04-16 06:07:15 +00:00
|
|
|
// Load all actor scripts.
|
|
|
|
s.drawing.SetScriptSupervisor(s.scripting)
|
|
|
|
if err := s.scripting.InstallScripts(s.Level); err != nil {
|
|
|
|
log.Error("PlayScene.Setup: failed to InstallScripts: %s", err)
|
|
|
|
}
|
2019-05-02 01:30:30 +00:00
|
|
|
|
|
|
|
// Load in the player character.
|
2022-01-19 05:24:36 +00:00
|
|
|
s.setupPlayer(balance.PlayerCharacterDoodad)
|
2019-05-02 01:30:30 +00:00
|
|
|
|
|
|
|
// Run all the actor scripts' main() functions.
|
|
|
|
if err := s.drawing.InstallScripts(); err != nil {
|
|
|
|
log.Error("PlayScene.Setup: failed to drawing.InstallScripts: %s", err)
|
|
|
|
}
|
|
|
|
|
2019-07-07 06:50:38 +00:00
|
|
|
if s.CanEdit {
|
|
|
|
d.Flash("Entered Play Mode. Press 'E' to edit this map.")
|
|
|
|
} else {
|
2021-10-08 01:24:18 +00:00
|
|
|
d.FlashError("%s", s.Level.Title)
|
2019-07-07 06:50:38 +00:00
|
|
|
}
|
|
|
|
|
2021-07-19 03:04:24 +00:00
|
|
|
// Pre-cache all bitmap images from the level chunks.
|
|
|
|
// Note: we are not running on the main thread, so SDL2 Textures
|
|
|
|
// don't get created yet, but we do the full work of caching bitmap
|
|
|
|
// images which later get fed directly into SDL2 saving speed at
|
|
|
|
// runtime, + the bitmap generation is pretty wicked fast anyway.
|
|
|
|
loadscreen.PreloadAllChunkBitmaps(s.Level.Chunker)
|
|
|
|
|
2022-02-20 02:25:36 +00:00
|
|
|
// Gamepad: put into GameplayMode.
|
|
|
|
gamepad.SetMode(gamepad.GameplayMode)
|
|
|
|
|
2022-01-03 00:28:43 +00:00
|
|
|
s.startTime = time.Now()
|
|
|
|
s.perfectRun = true
|
2019-07-02 22:24:46 +00:00
|
|
|
s.running = true
|
2018-07-24 03:10:53 +00:00
|
|
|
|
2018-06-21 02:00:46 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-01-19 05:24:36 +00:00
|
|
|
// SetPlayerCharacter changes the doodad used for the player, by destroying the
|
|
|
|
// current player character and making it from scratch.
|
|
|
|
func (s *PlayScene) SetPlayerCharacter(filename string) {
|
|
|
|
spawn := s.Player.Position()
|
|
|
|
s.Player.Destroy()
|
|
|
|
s.drawing.RemoveActor(s.Player)
|
|
|
|
|
|
|
|
log.Info("SetPlayerCharacter: %s", filename)
|
|
|
|
s.installPlayerDoodad(filename, spawn, render.Rect{})
|
|
|
|
if err := s.drawing.InstallScripts(); err != nil {
|
|
|
|
log.Error("SetPlayerCharacter: InstallScripts: %s", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-31 02:13:28 +00:00
|
|
|
// setupPlayer creates and configures the Player Character in the level.
|
2022-01-19 05:24:36 +00:00
|
|
|
func (s *PlayScene) setupPlayer(playerCharacterFilename string) {
|
2019-12-31 02:13:28 +00:00
|
|
|
// Find the spawn point of the player. Search the level for the
|
|
|
|
// "start-flag.doodad"
|
|
|
|
var (
|
2022-01-19 05:24:36 +00:00
|
|
|
isStartFlagCharacter bool
|
2021-08-12 03:40:31 +00:00
|
|
|
|
2019-12-31 02:13:28 +00:00
|
|
|
spawn render.Point
|
2022-01-19 05:24:36 +00:00
|
|
|
centerIn render.Rect
|
2021-08-16 00:01:18 +00:00
|
|
|
flag = &level.Actor{}
|
2021-08-12 03:40:31 +00:00
|
|
|
flagSize = render.NewRect(86, 86) // TODO: start-flag.doodad is 86x86 px
|
2019-12-31 02:13:28 +00:00
|
|
|
flagCount int
|
|
|
|
)
|
|
|
|
for actorID, actor := range s.Level.Actors {
|
|
|
|
if actor.Filename == "start-flag.doodad" {
|
2021-08-12 03:40:31 +00:00
|
|
|
// Support alternative player characters: if the Start Flag is linked
|
|
|
|
// to another actor, that actor becomes the player character.
|
|
|
|
for _, linkID := range actor.Links {
|
|
|
|
if linkedActor, ok := s.Level.Actors[linkID]; ok {
|
|
|
|
playerCharacterFilename = linkedActor.Filename
|
2022-01-09 02:27:37 +00:00
|
|
|
isStartFlagCharacter = true
|
2021-08-12 03:40:31 +00:00
|
|
|
log.Info("Playing as: %s", playerCharacterFilename)
|
2022-01-03 00:28:43 +00:00
|
|
|
break
|
2021-08-12 03:40:31 +00:00
|
|
|
}
|
2019-12-31 02:13:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: start-flag.doodad is 86x86 pixels but we can't tell that
|
|
|
|
// from right here.
|
|
|
|
log.Info("Found start-flag.doodad at %s (ID %s)", actor.Point, actorID)
|
2021-08-12 03:40:31 +00:00
|
|
|
flag = actor
|
2021-08-16 03:26:47 +00:00
|
|
|
flagCount++
|
2021-08-12 03:40:31 +00:00
|
|
|
break
|
2019-12-31 02:13:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-09 02:27:37 +00:00
|
|
|
// If the user is cheating for the player character, mark the
|
|
|
|
// session cheated already. e.g. "Play as Bird" cheat would let
|
|
|
|
// them just fly to the goal in levels that don't link their
|
|
|
|
// Start Flag to a specific character.
|
|
|
|
if !isStartFlagCharacter && !balance.IsPlayerCharacterDefault() {
|
|
|
|
log.Warn("Mark session as cheated: the player spawned as %s instead of default", playerCharacterFilename)
|
|
|
|
s.SetCheated()
|
|
|
|
}
|
|
|
|
|
2021-08-16 03:17:53 +00:00
|
|
|
// The Start Flag becomes the player's initial checkpoint.
|
|
|
|
s.lastCheckpoint = flag.Point
|
|
|
|
|
2021-10-05 05:02:00 +00:00
|
|
|
if !s.SpawnPoint.IsZero() {
|
|
|
|
spawn = s.SpawnPoint
|
|
|
|
} else {
|
2022-01-19 05:24:36 +00:00
|
|
|
spawn = flag.Point
|
|
|
|
centerIn = render.Rect{
|
|
|
|
W: flagSize.W,
|
|
|
|
H: flagSize.H,
|
|
|
|
}
|
2021-10-05 05:02:00 +00:00
|
|
|
}
|
2021-08-12 03:40:31 +00:00
|
|
|
|
2019-12-31 02:13:28 +00:00
|
|
|
// Surface warnings around the spawn flag.
|
|
|
|
if flagCount == 0 {
|
2021-10-08 01:24:18 +00:00
|
|
|
s.d.FlashError("Warning: this level contained no Start Flag.")
|
2019-12-31 02:13:28 +00:00
|
|
|
} else if flagCount > 1 {
|
2021-10-08 01:24:18 +00:00
|
|
|
s.d.FlashError("Warning: this level contains multiple Start Flags. Player spawn point is ambiguous.")
|
2019-12-31 02:13:28 +00:00
|
|
|
}
|
|
|
|
|
2022-01-19 05:24:36 +00:00
|
|
|
s.installPlayerDoodad(playerCharacterFilename, spawn, centerIn)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load and install the player doodad onto the level.
|
|
|
|
// Make sure the previous PLAYER was removed.
|
|
|
|
// If spawn is zero, uses the player's last spawn point.
|
|
|
|
// centerIn is optional, ignored if zero.
|
|
|
|
func (s *PlayScene) installPlayerDoodad(filename string, spawn render.Point, centerIn render.Rect) {
|
|
|
|
// Load in the player character.
|
|
|
|
player, err := doodads.LoadFile(filename)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("PlayScene.Setup: failed to load player doodad: %s", err)
|
|
|
|
player = doodads.NewDummy(32)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Center the player within the box of the doodad, for the Start Flag especially.
|
|
|
|
if !centerIn.IsZero() {
|
|
|
|
spawn = render.NewPoint(
|
|
|
|
spawn.X+(centerIn.W/2)-(player.Layers[0].Chunker.Size/2),
|
|
|
|
|
|
|
|
// Y: the bottom of the flag, 4 pixels from the floor.
|
|
|
|
spawn.Y+centerIn.H-4-(player.Layers[0].Chunker.Size),
|
|
|
|
)
|
|
|
|
} else if spawn.IsZero() && !s.SpawnPoint.IsZero() {
|
|
|
|
spawn = s.SpawnPoint
|
|
|
|
}
|
|
|
|
|
2019-12-31 02:13:28 +00:00
|
|
|
s.Player = uix.NewActor("PLAYER", &level.Actor{}, player)
|
2022-02-20 19:48:36 +00:00
|
|
|
s.Player.SetInventory(true) // player always can pick up items
|
2019-12-31 02:13:28 +00:00
|
|
|
s.Player.MoveTo(spawn)
|
|
|
|
s.drawing.AddActor(s.Player)
|
|
|
|
s.drawing.FollowActor = s.Player.ID()
|
|
|
|
|
2020-04-05 04:00:32 +00:00
|
|
|
// 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),
|
|
|
|
Acceleration: 0.025,
|
|
|
|
Friction: 0.1,
|
|
|
|
}
|
|
|
|
|
2019-12-31 02:13:28 +00:00
|
|
|
// Set up the player character's script in the VM.
|
2021-10-10 03:45:38 +00:00
|
|
|
if err := s.scripting.AddLevelScript(s.Player.ID(), s.Player.Actor.Filename); err != nil {
|
2019-12-31 02:13:28 +00:00
|
|
|
log.Error("PlayScene.Setup: scripting.InstallActor(player) failed: %s", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-26 01:36:53 +00:00
|
|
|
// EditLevel toggles out of Play Mode to edit the level.
|
|
|
|
func (s *PlayScene) EditLevel() {
|
|
|
|
log.Info("Edit Mode, Go!")
|
2022-02-20 02:25:36 +00:00
|
|
|
gamepad.SetMode(gamepad.MouseMode)
|
2019-06-26 01:36:53 +00:00
|
|
|
s.d.Goto(&EditorScene{
|
2021-10-05 03:49:11 +00:00
|
|
|
Filename: s.Filename,
|
|
|
|
Level: s.Level,
|
|
|
|
RememberScrollPosition: s.RememberScrollPosition,
|
2019-06-26 01:36:53 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-07-02 22:24:46 +00:00
|
|
|
// RestartLevel starts the level over again.
|
|
|
|
func (s *PlayScene) RestartLevel() {
|
|
|
|
log.Info("Restart Level")
|
|
|
|
s.d.Goto(&PlayScene{
|
|
|
|
Filename: s.Filename,
|
|
|
|
Level: s.Level,
|
|
|
|
CanEdit: s.CanEdit,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-08-16 03:17:53 +00:00
|
|
|
// SetCheckpoint sets the player's checkpoint.
|
|
|
|
func (s *PlayScene) SetCheckpoint(where render.Point) {
|
|
|
|
s.lastCheckpoint = where
|
|
|
|
}
|
|
|
|
|
|
|
|
// RetryCheckpoint moves the player back to their last checkpoint.
|
|
|
|
func (s *PlayScene) RetryCheckpoint() {
|
2022-01-19 02:32:15 +00:00
|
|
|
// Grant the player invulnerability for 5 seconds
|
|
|
|
s.godModeUntil = time.Now().Add(balance.RespawnGodModeTimer)
|
|
|
|
|
2021-08-16 03:17:53 +00:00
|
|
|
log.Info("Move player back to last checkpoint")
|
|
|
|
s.Player.MoveTo(s.lastCheckpoint)
|
|
|
|
s.running = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// BeatLevel handles the level success condition.
|
|
|
|
func (s *PlayScene) BeatLevel() {
|
|
|
|
s.d.Flash("Hurray!")
|
|
|
|
s.ShowEndLevelModal(
|
|
|
|
true,
|
|
|
|
"Level Completed",
|
|
|
|
"Congratulations on clearing the level!",
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// FailLevel handles a level failure triggered by a doodad.
|
|
|
|
func (s *PlayScene) FailLevel(message string) {
|
2022-02-20 19:48:36 +00:00
|
|
|
if s.Player.Invulnerable() || s.godMode || s.godModeUntil.After(time.Now()) {
|
2022-01-18 06:02:27 +00:00
|
|
|
return
|
|
|
|
}
|
2022-01-03 00:28:43 +00:00
|
|
|
s.SetImperfect()
|
2021-10-08 01:24:18 +00:00
|
|
|
s.d.FlashError(message)
|
2022-01-19 02:32:15 +00:00
|
|
|
|
2021-08-16 03:17:53 +00:00
|
|
|
s.ShowEndLevelModal(
|
|
|
|
false,
|
|
|
|
"You've died!",
|
|
|
|
message,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-03-31 06:33:25 +00:00
|
|
|
// DieByFire ends the level by "fire", or w/e the swatch is named.
|
|
|
|
func (s *PlayScene) DieByFire(name string) {
|
2021-08-16 03:17:53 +00:00
|
|
|
s.FailLevel(fmt.Sprintf("Watch out for %s!", name))
|
|
|
|
}
|
|
|
|
|
2022-01-03 00:28:43 +00:00
|
|
|
// SetImperfect sets the perfectRun flag to false and changes the icon for the timer.
|
|
|
|
func (s *PlayScene) SetImperfect() {
|
|
|
|
if s.cheated {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
s.perfectRun = false
|
|
|
|
if s.timerPerfectImage != nil {
|
|
|
|
s.timerPerfectImage.Hide()
|
|
|
|
}
|
|
|
|
if s.timerImperfectImage != nil {
|
|
|
|
s.timerImperfectImage.Show()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetCheated marks the level as having been cheated. The developer shell will call
|
|
|
|
// this if the user enters a cheat code during gameplay.
|
|
|
|
func (s *PlayScene) SetCheated() {
|
|
|
|
s.cheated = true
|
|
|
|
s.perfectRun = false
|
|
|
|
|
|
|
|
// Hide both timer icons.
|
|
|
|
if s.timerPerfectImage != nil {
|
|
|
|
s.timerPerfectImage.Hide()
|
|
|
|
}
|
|
|
|
if s.timerImperfectImage != nil {
|
|
|
|
s.timerImperfectImage.Hide()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-16 03:17:53 +00:00
|
|
|
// ShowEndLevelModal centralizes the EndLevel modal config.
|
|
|
|
// This is the common handler function between easy methods such as
|
|
|
|
// BeatLevel, FailLevel, and DieByFire.
|
|
|
|
func (s *PlayScene) ShowEndLevelModal(success bool, title, message string) {
|
|
|
|
config := modal.ConfigEndLevel{
|
2022-01-03 00:28:43 +00:00
|
|
|
Engine: s.d.Engine,
|
2021-08-16 03:17:53 +00:00
|
|
|
Success: success,
|
|
|
|
OnRestartLevel: s.RestartLevel,
|
|
|
|
OnRetryCheckpoint: s.RetryCheckpoint,
|
|
|
|
OnExitToMenu: func() {
|
2022-02-20 02:25:36 +00:00
|
|
|
gamepad.SetMode(gamepad.MouseMode)
|
2021-08-16 03:17:53 +00:00
|
|
|
s.d.Goto(&MainScene{})
|
|
|
|
},
|
|
|
|
}
|
2019-07-07 03:31:50 +00:00
|
|
|
|
|
|
|
if s.CanEdit {
|
2021-08-16 03:17:53 +00:00
|
|
|
config.OnEditLevel = s.EditLevel
|
|
|
|
}
|
|
|
|
|
|
|
|
// Beaten the level?
|
|
|
|
if success {
|
|
|
|
config.OnRetryCheckpoint = nil
|
2021-12-27 04:48:29 +00:00
|
|
|
|
2022-01-03 00:28:43 +00:00
|
|
|
// Are we in a levelpack?
|
2021-12-27 04:48:29 +00:00
|
|
|
if s.LevelPack != nil {
|
2022-01-03 00:28:43 +00:00
|
|
|
// Update the savegame to mark the level completed.
|
|
|
|
save, err := savegame.GetOrCreate()
|
|
|
|
if err != nil {
|
|
|
|
log.Warn("Load savegame file: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Info("Mark level '%s' from pack '%s' as completed", s.Filename, s.LevelPack.Filename)
|
|
|
|
if !s.cheated {
|
|
|
|
elapsed := time.Since(s.startTime)
|
|
|
|
highscore := save.NewHighScore(s.LevelPack.Filename, s.Filename, s.perfectRun, elapsed)
|
|
|
|
if highscore {
|
|
|
|
s.d.Flash("New record!")
|
|
|
|
config.NewRecord = true
|
|
|
|
config.IsPerfect = s.perfectRun
|
|
|
|
config.TimeElapsed = elapsed
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Player has cheated! Mark the level completed but grant no high score.
|
|
|
|
save.MarkCompleted(s.LevelPack.Filename, s.Filename)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save the player's scores file.
|
|
|
|
if err = save.Save(); err != nil {
|
|
|
|
log.Error("Couldn't save game: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Show the "Next Level" button if there is a sequel to this level.
|
2021-12-27 04:48:29 +00:00
|
|
|
for i, level := range s.LevelPack.Levels {
|
|
|
|
i := i
|
|
|
|
level := level
|
|
|
|
|
|
|
|
if level.Filename == s.Filename && i < len(s.LevelPack.Levels)-1 {
|
|
|
|
// Show "Next" button!
|
|
|
|
config.OnNextLevel = func() {
|
|
|
|
nextLevel := s.LevelPack.Levels[i+1]
|
|
|
|
log.Info("Advance to next level: %s", nextLevel.Filename)
|
|
|
|
s.d.PlayFromLevelpack(*s.LevelPack, nextLevel)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-07-07 03:31:50 +00:00
|
|
|
}
|
|
|
|
|
2021-08-16 03:17:53 +00:00
|
|
|
// Show the modal.
|
|
|
|
modal.EndLevel(config, title, message)
|
2019-07-07 03:31:50 +00:00
|
|
|
|
|
|
|
// Stop the simulation.
|
|
|
|
s.running = false
|
|
|
|
}
|
|
|
|
|
2018-06-21 02:00:46 +00:00
|
|
|
// Loop the editor scene.
|
2019-12-22 22:11:01 +00:00
|
|
|
func (s *PlayScene) Loop(d *Doodle, ev *event.State) error {
|
2021-07-19 03:04:24 +00:00
|
|
|
// Skip if still loading.
|
|
|
|
if loadscreen.IsActive() {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-02-20 02:25:36 +00:00
|
|
|
// Inform the gamepad controller whether we have antigravity controls.
|
|
|
|
gamepad.PlayModeAntigravity = s.antigravity || !s.Player.HasGravity()
|
|
|
|
|
2019-04-10 01:28:08 +00:00
|
|
|
// Update debug overlay values.
|
2019-12-28 03:16:34 +00:00
|
|
|
*s.debWorldIndex = s.drawing.WorldIndexAt(render.NewPoint(ev.CursorX, ev.CursorY)).String()
|
2019-04-14 22:25:03 +00:00
|
|
|
*s.debPosition = s.Player.Position().String() + " vel " + s.Player.Velocity().String()
|
2019-04-10 01:28:08 +00:00
|
|
|
*s.debViewport = s.drawing.Viewport().String()
|
|
|
|
*s.debScroll = s.drawing.Scroll.String()
|
|
|
|
|
2022-01-03 00:28:43 +00:00
|
|
|
// Update the timer.
|
|
|
|
s.timerLabel.Text = savegame.FormatDuration(time.Since(s.startTime))
|
|
|
|
|
2019-06-26 01:36:53 +00:00
|
|
|
s.supervisor.Loop(ev)
|
|
|
|
|
2019-04-10 01:28:08 +00:00
|
|
|
// Has the window been resized?
|
2019-12-22 22:11:01 +00:00
|
|
|
if ev.WindowResized {
|
2019-04-10 01:28:08 +00:00
|
|
|
w, h := d.Engine.WindowSize()
|
|
|
|
if w != d.width || h != d.height {
|
|
|
|
d.width = w
|
|
|
|
d.height = h
|
2019-12-28 03:16:34 +00:00
|
|
|
s.drawing.Resize(render.NewRect(d.width, d.height))
|
2022-01-03 00:28:43 +00:00
|
|
|
s.screen.Resize(render.NewRect(d.width, d.height))
|
2019-04-10 01:28:08 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-07-24 03:10:53 +00:00
|
|
|
// Switching to Edit Mode?
|
2020-11-18 02:22:48 +00:00
|
|
|
if s.CanEdit && keybind.GotoEdit(ev) {
|
2022-02-20 02:25:36 +00:00
|
|
|
gamepad.SetMode(gamepad.MouseMode)
|
2019-06-26 01:36:53 +00:00
|
|
|
s.EditLevel()
|
2018-07-24 03:10:53 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-07-02 22:24:46 +00:00
|
|
|
// Is the simulation still running?
|
|
|
|
if s.running {
|
|
|
|
// Loop the script supervisor so timeouts/intervals can fire in scripts.
|
|
|
|
if err := s.scripting.Loop(); err != nil {
|
|
|
|
log.Error("PlayScene.Loop: scripting.Loop: %s", err)
|
|
|
|
}
|
2019-04-19 01:15:05 +00:00
|
|
|
|
2021-10-05 02:51:31 +00:00
|
|
|
// Touch regions.
|
|
|
|
s.LoopTouchable(ev)
|
|
|
|
|
2019-07-02 22:24:46 +00:00
|
|
|
s.movePlayer(ev)
|
|
|
|
if err := s.drawing.Loop(ev); err != nil {
|
|
|
|
log.Error("Drawing loop error: %s", err.Error())
|
|
|
|
}
|
2020-04-03 06:09:46 +00:00
|
|
|
|
2021-08-16 00:01:18 +00:00
|
|
|
// Check if the player hit the death barrier.
|
|
|
|
if s.Player.Position().Y > s.deathBarrier {
|
|
|
|
s.DieByFire("falling off the map")
|
|
|
|
}
|
|
|
|
|
2020-04-03 06:09:46 +00:00
|
|
|
// Update the inventory HUD.
|
|
|
|
s.computeInventory()
|
2019-04-10 02:17:56 +00:00
|
|
|
}
|
|
|
|
|
2018-07-22 03:43:01 +00:00
|
|
|
return nil
|
2018-06-21 02:00:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Draw the pixels on this frame.
|
|
|
|
func (s *PlayScene) Draw(d *Doodle) error {
|
2021-07-19 03:04:24 +00:00
|
|
|
// Skip if still loading.
|
|
|
|
if loadscreen.IsActive() {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-06-21 02:00:46 +00:00
|
|
|
// Clear the canvas and fill it with white.
|
2018-07-22 00:12:22 +00:00
|
|
|
d.Engine.Clear(render.White)
|
2018-06-21 02:00:46 +00:00
|
|
|
|
2018-09-25 16:40:34 +00:00
|
|
|
// Draw the level.
|
|
|
|
s.drawing.Present(d.Engine, s.drawing.Point())
|
2018-06-21 02:00:46 +00:00
|
|
|
|
2018-07-25 03:57:22 +00:00
|
|
|
// Draw out bounding boxes.
|
2021-06-03 03:41:53 +00:00
|
|
|
if DebugCollision {
|
|
|
|
for _, actor := range s.drawing.Actors() {
|
|
|
|
d.DrawCollisionBox(s.drawing, actor)
|
|
|
|
}
|
|
|
|
}
|
2018-07-25 03:57:22 +00:00
|
|
|
|
2020-04-03 06:09:46 +00:00
|
|
|
// Draw the UI screen and any widgets that attached to it.
|
|
|
|
s.screen.Compute(d.Engine)
|
|
|
|
s.screen.Present(d.Engine, render.Origin)
|
|
|
|
|
2019-06-26 01:36:53 +00:00
|
|
|
// Draw the Edit button.
|
|
|
|
var (
|
2019-12-28 03:16:34 +00:00
|
|
|
canSize = s.drawing.Size()
|
|
|
|
size = s.editButton.Size()
|
|
|
|
padding = 8
|
2019-06-26 01:36:53 +00:00
|
|
|
)
|
2019-12-29 08:01:47 +00:00
|
|
|
s.editButton.MoveTo(render.Point{
|
2019-06-26 01:36:53 +00:00
|
|
|
X: canSize.W - size.W - padding,
|
|
|
|
Y: canSize.H - size.H - padding,
|
|
|
|
})
|
2019-12-29 08:01:47 +00:00
|
|
|
s.editButton.Present(d.Engine, s.editButton.Point())
|
2019-06-26 01:36:53 +00:00
|
|
|
|
2021-10-05 02:51:31 +00:00
|
|
|
// Visualize the touch regions?
|
|
|
|
s.DrawTouchable()
|
|
|
|
|
2022-01-03 06:36:32 +00:00
|
|
|
// Let Supervisor draw menus
|
|
|
|
s.supervisor.Present(d.Engine)
|
|
|
|
|
2018-06-21 02:00:46 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-07-22 00:12:22 +00:00
|
|
|
// movePlayer updates the player's X,Y coordinate based on key pressed.
|
2019-12-22 22:11:01 +00:00
|
|
|
func (s *PlayScene) movePlayer(ev *event.State) {
|
2020-04-05 04:00:32 +00:00
|
|
|
var (
|
|
|
|
playerSpeed = float64(balance.PlayerMaxVelocity)
|
|
|
|
velocity = s.Player.Velocity()
|
|
|
|
direction float64
|
|
|
|
jumping bool
|
|
|
|
)
|
2019-04-10 02:17:56 +00:00
|
|
|
|
2020-04-05 04:00:32 +00:00
|
|
|
// Antigravity: player can move anywhere with arrow keys.
|
Thief and Inventory APIs
This commit adds the Thief character with starter graphics
(no animations).
The Thief walks back and forth and will steal items from other
doodads, including the player. For singleton items that have no
quantity, like the Colored Keys, the Thief will only steal one
if he does not already have it. Quantitied items like the
Small Key are always stolen.
Flexibility in the playable character is introduced: Boy,
Azulian, Bird, and Thief all respond to playable controls.
There is not currently a method to enable these apart from
modifying balance.PlayerCharacterDoodad at compile time.
New and Changed Doodads
* Thief: new doodad that walks back and forth and will steal
items from other characters inventory.
* Bird: has no inventory and cannot pick up items, unless player
controlled. Its hitbox has also been fixed so it collides with
floors correctly - not something normally seen in the Bird.
* Boy: opts in to have inventory.
* Keys (all): only gives themselves to actors having inventories.
JavaScript API - New functions available
* Self.IsPlayer() - returns if the current actor IS the player.
* Self.SetInventory(bool) - doodads must opt-in to having an
inventory. Keys should only give themselves to doodads having
an inventory.
* Self.HasInventory() bool
* Self.AddItem(filename, qty)
* Self.RemoveItem(filename, qty)
* Self.HasItem(filename)
* Self.Inventory() - returns map[string]int
* Self.ClearInventory()
* Self.OnLeave(func(e)) now receives a CollideEvent as parameter
instead of the useless actor ID. Notably, e.Actor is the
leaving actor and e.Settled is always true.
Other Changes
* Play Mode: if playing as a character which doesn't obey gravity,
such as the bird, antigravity controls are enabled by default.
If you `import antigravity` you can turn gravity back on.
* Doodad collision scripts are no longer run in parallel
goroutines. It made the Thief's job difficult trying to steal
items in many threads simultaneously!
2021-08-10 05:42:22 +00:00
|
|
|
if s.antigravity || !s.Player.HasGravity() {
|
2020-04-05 04:00:32 +00:00
|
|
|
velocity.X = 0
|
|
|
|
velocity.Y = 0
|
2020-01-03 06:05:49 +00:00
|
|
|
|
2020-04-05 04:00:32 +00:00
|
|
|
// Shift to slow your roll to 1 pixel per tick.
|
2021-01-03 23:19:21 +00:00
|
|
|
if keybind.Shift(ev) {
|
2020-04-05 04:00:32 +00:00
|
|
|
playerSpeed = 1
|
|
|
|
}
|
2018-07-24 03:10:53 +00:00
|
|
|
|
2021-01-03 23:19:21 +00:00
|
|
|
if keybind.Left(ev) {
|
2020-04-05 04:00:32 +00:00
|
|
|
velocity.X = -playerSpeed
|
2021-01-03 23:19:21 +00:00
|
|
|
} else if keybind.Right(ev) {
|
2020-04-05 04:00:32 +00:00
|
|
|
velocity.X = playerSpeed
|
|
|
|
}
|
2021-01-03 23:19:21 +00:00
|
|
|
if keybind.Up(ev) {
|
2020-04-05 04:00:32 +00:00
|
|
|
velocity.Y = -playerSpeed
|
2021-01-03 23:19:21 +00:00
|
|
|
} else if keybind.Down(ev) {
|
2020-04-05 04:00:32 +00:00
|
|
|
velocity.Y = playerSpeed
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Moving left or right.
|
2021-01-03 23:19:21 +00:00
|
|
|
if keybind.Left(ev) {
|
2020-04-05 04:00:32 +00:00
|
|
|
direction = -1
|
2021-01-03 23:19:21 +00:00
|
|
|
} else if keybind.Right(ev) {
|
2020-04-05 04:00:32 +00:00
|
|
|
direction = 1
|
|
|
|
}
|
2019-07-05 22:02:22 +00:00
|
|
|
|
2020-04-05 04:00:32 +00:00
|
|
|
// Up button to signal they want to jump.
|
2021-10-08 01:24:18 +00:00
|
|
|
if keybind.Up(ev) {
|
2020-04-05 04:00:32 +00:00
|
|
|
if s.Player.Grounded() {
|
2021-10-08 01:24:18 +00:00
|
|
|
velocity.Y = balance.PlayerJumpVelocity
|
2020-04-05 04:00:32 +00:00
|
|
|
}
|
2021-10-08 01:24:18 +00:00
|
|
|
} else if velocity.Y < 0 {
|
|
|
|
velocity.Y = 0
|
2020-04-05 04:00:32 +00:00
|
|
|
}
|
2021-10-08 01:24:18 +00:00
|
|
|
// 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
|
|
|
|
// }
|
|
|
|
// }
|
2020-04-05 04:00:32 +00:00
|
|
|
|
|
|
|
// Moving left or right? Interpolate their velocity by acceleration.
|
|
|
|
if direction != 0 {
|
2021-10-08 01:24:18 +00:00
|
|
|
if s.playerLastDirection != direction {
|
|
|
|
velocity.X = 0
|
|
|
|
}
|
|
|
|
|
2020-04-05 04:00:32 +00:00
|
|
|
// 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,
|
|
|
|
)
|
2019-07-05 22:02:22 +00:00
|
|
|
}
|
|
|
|
|
2020-04-05 04:00:32 +00:00
|
|
|
// 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--
|
|
|
|
}
|
2018-07-24 03:10:53 +00:00
|
|
|
}
|
|
|
|
|
2021-10-08 01:24:18 +00:00
|
|
|
s.playerLastDirection = direction
|
|
|
|
|
2021-01-03 23:19:21 +00:00
|
|
|
// 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
|
|
|
|
}
|
2019-04-14 22:25:03 +00:00
|
|
|
s.Player.SetVelocity(velocity)
|
2019-05-05 21:03:20 +00:00
|
|
|
|
2021-01-03 23:19:21 +00:00
|
|
|
// If the "Use" key is pressed, set an actor flag on the player.
|
|
|
|
s.Player.SetUsing(keybind.Use(ev))
|
|
|
|
|
2021-06-20 05:14:41 +00:00
|
|
|
s.scripting.To(s.Player.ID()).Events.RunKeypress(keybind.FromEvent(ev))
|
2019-04-14 22:25:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Drawing returns the private world drawing, for debugging with the console.
|
|
|
|
func (s *PlayScene) Drawing() *uix.Canvas {
|
|
|
|
return s.drawing
|
2018-06-21 02:00:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// LoadLevel loads a level from disk.
|
2021-12-27 04:48:29 +00:00
|
|
|
//
|
|
|
|
// If the PlayScene was called with a LevelPack, it will check there
|
|
|
|
// first before the usual locations.
|
|
|
|
//
|
|
|
|
// The usual locations are: embedded bindata, ./assets folder on disk,
|
|
|
|
// and user content finally.
|
2018-07-21 22:11:00 +00:00
|
|
|
func (s *PlayScene) LoadLevel(filename string) error {
|
2018-09-25 16:40:34 +00:00
|
|
|
s.Filename = filename
|
2018-06-21 02:00:46 +00:00
|
|
|
|
2021-12-27 04:48:29 +00:00
|
|
|
var (
|
|
|
|
lvl *level.Level
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
|
|
|
|
// Are we playing out of a levelpack?
|
|
|
|
if s.LevelPack != nil {
|
|
|
|
levelbin, err := s.LevelPack.GetData("levels/" + filename)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("Error reading levels/%s from zip: %s", filename, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
lvl, err = level.FromJSON(filename, levelbin)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("PlayScene.LoadLevel(%s) from zipfile: %s", filename, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Info("PlayScene.LoadLevel: found %s in LevelPack zip data", filename)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try the usual suspects.
|
|
|
|
if lvl == nil {
|
|
|
|
log.Info("PlayScene.LoadLevel: trying the usual places")
|
|
|
|
lvl, err = level.LoadFile(filename)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("PlayScene.LoadLevel(%s): %s", filename, err)
|
|
|
|
}
|
2018-09-25 16:40:34 +00:00
|
|
|
}
|
2018-06-21 02:00:46 +00:00
|
|
|
|
2021-12-27 04:48:29 +00:00
|
|
|
s.Level = lvl
|
2021-07-20 00:14:00 +00:00
|
|
|
s.drawing.LoadLevel(s.Level)
|
2019-07-02 22:24:46 +00:00
|
|
|
s.drawing.InstallActors(s.Level.Actors)
|
2018-06-21 02:00:46 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2018-07-24 03:10:53 +00:00
|
|
|
|
|
|
|
// Destroy the scene.
|
|
|
|
func (s *PlayScene) Destroy() error {
|
|
|
|
return nil
|
|
|
|
}
|