Mobile Enemy Doodad Test

* Add a Red Azulian as a test for mobile enemies.
  * Its A.I. has it walk back and forth, changing directions when it
    comes up against an obstacle for a few moments.
  * It plays walking animations and can trigger collision events with
    other Doodads, such as the Electric Door and Trapdoor.
* Move Gravity responsibility to the doodad scripts themselves.
  * Call `Self.SetGravity(true)` to opt the Doodad in to gravity.
  * The canvas.Loop() adds gravity to any doodad that has it enabled.
This commit is contained in:
Noah 2019-05-05 19:04:02 -07:00
parent a73dec9f31
commit d28745f89e
20 changed files with 275 additions and 34 deletions

View File

@ -0,0 +1,45 @@
function main() {
log.Info("Azulian '%s' initialized!", Self.Doodad.Title);
var playerSpeed = 4;
var gravity = 4;
var Vx = Vy = 0;
var direction = "right";
Self.SetGravity(true);
Self.AddAnimation("walk-left", 100, ["red-wl1", "red-wl2", "red-wl3", "red-wl4"]);
Self.AddAnimation("walk-right", 100, ["red-wr1", "red-wr2", "red-wr3", "red-wr4"]);
// var nextTurn = time.Add(time.Now(), 2500);
// Sample our X position every few frames and detect if we've hit a solid wall.
var sampleTick = 0;
var sampleRate = 5;
var lastSampledX = 0;
setInterval(function() {
// if (time.Now().After(nextTurn)) {
// direction = direction === "right" ? "left" : "right";
// nextTurn = time.Add(time.Now(), 2500);
// }
if (sampleTick % sampleRate === 0) {
var curX = Self.Position().X;
var delta = Math.abs(curX - lastSampledX);
if (delta < 5) {
log.Error("flip red azulian");
direction = direction === "right" ? "left" : "right";
}
lastSampledX = curX;
}
sampleTick++;
var Vx = playerSpeed * (direction === "left" ? -1 : 1);
Self.SetVelocity(Point(Vx, 0));
if (!Self.IsAnimating()) {
Self.PlayAnimation("walk-"+direction, null);
}
}, 100);
}

View File

