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
build:
doodad convert -t "Bird (red)" left-1.png left-2.png right-1.png right-2.png \
dive-left.png dive-right.png bird-red.doodad
doodad convert -t "Bird (red)" red/left-1.png red/left-2.png red/right-1.png \
red/right-2.png red/dive-left.png red/dive-right.png \
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
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,
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",
lastDirection = "left";
@ -42,6 +60,11 @@ function main() {
lastSampled = Point(0, 0);
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.
if (sampleTick % sampleRate === 0) {
let curP = Self.Position()
@ -65,8 +88,8 @@ function main() {
// If we are not flying at our original altitude, correct for that.
let curV = Self.Position();
Vy = 0.0;
if (curV.Y != altitude) {
Vy = curV.Y < altitude ? 1 : -1;
if (curV.Y != targetAltitude) {
Vy = curV.Y < targetAltitude ? 1 : -1;
}
// Scan for the player character and dive.
@ -114,9 +137,9 @@ function AI_ScanForPlayer() {
return
}
let stepY = 12, // number of pixels to skip
let stepY = searchStep, // number of pixels to skip
stepX = stepY,
limit = stepX * 20, // furthest we'll scan
limit = stepX * searchLimit, // furthest we'll scan
scanX = scanY = 0,
size = Self.Size(),
fromPoint = Self.Position();
@ -147,6 +170,35 @@ function AI_ScanForPlayer() {
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.
function player() {
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"
CheatDropItems = "drop all items"
CheatPlayAsBird = "fly like a bird"
CheatPlayAsBoy = "pinocchio"
CheatPlayAsAzuBlue = "the cell"
CheatPlayAsThief = "play as thief"
CheatPlayAsAnvil = "megaton weight"
CheatGodMode = "god mode"
CheatDebugLoadScreen = "test load screen"
CheatUnlockLevels = "master key"
@ -39,3 +35,15 @@ var (
var (
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
// Number of chunks margin outside the Canvas Viewport for the LoadingViewport.
LoadingViewportMarginChunks = 2
CanvasLoadUnloadModuloTicks uint64 = 4
LoadingViewportMarginChunks = render.NewPoint(8, 4) // hoz, vert
CanvasLoadUnloadModuloTicks uint64 = 2
)
// Edit Mode Values

View File

@ -1,6 +1,7 @@
package doodle
import (
"strings"
"time"
"git.kirsle.net/apps/doodle/pkg/balance"
@ -134,26 +135,6 @@ func (c Command) cheatCommand(d *Doodle) bool {
setPlayerCharacter = true
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:
if isPlay {
d.Flash("God mode toggled")
@ -189,7 +170,15 @@ func (c Command) cheatCommand(d *Doodle) bool {
}
default:
return false
// 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
}
}
// If we're setting the player character and in Play Mode, do it.

View File

@ -20,7 +20,7 @@ type Chunker struct {
// Layer is optional for the caller, levels use only 0 and
// doodads use them for frames. When chunks are exported to
// zipfile the Layer keeps them from overlapping.
Layer int
Layer int `json:"-"` // internal use only
Size int `json:"size"`
// 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.
if c.Zipfile != 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.logChunkAccess(p, chunk) // for the LRU cache
if c.pal != nil {

View File

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

View File

@ -377,10 +377,10 @@ func (w *Canvas) LoadingViewport() render.Rect {
}
return render.Rect{
X: vp.X - chunkSize*margin,
Y: vp.Y - chunkSize*margin,
W: vp.W + chunkSize*margin,
H: vp.H + chunkSize*margin,
X: vp.X - chunkSize*margin.X,
Y: vp.Y - chunkSize*margin.Y,
W: vp.W + chunkSize*margin.X,
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",
Font: balance.UIFont,
BoolVariable: c.HideTouchHints,
OnClick: onClick,
},
{
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",
Font: balance.UIFont,
BoolVariable: c.HorizontalToolbars,
OnClick: onClick,
Tooltip: ui.Tooltip{
Text: "Note: reload your level after changing this option.\n" +
"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",
Font: balance.UIFont,
BoolVariable: c.DisableAutosave,
OnClick: onClick,
},
{
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,
// so we can write the updated info to disk.
onClick := func(ed ui.EventData) error {
onClick := func() {
saveGameSettings()
return nil
}
rows := []struct {
Header string
Text string
Boolean *bool
TextVariable *string
PadY int
PadX int
name string // for special cases
}{
form := magicform.Form{
Supervisor: c.Supervisor,
Engine: c.Engine,
Vertical: true,
LabelWidth: 150,
}
form.Create(tab, []magicform.Field{
{
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" +
"may have unstable or buggy behavior.",
PadY: 2,
Font: balance.UIFont,
},
{
Header: "Viewport window",
Label: "Viewport window",
Font: balance.LabelFont,
},
{
Text: "This option in the Level menu opens another view into\n" +
Label: "This option in the Level menu opens another view into\n" +
"the level. Has glitchy wallpaper problems.",
PadY: 2,
Font: balance.UIFont,
},
{
Boolean: c.EnableFeatures,
Text: "Enable experimental features",
PadX: 4,
BoolVariable: c.EnableFeatures,
Label: "Enable experimental features",
Font: balance.UIFont,
OnClick: onClick,
},
{
Text: "Restart the game for changes to take effect.",
PadY: 2,
Label: "Restart the game for changes to take effect.",
Font: balance.UIFont,
},
}
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,
})
frame.Pack(label, ui.Pack{
Side: ui.W,
PadX: row.PadX,
})
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,
}))
cb.Handle(ui.Click, onClick)
cb.Supervise(c.Supervisor)
// 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,
})
tf.Pack(label, ui.Pack{
Side: ui.W,
})
frame.Pack(tf, ui.Pack{
Side: ui.W,
})
}
}
})
return tab
}