diff --git a/dev-assets/doodads/azulian/azulian-red.js b/dev-assets/doodads/azulian/azulian-red.js new file mode 100644 index 0000000..4ff17bc --- /dev/null +++ b/dev-assets/doodads/azulian/azulian-red.js @@ -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); +} diff --git a/dev-assets/doodads/azulian/azulian.js b/dev-assets/doodads/azulian/azulian.js index 25ff361..69bf9c8 100644 --- a/dev-assets/doodads/azulian/azulian.js +++ b/dev-assets/doodads/azulian/azulian.js @@ -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)); }) } diff --git a/dev-assets/doodads/azulian/red-back.png b/dev-assets/doodads/azulian/red-back.png new file mode 100644 index 0000000..2f5d2ef Binary files /dev/null and b/dev-assets/doodads/azulian/red-back.png differ diff --git a/dev-assets/doodads/azulian/red-front.png b/dev-assets/doodads/azulian/red-front.png new file mode 100644 index 0000000..8e7b797 Binary files /dev/null and b/dev-assets/doodads/azulian/red-front.png differ diff --git a/dev-assets/doodads/azulian/red-wl1.png b/dev-assets/doodads/azulian/red-wl1.png new file mode 100644 index 0000000..a05f987 Binary files /dev/null and b/dev-assets/doodads/azulian/red-wl1.png differ diff --git a/dev-assets/doodads/azulian/red-wl2.png b/dev-assets/doodads/azulian/red-wl2.png new file mode 100644 index 0000000..a4f7468 Binary files /dev/null and b/dev-assets/doodads/azulian/red-wl2.png differ diff --git a/dev-assets/doodads/azulian/red-wl3.png b/dev-assets/doodads/azulian/red-wl3.png new file mode 100644 index 0000000..00c776d Binary files /dev/null and b/dev-assets/doodads/azulian/red-wl3.png differ diff --git a/dev-assets/doodads/azulian/red-wl4.png b/dev-assets/doodads/azulian/red-wl4.png new file mode 100644 index 0000000..9b0c599 Binary files /dev/null and b/dev-assets/doodads/azulian/red-wl4.png differ diff --git a/dev-assets/doodads/azulian/red-wr1.png b/dev-assets/doodads/azulian/red-wr1.png new file mode 100644 index 0000000..dfcfffe Binary files /dev/null and b/dev-assets/doodads/azulian/red-wr1.png differ diff --git a/dev-assets/doodads/azulian/red-wr2.png b/dev-assets/doodads/azulian/red-wr2.png new file mode 100644 index 0000000..a79f3a3 Binary files /dev/null and b/dev-assets/doodads/azulian/red-wr2.png differ diff --git a/dev-assets/doodads/azulian/red-wr3.png b/dev-assets/doodads/azulian/red-wr3.png new file mode 100644 index 0000000..5e79c36 Binary files /dev/null and b/dev-assets/doodads/azulian/red-wr3.png differ diff --git a/dev-assets/doodads/azulian/red-wr4.png b/dev-assets/doodads/azulian/red-wr4.png new file mode 100644 index 0000000..219ec34 Binary files /dev/null and b/dev-assets/doodads/azulian/red-wr4.png differ diff --git a/docs/public/Doodad Scripts.md b/docs/public/Doodad Scripts.md new file mode 100644 index 0000000..72dc87a --- /dev/null +++ b/docs/public/Doodad Scripts.md @@ -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. diff --git a/pkg/doodle.go b/pkg/doodle.go index 4cc24de..3812a99 100644 --- a/pkg/doodle.go +++ b/pkg/doodle.go @@ -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 diff --git a/pkg/level/chunk.go b/pkg/level/chunk.go index fda8515..33c12aa 100644 --- a/pkg/level/chunk.go +++ b/pkg/level/chunk.go @@ -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) diff --git a/pkg/play_scene.go b/pkg/play_scene.go index b502e83..c2ae7ab 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -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) diff --git a/pkg/scripting/vm.go b/pkg/scripting/vm.go index b1b6543..63d0082 100644 --- a/pkg/scripting/vm.go +++ b/pkg/scripting/vm.go @@ -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, diff --git a/pkg/uix/actor.go b/pkg/uix/actor.go index 4405a5f..48d11d2 100644 --- a/pkg/uix/actor.go +++ b/pkg/uix/actor.go @@ -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) diff --git a/pkg/uix/actor_animation.go b/pkg/uix/actor_animation.go index 585163c..ea25dd1 100644 --- a/pkg/uix/actor_animation.go +++ b/pkg/uix/actor_animation.go @@ -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 } diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index f0ed9ff..78ac0e1 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -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 {