@ -9,44 +9,29 @@ function main() {
var animStart = animEnd = 0; var animStart = animEnd = 0;
var animFrame = animStart; var animFrame = animStart;
setInterval(function() { Self.SetGravity(true);
if (animating) { Self.AddAnimation("walk-left", 100, ["blu-wl1", "blu-wl2", "blu-wl3", "blu-wl4"]);
if (animFrame < animStart || animFrame > animEnd) { Self.AddAnimation("walk-right", 100, ["blu-wr1", "blu-wr2", "blu-wr3", "blu-wr4"]);
animFrame = animStart;
}
animFrame++;
if (animFrame === animEnd) {
animFrame = animStart;
}
Self.ShowLayer(animFrame);
} else {
Self.ShowLayer(animStart);
}
}, 100);
Events.OnKeypress(function(ev) { Events.OnKeypress(function(ev) {
Vx = 0; Vx = 0;
Vy = 0; Vy = 0;
if (ev.Right.Now) { if (ev.Right.Now) {
animStart = 2; if (!Self.IsAnimating()) {
animEnd = animStart+4; Self.PlayAnimation("walk-right", null);
animating = true; }
Vx = playerSpeed; Vx = playerSpeed;
} else if (ev.Left.Now) { } else if (ev.Left.Now) {
animStart = 6; if (!Self.IsAnimating()) {
animEnd = animStart+4; Self.PlayAnimation("walk-left", null);
animating = true; }
Vx = -playerSpeed; Vx = -playerSpeed;
} else { } else {
Self.StopAnimation();
animating = false; animating = false;
} }
if (!Self.Grounded()) {
Vy += gravity;
}
// Self.SetVelocity(Point(Vx, Vy)); // Self.SetVelocity(Point(Vx, Vy));
}) })
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 834 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

View File

@ -0,0 +1,189 @@
# Doodad Scripts
Each Doodad can have a JavaScript file attached to give them some logic and code
to run in-game when a level is being played. Buttons, trap doors, and other
dynamic doodads have JavaScript code that tells them how to behave.
This game uses [otto](https://github.com/robertkrimen/otto) for its JavaScript
engine, so it only works with ES5 syntax and has a few weird quirks. You get
used to them.
## Example
Provide a script file with a `main` function:
```javascript
function main() {
console.log("%s initialized!", Self.Doodad.Title);
var timer = 0;
Events.OnCollide( function() {
if (timer > 0) {
clearTimeout(timer);
}
Self.ShowLayer(1);
timer = setTimeout(function() {
Self.ShowLayer(0);
timer = 0;
}, 200);
})
}
```
# JavaScript API
# Functions
Global functions available to your script:
## RGBA(uint8 red, uint8 green, uint8 blue, uint8 alpha)
Get a render.Color object from the given color code. Each number
must be between 0 and 255. For example, RGBA(255, 0, 255, 255)
creates an opaque magenta color equivalent to #FF00FF.
The render.Color type may be needed in certain API calls that
require the game's native color type.
## Point(x int, y int)
Get a render.Point object that refers to a position in the game
world.
## Common JavaScript Functions
The following common JavaScript APIs seen in web browsers work
in the doodad scripts:
* int setTimeout(function, int milliseconds)
Set a timeout to run your function after a delay in
milliseconds. Returns a timer ID to be used with
clearTimeout() if you want to cancel the timeout.
* int setInterval(function, int milliseconds)
Like setTimeout, but repeatedly re-runs the function after
the delay in milliseconds. Returns a timer ID to be used
with clearInterval() if you want to cancel the interval.
* clearTimeout(int timerID), clearInterval(int timerID)
Cancel a timeout or interval by passing its timer ID, which
was returned when the timer or interval was first created.
* console.log(str message, v...)
Write to the game's log console. There are also `console.warn`,
`console.error` and `console.log` variants.
## Self
The global variable `Self` holds an API for the current doodad. The full
surface area of this API is subject to change, but some useful examples you
can do with this are as follows.
### Self.Doodad
Self.Doodad is a pointer into the doodad's metadata file. Not
all properties in there can be written to or read from the
JavaScript engine, but some useful attributes are:
* `str Self.Doodad.Title`: the title of the doodad.
* `str Self.Doodad.Author`: the author name of the doodad.
* `str Self.Doodad.Script`: your own source code. Note that
editing this won't have any effect in-game, as your doodad's
source has already been loaded into the interpreter.
* `str Self.Doodad.GameVersion`: the game version that created
the doodad.
### Self.ShowLayer(int index)
Set the doodad's visible layer to the index.
A layer is a drawing created in the in-game format. Only one layer
is visible on-screen during Edit Mode or Play Mode. Layers can be
used to store alternate versions of your doodad to show different
states or as animation frames.
The first and default layer is always zero. Use `CountLayers()`
to query how many layers are in the doodad.
### int Self.CountLayers()
Returns the number of layers, or frames, available in your doodad's
drawing data. Usually these layers are for alternate drawings or animation frames.
The number is the `len()` of the array, and layers are
zero-indexed, so the first and default layer is always layer 0
and the final layer is like so:
```javascript
// set the final frame as the active one
Self.ShowLayer( Self.CountLayers() - 1 );
```
## Animations
### Self.AddAnimation(string name, int interval, [layers...]) error
Add a new animation using some of the layers in your doodad's drawing.
The interval is counted in milliseconds, with 1000 meaning one second between
frames of the animation.
The layers are an array of strings or integers. If strings, use the layer names
from the drawing. With integers, these are the layer index numbers where 0 is
the first (default) layer.
### Self.PlayAnimation(string name, function callback)
Play the named animation. When the animation is finished, the callback function
will be called. Set the callback to `null` if you don't want a callback function.
### Self.StopAnimation()
Stop and cancel any current animations. Their callback functions will not be
called.
### Self.IsAnimating() bool
Returns `true` if an animation is currently playing.
## Events
Use the Events object to register event handlers from your
doodad. Usually you'll configure these in your main() function.
Example configuring event handlers:
```javascript
function main() {
Events.OnCollide(function(e) {
console.log("I've been collided with!");
})
}
```
### OnCollide
Triggers when another doodad has collided with your doodad's box
space on the level. Arguments TBD.
### OnEnter
Triggers when another doodad has fully intersected your doodad's
box.
### OnLeave
Triggers when a doodad who was intersecting your box has left
your box.
### KeypressEvent
Triggers when the player character has pressed a key.
This only triggers when your doodad is the focus of the camera
in-game, i.e. for the player character doodad.

View File

@ -60,6 +60,7 @@ func New(debug bool, engine render.Engine) *Doodle {
if debug { if debug {
log.Logger.Config.Level = golog.DebugLevel log.Logger.Config.Level = golog.DebugLevel
DebugOverlay = true // on by default in debug mode, F3 to disable
} }
return d return d

View File

@ -123,6 +123,8 @@ func (c *Chunk) toBitmap(mask render.Color) string {
) )
} }
log.Info("Chunk<%d>.toBitmap() called", c.Size)
// Get the temp bitmap image. // Get the temp bitmap image.
bitmap := userdir.CacheFilename("chunk", filename+".bmp") bitmap := userdir.CacheFilename("chunk", filename+".bmp")
err := c.ToBitmap(bitmap, mask) err := c.ToBitmap(bitmap, mask)

