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.
45
dev-assets/doodads/azulian/azulian-red.js
Normal 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);
|
||||||
|
}
|
|
@ -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));
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
BIN
dev-assets/doodads/azulian/red-back.png
Normal file
After Width: | Height: | Size: 826 B |
BIN
dev-assets/doodads/azulian/red-front.png
Normal file
After Width: | Height: | Size: 839 B |
BIN
dev-assets/doodads/azulian/red-wl1.png
Normal file
After Width: | Height: | Size: 803 B |
BIN
dev-assets/doodads/azulian/red-wl2.png
Normal file
After Width: | Height: | Size: 816 B |
BIN
dev-assets/doodads/azulian/red-wl3.png
Normal file
After Width: | Height: | Size: 800 B |
BIN
dev-assets/doodads/azulian/red-wl4.png
Normal file
After Width: | Height: | Size: 819 B |
BIN
dev-assets/doodads/azulian/red-wr1.png
Normal file
After Width: | Height: | Size: 829 B |
BIN
dev-assets/doodads/azulian/red-wr2.png
Normal file
After Width: | Height: | Size: 834 B |
BIN
dev-assets/doodads/azulian/red-wr3.png
Normal file
After Width: | Height: | Size: 815 B |
BIN
dev-assets/doodads/azulian/red-wr4.png
Normal file
After Width: | Height: | Size: 838 B |
189
docs/public/Doodad Scripts.md
Normal 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.
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|