New Doodad: Blue Bird

* The blue bird follows the same base AI as the red bird (it has a
  target altitude that it tries to maintain, and it will dive at the
  player) but the blue bird flies in a sine wave pattern around its
  target altitude. It also has a longer scan radius to search for the
  player than the red bird.
* The sine wave pattern of the blue bird means you may fly under its
  radar depending how high it is on average.

Cheat codes that replace the player character are refactored to make
it easier to extend, and new cheats have been added:

* super azulian: play as the Red Azulian.
* hyper azulian: play as the White Azulian.
* bluebird: play as the new Bird (blue).
This commit is contained in:
Noah 2022-04-30 17:59:55 -07:00
parent 402b5efa7e
commit ad67e2b42b
21 changed files with 125 additions and 127 deletions

View File

@ -2,9 +2,17 @@ ALL: build
.PHONY: build .PHONY: build
build: build:
doodad convert -t "Bird (red)" left-1.png left-2.png right-1.png right-2.png \ doodad convert -t "Bird (red)" red/left-1.png red/left-2.png red/right-1.png \
dive-left.png dive-right.png bird-red.doodad red/right-2.png red/dive-left.png red/dive-right.png \
bird-red.doodad
doodad install-script bird.js bird-red.doodad doodad install-script bird.js bird-red.doodad
doodad edit-doodad --tag "color=red" bird-red.doodad
doodad convert -t "Bird (blue)" blue/left-1.png blue/left-2.png blue/right-1.png \
blue/right-2.png blue/dive-left.png blue/dive-right.png \
bird-blue.doodad
doodad install-script bird.js bird-blue.doodad
doodad edit-doodad --tag "color=blue" bird-blue.doodad
# Tag the category for these doodads # Tag the category for these doodads
for i in *.doodad; do\ for i in *.doodad; do\

View File

