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 animFrame = animStart;
|
||||
|
||||
setInterval(function() {
|
||||
if (animating) {
|
||||
if (animFrame < animStart || animFrame > animEnd) {
|
||||
animFrame = animStart;
|
||||
}
|
||||
|
||||
animFrame++;
|
||||
if (animFrame === animEnd) {
|
||||
animFrame = animStart;
|
||||
}
|
||||
Self.ShowLayer(animFrame);
|
||||
} else {
|
||||
Self.ShowLayer(animStart);
|
||||
}
|
||||
}, 100);
|
||||
Self.SetGravity(true);
|
||||
Self.AddAnimation("walk-left", 100, ["blu-wl1", "blu-wl2", "blu-wl3", "blu-wl4"]);
|
||||
Self.AddAnimation("walk-right", 100, ["blu-wr1", "blu-wr2", "blu-wr3", "blu-wr4"]);
|
||||
|
||||
Events.OnKeypress(function(ev) {
|
||||
Vx = 0;
|
||||
Vy = 0;
|
||||
|
||||
if (ev.Right.Now) {
|
||||
animStart = 2;
|
||||
animEnd = animStart+4;
|
||||
animating = true;
|
||||
if (!Self.IsAnimating()) {
|
||||
Self.PlayAnimation("walk-right", null);
|
||||
}
|
||||
Vx = playerSpeed;
|
||||
} else if (ev.Left.Now) {
|
||||
animStart = 6;
|
||||
animEnd = animStart+4;
|
||||
animating = true;
|
||||
if (!Self.IsAnimating()) {
|
||||
Self.PlayAnimation("walk-left", null);
|
||||
}
|
||||
Vx = -playerSpeed;
|
||||
} else {
|
||||
Self.StopAnimation();
|
||||
animating = false;
|
||||
}
|
||||
|
||||
if (!Self.Grounded()) {
|
||||
Vy += gravity;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
log.Logger.Config.Level = golog.DebugLevel
|
||||
DebugOverlay = true // on by default in debug mode, F3 to disable
|
||||
}
|
||||
|
||||
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.
|
||||
bitmap := userdir.CacheFilename("chunk", filename+".bmp")
|
||||
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.
|
||||
func (s *PlayScene) movePlayer(ev *events.State) {
|
||||
var playerSpeed = int32(balance.PlayerMaxVelocity)
|
||||
var gravity = int32(balance.Gravity)
|
||||
// var gravity = int32(balance.Gravity)
|
||||
|
||||
var velocity render.Point
|
||||
|
||||
|
@ -190,12 +190,12 @@ func (s *PlayScene) movePlayer(ev *events.State) {
|
|||
velocity.Y = -playerSpeed
|
||||
}
|
||||
|
||||
// Apply gravity if not grounded.
|
||||
if !s.Player.Grounded() {
|
||||
// Gravity has to pipe through the collision checker, too, so it
|
||||
// can't give us a cheated downward boost.
|
||||
velocity.Y += gravity
|
||||
}
|
||||
// // Apply gravity if not grounded.
|
||||
// if !s.Player.Grounded() {
|
||||
// // Gravity has to pipe through the collision checker, too, so it
|
||||
// // can't give us a cheated downward boost.
|
||||
// velocity.Y += gravity
|
||||
// }
|
||||
|
||||
s.Player.SetVelocity(velocity)
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ package scripting
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
"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
|
||||
"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.
|
||||
"setTimeout": vm.SetTimeout,
|
||||
"setInterval": vm.SetInterval,
|
||||
|
|
|
@ -27,6 +27,9 @@ type Actor struct {
|
|||
activeLayer int // active drawing frame for display
|
||||
flagDestroy bool // flag the actor for destruction
|
||||
|
||||
// Actor runtime variables.
|
||||
hasGravity bool
|
||||
|
||||
// Animation variables.
|
||||
animations map[string]*Animation
|
||||
activeAnimation *Animation
|
||||
|
@ -65,6 +68,11 @@ func NewActor(id string, levelActor *level.Actor, doodad *doodads.Doodad) *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.
|
||||
func (a *Actor) LayerCount() int {
|
||||
return len(a.Doodad.Layers)
|
||||
|
|
|
@ -34,12 +34,10 @@ frames left to animate.
|
|||
func (a *Actor) TickAnimation(an *Animation) bool {
|
||||
an.activeLayer++
|
||||
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])
|
||||
} else if an.activeLayer >= len(an.Layers) {
|
||||
// final layer has been shown for 2 ticks, return that the animation has
|
||||
// been concluded.
|
||||
log.Warn("TickAnimation(%s): finished", a.activeAnimation.Name)
|
||||
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.
|
||||
v := a.Velocity()
|
||||
if a.hasGravity {
|
||||
v.Y += int32(balance.Gravity)
|
||||
}
|
||||
|
||||
// If not moving, grab the bounding box right now.
|
||||
if v == render.Origin {
|
||||
|
|