View File

@ -173,7 +173,7 @@ func (s *PlayScene) Draw(d *Doodle) error {
// movePlayer updates the player's X,Y coordinate based on key pressed. // movePlayer updates the player's X,Y coordinate based on key pressed.
func (s *PlayScene) movePlayer(ev *events.State) { func (s *PlayScene) movePlayer(ev *events.State) {
var playerSpeed = int32(balance.PlayerMaxVelocity) var playerSpeed = int32(balance.PlayerMaxVelocity)
var gravity = int32(balance.Gravity) // var gravity = int32(balance.Gravity)
var velocity render.Point var velocity render.Point
@ -190,12 +190,12 @@ func (s *PlayScene) movePlayer(ev *events.State) {
velocity.Y = -playerSpeed velocity.Y = -playerSpeed
} }
// Apply gravity if not grounded. // // Apply gravity if not grounded.
if !s.Player.Grounded() { // if !s.Player.Grounded() {
// Gravity has to pipe through the collision checker, too, so it // // Gravity has to pipe through the collision checker, too, so it
// can't give us a cheated downward boost. // // can't give us a cheated downward boost.
velocity.Y += gravity // velocity.Y += gravity
} // }
s.Player.SetVelocity(velocity) s.Player.SetVelocity(velocity)

View File

@ -2,6 +2,8 @@ package scripting
import ( import (
"fmt" "fmt"
"reflect"
"time"
"git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
@ -55,6 +57,14 @@ func (vm *VM) RegisterLevelHooks() error {
"Self": vm.Self, // i.e., the uix.Actor object "Self": vm.Self, // i.e., the uix.Actor object
"Events": vm.Events, "Events": vm.Events,
"TypeOf": reflect.TypeOf,
"time": map[string]interface{}{
"Now": time.Now,
"Add": func(t time.Time, ms int64) time.Time {
return t.Add(time.Duration(ms) * time.Millisecond)
},
},
// Timer functions with APIs similar to the web browsers. // Timer functions with APIs similar to the web browsers.
"setTimeout": vm.SetTimeout, "setTimeout": vm.SetTimeout,
"setInterval": vm.SetInterval, "setInterval": vm.SetInterval,

View File

@ -27,6 +27,9 @@ type Actor struct {
activeLayer int // active drawing frame for display activeLayer int // active drawing frame for display
flagDestroy bool // flag the actor for destruction flagDestroy bool // flag the actor for destruction
// Actor runtime variables.
hasGravity bool
// Animation variables. // Animation variables.
animations map[string]*Animation animations map[string]*Animation
activeAnimation *Animation activeAnimation *Animation
@ -65,6 +68,11 @@ func NewActor(id string, levelActor *level.Actor, doodad *doodads.Doodad) *Actor
return actor return actor
} }
// SetGravity configures whether the actor is affected by gravity.
func (a *Actor) SetGravity(v bool) {
a.hasGravity = v
}
// LayerCount returns the number of layers in this actor's drawing. // LayerCount returns the number of layers in this actor's drawing.
func (a *Actor) LayerCount() int { func (a *Actor) LayerCount() int {
return len(a.Doodad.Layers) return len(a.Doodad.Layers)

View File

@ -34,12 +34,10 @@ frames left to animate.
func (a *Actor) TickAnimation(an *Animation) bool { func (a *Actor) TickAnimation(an *Animation) bool {
an.activeLayer++ an.activeLayer++
if an.activeLayer < len(an.Layers) { if an.activeLayer < len(an.Layers) {
log.Warn("TickAnimation(%s): new layer=%d", a.activeAnimation.Name, an.Layers[an.activeLayer])
a.ShowLayer(an.Layers[an.activeLayer]) a.ShowLayer(an.Layers[an.activeLayer])
} else if an.activeLayer >= len(an.Layers) { } else if an.activeLayer >= len(an.Layers) {
// final layer has been shown for 2 ticks, return that the animation has // final layer has been shown for 2 ticks, return that the animation has
// been concluded. // been concluded.
log.Warn("TickAnimation(%s): finished", a.activeAnimation.Name)
return true return true
} }

View File

@ -207,6 +207,9 @@ func (w *Canvas) Loop(ev *events.State) error {
// Get the actor's velocity to see if it's moving this tick. // Get the actor's velocity to see if it's moving this tick.
v := a.Velocity() v := a.Velocity()
if a.hasGravity {
v.Y += int32(balance.Gravity)
}
// If not moving, grab the bounding box right now. // If not moving, grab the bounding box right now.
if v == render.Origin { if v == render.Origin {