@ -1,8 +1,26 @@
// Bird // Bird (red and blue)
/*
Base A.I. behaviors (red bird) are:
- Tries to maintain original altitude in level and flies left/right.
- Divebombs the player to attack, then climbs back to its original
altitude when it hits a floor/wall.
Blue bird:
- Flies in a sine wave pattern (its target altitude fluctuates
around the bird's original placement on the level).
- Longer aggro radius to dive.
*/
let speed = 4, let speed = 4,
Vx = Vy = 0, Vx = Vy = 0,
altitude = Self.Position().Y; // original height in the level color = Self.GetTag("color"), // informs our A.I. behaviors
searchStep = 12 // pixels to step while searching for a player
searchLimit = color === "blue" ? 24 : 12, // multiples of searchStep for aggro radius
altitude = Self.Position().Y, // original height in level
targetAltitude = altitude; // bird's target height to maintain
let direction = "left", let direction = "left",
lastDirection = "left"; lastDirection = "left";
@ -42,6 +60,11 @@ function main() {
lastSampled = Point(0, 0); lastSampled = Point(0, 0);
setInterval(() => { setInterval(() => {
// Run blue bird A.I. if we are blue: moves our target altitude along a sine wave.
if (color === "blue") {
AI_BlueBird();
}
// Sample how far we've moved to detect hitting a wall. // Sample how far we've moved to detect hitting a wall.
if (sampleTick % sampleRate === 0) { if (sampleTick % sampleRate === 0) {
let curP = Self.Position() let curP = Self.Position()
@ -65,8 +88,8 @@ function main() {
// If we are not flying at our original altitude, correct for that. // If we are not flying at our original altitude, correct for that.
let curV = Self.Position(); let curV = Self.Position();
Vy = 0.0; Vy = 0.0;
if (curV.Y != altitude) { if (curV.Y != targetAltitude) {
Vy = curV.Y < altitude ? 1 : -1; Vy = curV.Y < targetAltitude ? 1 : -1;
} }
// Scan for the player character and dive. // Scan for the player character and dive.
@ -114,9 +137,9 @@ function AI_ScanForPlayer() {
return return
} }
let stepY = 12, // number of pixels to skip let stepY = searchStep, // number of pixels to skip
stepX = stepY, stepX = stepY,
limit = stepX * 20, // furthest we'll scan limit = stepX * searchLimit, // furthest we'll scan
scanX = scanY = 0, scanX = scanY = 0,
size = Self.Size(), size = Self.Size(),
fromPoint = Self.Position(); fromPoint = Self.Position();
@ -147,6 +170,35 @@ function AI_ScanForPlayer() {
return; return;
} }
// Sine wave controls for the Blue bird.
var sineLimit = 32,
sineCounter = 0,
sineDirection = 1,
sineFrequency = 5, // every 500ms
sineStep = 16;
// A.I. Subroutine: sine wave pattern (Blue bird).
function AI_BlueBird() {
// The main loop runs on a 100ms interval, control how frequently
// to adjust the bird's target velocity.
if (sineCounter > 0 && (sineCounter % sineFrequency) > 1) {
sineCounter++;
return;
}
sineCounter++;
targetAltitude += sineStep*sineDirection;
// Cap the distance between our starting altitude and sine-wave target.
if (targetAltitude < altitude && altitude - targetAltitude >= sineLimit) {
targetAltitude = altitude - sineLimit;
sineDirection = 1
} else if (targetAltitude > altitude && targetAltitude - altitude >= sineLimit) {
targetAltitude = altitude + sineLimit;
sineDirection = -1
}
}
// If under control of the player character. // If under control of the player character.
function player() { function player() {
let playerSpeed = 12, let playerSpeed = 12,

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 959 B

After

Width:  |  Height:  |  Size: 959 B

View File

Before

Width:  |  Height:  |  Size: 989 B

After

Width:  |  Height:  |  Size: 989 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1022 B

After

Width:  |  Height:  |  Size: 1022 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -26,10 +26,6 @@ var (
CheatGiveKeys = "give all keys" CheatGiveKeys = "give all keys"
CheatDropItems = "drop all items" CheatDropItems = "drop all items"
CheatPlayAsBird = "fly like a bird" CheatPlayAsBird = "fly like a bird"
CheatPlayAsBoy = "pinocchio"
CheatPlayAsAzuBlue = "the cell"
CheatPlayAsThief = "play as thief"
CheatPlayAsAnvil = "megaton weight"
CheatGodMode = "god mode" CheatGodMode = "god mode"
CheatDebugLoadScreen = "test load screen" CheatDebugLoadScreen = "test load screen"
CheatUnlockLevels = "master key" CheatUnlockLevels = "master key"
@ -39,3 +35,15 @@ var (
var ( var (
CheatEnabledUnlockLevels bool CheatEnabledUnlockLevels bool
) )
// Actor replacement cheats
var CheatActors = map[string]string{
"pinocchio": "boy",
"the cell": "azu-blue",
"super azulian": "azu-red",
"hyper azulian": "azu-white",
"fly like a bird": "bird-red",
"bluebird": "bird-blue",
"megaton weight": "anvil",
"play as thief": "thief",
}

View File

@ -131,8 +131,8 @@ var (
EagerRenderLevelChunks = true EagerRenderLevelChunks = true
// Number of chunks margin outside the Canvas Viewport for the LoadingViewport. // Number of chunks margin outside the Canvas Viewport for the LoadingViewport.
LoadingViewportMarginChunks = 2 LoadingViewportMarginChunks = render.NewPoint(8, 4) // hoz, vert
CanvasLoadUnloadModuloTicks uint64 = 4 CanvasLoadUnloadModuloTicks uint64 = 2
) )
// Edit Mode Values // Edit Mode Values

View File

@ -1,6 +1,7 @@
package doodle package doodle
import ( import (
"strings"
"time" "time"
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
@ -134,26 +135,6 @@ func (c Command) cheatCommand(d *Doodle) bool {
setPlayerCharacter = true setPlayerCharacter = true
d.Flash("Set default player character to Bird (red)") d.Flash("Set default player character to Bird (red)")
case balance.CheatPlayAsBoy:
balance.PlayerCharacterDoodad = "boy.doodad"
setPlayerCharacter = true
d.Flash("Set default player character to Boy")
case balance.CheatPlayAsAzuBlue:
balance.PlayerCharacterDoodad = "azu-blu.doodad"
setPlayerCharacter = true
d.Flash("Set default player character to Blue Azulian")
case balance.CheatPlayAsThief:
balance.PlayerCharacterDoodad = "thief.doodad"
setPlayerCharacter = true
d.Flash("Set default player character to Thief")
case balance.CheatPlayAsAnvil:
balance.PlayerCharacterDoodad = "anvil.doodad"
setPlayerCharacter = true
d.Flash("Set default player character to the Anvil")
case balance.CheatGodMode: case balance.CheatGodMode:
if isPlay { if isPlay {
d.Flash("God mode toggled") d.Flash("God mode toggled")
@ -189,8 +170,16 @@ func (c Command) cheatCommand(d *Doodle) bool {
} }
default: default:
// See if it was an endorsed actor cheat.
if filename, ok := balance.CheatActors[strings.ToLower(c.Raw)]; ok {
d.Flash("Set default player character to %s.", filename)
balance.PlayerCharacterDoodad = filename
setPlayerCharacter = true
} else {
// Not a cheat code.
return false return false
} }
}
// If we're setting the player character and in Play Mode, do it. // If we're setting the player character and in Play Mode, do it.
if setPlayerCharacter && isPlay { if setPlayerCharacter && isPlay {

View File

@ -20,7 +20,7 @@ type Chunker struct {
// Layer is optional for the caller, levels use only 0 and // Layer is optional for the caller, levels use only 0 and
// doodads use them for frames. When chunks are exported to // doodads use them for frames. When chunks are exported to
// zipfile the Layer keeps them from overlapping. // zipfile the Layer keeps them from overlapping.
Layer int Layer int `json:"-"` // internal use only
Size int `json:"size"` Size int `json:"size"`
// A Zipfile reference for new-style levels and doodads which // A Zipfile reference for new-style levels and doodads which
@ -311,7 +311,7 @@ func (c *Chunker) GetChunk(p render.Point) (*Chunk, bool) {
// Hit the zipfile for it. // Hit the zipfile for it.
if c.Zipfile != nil { if c.Zipfile != nil {
if chunk, err := ChunkFromZipfile(c.Zipfile, c.Layer, p); err == nil { if chunk, err := ChunkFromZipfile(c.Zipfile, c.Layer, p); err == nil {
log.Debug("GetChunk(%s) cache miss, read from zip", p) // log.Debug("GetChunk(%s) cache miss, read from zip", p)
c.SetChunk(p, chunk) // cache it c.SetChunk(p, chunk) // cache it
c.logChunkAccess(p, chunk) // for the LRU cache c.logChunkAccess(p, chunk) // for the LRU cache
if c.pal != nil { if c.pal != nil {

View File

@ -35,7 +35,7 @@ func RegisterPublishHooks(s *Supervisor, vm *VM) {
for { for {
select { select {
case <-vm.stop: case <-vm.stop:
log.Info("JavaScript VM %s stopping PubSub goroutine", vm.Name) log.Debug("JavaScript VM %s stopping PubSub goroutine", vm.Name)
return return
case msg := <-vm.Inbound: case msg := <-vm.Inbound:
vm.muSubscribe.Lock() vm.muSubscribe.Lock()

View File

@ -377,10 +377,10 @@ func (w *Canvas) LoadingViewport() render.Rect {
} }
return render.Rect{ return render.Rect{
X: vp.X - chunkSize*margin, X: vp.X - chunkSize*margin.X,
Y: vp.Y - chunkSize*margin, Y: vp.Y - chunkSize*margin.Y,
W: vp.W + chunkSize*margin, W: vp.W + chunkSize*margin.X,
H: vp.H + chunkSize*margin, H: vp.H + chunkSize*margin.Y,
} }
} }

View File

@ -132,6 +132,7 @@ func (c Settings) makeOptionsTab(tabFrame *ui.TabFrame, Width, Height int) *ui.F
Label: "Hide touchscreen control hints during Play Mode", Label: "Hide touchscreen control hints during Play Mode",
Font: balance.UIFont, Font: balance.UIFont,
BoolVariable: c.HideTouchHints, BoolVariable: c.HideTouchHints,
OnClick: onClick,
}, },
{ {
Label: "Level & Doodad Editor", Label: "Level & Doodad Editor",
@ -141,6 +142,7 @@ func (c Settings) makeOptionsTab(tabFrame *ui.TabFrame, Width, Height int) *ui.F
Label: "Horizontal instead of vertical toolbars", Label: "Horizontal instead of vertical toolbars",
Font: balance.UIFont, Font: balance.UIFont,
BoolVariable: c.HorizontalToolbars, BoolVariable: c.HorizontalToolbars,
OnClick: onClick,
Tooltip: ui.Tooltip{ Tooltip: ui.Tooltip{
Text: "Note: reload your level after changing this option.\n" + Text: "Note: reload your level after changing this option.\n" +
"Playtesting and returning will do.", "Playtesting and returning will do.",
@ -151,6 +153,7 @@ func (c Settings) makeOptionsTab(tabFrame *ui.TabFrame, Width, Height int) *ui.F
Label: "Disable auto-save in the Editor", Label: "Disable auto-save in the Editor",
Font: balance.UIFont, Font: balance.UIFont,
BoolVariable: c.DisableAutosave, BoolVariable: c.DisableAutosave,
OnClick: onClick,
}, },
{ {
Label: "Draw a crosshair at the mouse cursor.", Label: "Draw a crosshair at the mouse cursor.",
@ -409,109 +412,47 @@ func (c Settings) makeExperimentalTab(tabFrame *ui.TabFrame, Width, Height int)
// Common click handler for all settings, // Common click handler for all settings,
// so we can write the updated info to disk. // so we can write the updated info to disk.
onClick := func(ed ui.EventData) error { onClick := func() {
saveGameSettings() saveGameSettings()
return nil
} }
rows := []struct { form := magicform.Form{
Header string Supervisor: c.Supervisor,
Text string Engine: c.Engine,
Boolean *bool Vertical: true,
TextVariable *string LabelWidth: 150,
PadY int }
PadX int form.Create(tab, []magicform.Field{
name string // for special cases
}{
{ {
Header: "Enable Experimental Features", Label: "Enable Experimental Features",
Font: balance.LabelFont,
}, },
{ {
Text: "The setting below can enable experimental features in this\n" + Label: "The setting below can enable experimental features in this\n" +
"game. These are features which are still in development and\n" + "game. These are features which are still in development and\n" +
"may have unstable or buggy behavior.", "may have unstable or buggy behavior.",
PadY: 2, Font: balance.UIFont,
}, },
{ {
Header: "Viewport window", Label: "Viewport window",
},
{
Text: "This option in the Level menu opens another view into\n" +
"the level. Has glitchy wallpaper problems.",
PadY: 2,
},
{
Boolean: c.EnableFeatures,
Text: "Enable experimental features",
PadX: 4,
},
{
Text: "Restart the game for changes to take effect.",
PadY: 2,
},
}
for _, row := range rows {
row := row
frame := ui.NewFrame("Frame")
tab.Pack(frame, ui.Pack{
Side: ui.N,
FillX: true,
PadY: row.PadY,
})
// Headers get their own row to themselves.
if row.Header != "" {
label := ui.NewLabel(ui.Label{
Text: row.Header,
Font: balance.LabelFont, Font: balance.LabelFont,
}) },
frame.Pack(label, ui.Pack{ {
Side: ui.W, Label: "This option in the Level menu opens another view into\n" +
PadX: row.PadX, "the level. Has glitchy wallpaper problems.",
})
continue
}
// Checkboxes get their own row.
if row.Boolean != nil {
cb := ui.NewCheckbox(row.Text, row.Boolean, ui.NewLabel(ui.Label{
Text: row.Text,
Font: balance.UIFont, Font: balance.UIFont,
})) },
cb.Handle(ui.Click, onClick) {
cb.Supervise(c.Supervisor) BoolVariable: c.EnableFeatures,
Label: "Enable experimental features",
// Add warning to the toolbars option if the EditMode is currently active.
if row.name == "toolbars" && c.SceneName == "Edit" {
ui.NewTooltip(cb, ui.Tooltip{
Text: "Note: reload your level after changing this option.\n" +
"Playtesting and returning will do.",
Edge: ui.Top,
})
}
frame.Pack(cb, ui.Pack{
Side: ui.W,
PadX: row.PadX,
})
continue
}
// Any leftover Text gets packed to the left.
if row.Text != "" {
tf := ui.NewFrame("TextFrame")
label := ui.NewLabel(ui.Label{
Text: row.Text,
Font: balance.UIFont, Font: balance.UIFont,
OnClick: onClick,
},
{
Label: "Restart the game for changes to take effect.",
Font: balance.UIFont,
},
}) })
tf.Pack(label, ui.Pack{
Side: ui.W,
})
frame.Pack(tf, ui.Pack{
Side: ui.W,
})
}
}
return tab return tab
} }