Tighten Doodad JavaScript API, User Documentation
* Tightens up the surface area of API methods available to the JavaScript VMs for doodads. Variables and functions are carefully passed in one-by-one so the doodad script can only access intended functions and not snoop on undocumented APIs. * Wrote tons of user documentation for Doodad Scripts: documented the full surface area of the exposed JavaScript API now that the surface area is known and limited. * Early WIP code for the Campaign JSON
This commit is contained in:
parent
44788e8032
commit
38614ee280
16
assets/campaigns/tutorial.json
Normal file
16
assets/campaigns/tutorial.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"version": 1,
|
||||
"title": "Tutorial Levels",
|
||||
"author": "kirsle",
|
||||
"levels": [
|
||||
{
|
||||
"filename": "example1.level",
|
||||
},
|
||||
{
|
||||
"filename": "example2.level",
|
||||
},
|
||||
{
|
||||
"filename": "example3.level",
|
||||
}
|
||||
]
|
||||
}
|
|
@ -32,7 +32,7 @@ func init() {
|
|||
Usage: "set the doodad author",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "tag",
|
||||
Name: "tag, t",
|
||||
Usage: "set a key/value tag on the doodad, in key=value format. Empty value deletes the tag.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
function main() {
|
||||
log.Info("Azulian '%s' initialized!", Self.Doodad().Title);
|
||||
console.log("Azulian '%s' initialized!", Self.Title);
|
||||
|
||||
var playerSpeed = 4;
|
||||
var gravity = 4;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
function main() {
|
||||
console.log("%s initialized!", Self.Doodad().Title);
|
||||
console.log("%s initialized!", Self.Title);
|
||||
|
||||
var timer = 0;
|
||||
|
||||
|
@ -26,9 +26,4 @@ function main() {
|
|||
timer = 0;
|
||||
}, 200);
|
||||
});
|
||||
|
||||
// Events.OnLeave(function(e) {
|
||||
// console.log("%s has stopped touching %s", e, Self.Doodad().Title)
|
||||
// Self.Canvas.SetBackground(RGBA(0, 0, 1, 0));
|
||||
// })
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
function main() {
|
||||
console.log("%s initialized!", Self.Doodad().Title);
|
||||
console.log("%s initialized!", Self.Title);
|
||||
|
||||
var pressed = false;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
function main() {
|
||||
var color = Self.Doodad().Tag("color");
|
||||
var color = Self.GetTag("color");
|
||||
var keyname = "key-" + color + ".doodad";
|
||||
|
||||
// Layers in the doodad image.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
function main() {
|
||||
console.log("%s initialized!", Self.Doodad().Title);
|
||||
console.log("%s initialized!", Self.Title);
|
||||
|
||||
Self.AddAnimation("open", 100, [0, 1, 2, 3]);
|
||||
Self.AddAnimation("close", 100, [3, 2, 1, 0]);
|
||||
|
@ -9,7 +9,7 @@ function main() {
|
|||
Self.SetHitbox(16, 0, 32, 64);
|
||||
|
||||
Message.Subscribe("power", function(powered) {
|
||||
console.log("%s got power=%+v", Self.Doodad().Title, powered);
|
||||
console.log("%s got power=%+v", Self.Title, powered);
|
||||
|
||||
if (powered) {
|
||||
if (animating || opened) {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
function main() {
|
||||
var color = Self.Doodad().Tag("color");
|
||||
var color = Self.GetTag("color");
|
||||
|
||||
Events.OnCollide(function(e) {
|
||||
if (e.Settled) {
|
||||
e.Actor.AddItem(Self.Doodad().Filename, 0);
|
||||
e.Actor.AddItem(Self.Filename, 0);
|
||||
Self.Destroy();
|
||||
}
|
||||
})
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
function main() {
|
||||
Self.AddAnimation("open", 0, [1]);
|
||||
var unlocked = false;
|
||||
var color = Self.Doodad().Tag("color");
|
||||
var color = Self.GetTag("color");
|
||||
|
||||
Self.SetHitbox(16, 0, 32, 64);
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Exit Flag.
|
||||
function main() {
|
||||
console.log("%s initialized!", Self.Doodad().Title);
|
||||
console.log("%s initialized!", Self.Title);
|
||||
Self.SetHitbox(22+16, 16, 75-16, 86);
|
||||
|
||||
Events.OnCollide(function(e) {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
var state = false;
|
||||
|
||||
function main() {
|
||||
console.log("%s ID '%s' initialized!", Self.Doodad().Title, Self.ID());
|
||||
console.log("%s ID '%s' initialized!", Self.Title, Self.ID());
|
||||
Self.SetHitbox(0, 0, 33, 33);
|
||||
|
||||
// When the button is activated, don't keep toggling state until we're not
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
function main() {
|
||||
console.log("%s initialized!", Self.Doodad().Title);
|
||||
console.log("%s initialized!", Self.Title);
|
||||
|
||||
// Switch has two frames:
|
||||
// 0: Off
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
function main() {
|
||||
console.log("%s initialized!", Self.Doodad().Title);
|
||||
console.log("%s initialized!", Self.Title);
|
||||
|
||||
var timer = 0;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
function main() {
|
||||
// What direction is the trapdoor facing?
|
||||
var direction = Self.Doodad().Tag("direction");
|
||||
var direction = Self.GetTag("direction");
|
||||
console.log("Trapdoor(%s) initialized", direction);
|
||||
|
||||
var timer = 0;
|
||||
|
|
|
@ -3,20 +3,18 @@
|
|||
Doodads are programmed using JavaScript which gives them their behavior
|
||||
and ability to interact with the player and other doodads.
|
||||
|
||||
Doodad scripts are run during "Play Mode" when a level _containing_ the doodad
|
||||
is being played. You can install a JavaScript (.js) file into a doodad using
|
||||
the command-line `doodad` program.
|
||||
|
||||
An example Doodad script looks like the following:
|
||||
|
||||
```javascript
|
||||
// The main function is called when the doodad is initialized in Play Mode
|
||||
// at the start of the level.
|
||||
// main() is called on level initialization for each
|
||||
// instance ("actor") of the doodad.
|
||||
function main() {
|
||||
// Important global variables:
|
||||
// - Self: information about the current Doodad running this script.
|
||||
// - Events: handle events raised during gameplay.
|
||||
// - Message: publish or subscribe to named messages to interact with
|
||||
// other doodads.
|
||||
|
||||
// Logs go to the game's log file (standard output on Linux/Mac).
|
||||
console.log("%s initialized!", Self.Doodad().Title);
|
||||
console.log("%s initialized!", Self.Title);
|
||||
|
||||
// If our doodad has 'solid' parts that should prohibit movement,
|
||||
// define the hitbox here. Coordinates are relative so 0,0 is the
|
||||
|
@ -37,6 +35,14 @@ function main() {
|
|||
// moving through.
|
||||
return false;
|
||||
}
|
||||
|
||||
// When movement is finalized, OnCollide is called one final time
|
||||
// with e.Settled=true; it is only then that a doodad should run
|
||||
// event handlers for a logical collide event.
|
||||
if (e.Settled) {
|
||||
// do something
|
||||
Message.Publish("power", true);
|
||||
}
|
||||
});
|
||||
|
||||
// OnLeave is called when an actor, who was previously colliding with
|
||||
|
@ -47,17 +53,230 @@ function main() {
|
|||
}
|
||||
```
|
||||
|
||||
# JavaScript API
|
||||
# Installing a Doodad Script
|
||||
|
||||
## Global Variables
|
||||
Use the command-line `doodad` tool to attach a script to your doodad file:
|
||||
|
||||
```bash
|
||||
# Attach the JavaScript at "script.js" to the doodad file "filename.doodad"
|
||||
doodad install-script script.js filename.doodad
|
||||
|
||||
# To view the script currently attached to a doodad
|
||||
# (prints the script to your terminal)
|
||||
doodad show --script filename.doodad
|
||||
```
|
||||
|
||||
# Testing Your Script
|
||||
|
||||
The best way to test your doodad script is to use it in a level!
|
||||
|
||||
Run the game in a console to watch the log output, and you can use functions
|
||||
like `console.log()` in your script to help debug issues. Drag your custom
|
||||
doodad into a level and playtest it! Your script's main() function is called
|
||||
when the level instance of your doodad is initialized.
|
||||
|
||||
# JavaScript API
|
||||
|
||||
The following global variables are available to all Doodad scripts.
|
||||
|
||||
### Self
|
||||
## Self
|
||||
|
||||
Self holds data about the current doodad instance loaded inside of a level.
|
||||
|
||||
**String attributes:**
|
||||
|
||||
* Self.Title: the doodad title.
|
||||
* Self.Filename: the doodad filename (useful for inventory items).
|
||||
|
||||
Methods are below.
|
||||
|
||||
### Self.ID() string
|
||||
|
||||
Returns the "actor ID" of the doodad instance loaded inside of a level. This
|
||||
is usually a random UUID string that was saved with the level data.
|
||||
|
||||
### Self.GetTag(string name) string
|
||||
|
||||
Return a "tag" that was saved with the doodad's file data.
|
||||
|
||||
Tags are an arbitrary key/value data store attached to the doodad file.
|
||||
You can use the `doodad.exe` tool shipped with the game to view and manage tags
|
||||
on your own custom doodads:
|
||||
|
||||
```bash
|
||||
# Command-line doodad tool usage:
|
||||
|
||||
# Show information about a doodad, look for the "Tags:" section.
|
||||
doodad show filename.doodad
|
||||
|
||||
# Set a tag. "-t" for short.
|
||||
doodad edit-doodad --tag 'color=blue' filename.doodad
|
||||
|
||||
# Set the tag to empty to remove it.
|
||||
doodad edit-doodad -t 'color=' filename.doodad
|
||||
```
|
||||
|
||||
This is useful for a set of multiple doodads to share the same script but
|
||||
have different behavior depending on how each is tagged.
|
||||
|
||||
### Self.Position() Point
|
||||
|
||||
Returns the doodad's current position in the level.
|
||||
|
||||
Point is an object with .X and .Y integer values.
|
||||
|
||||
```javascript
|
||||
var p = Self.Position()
|
||||
console.log("I am at %d,%d", p.X, p.Y)
|
||||
```
|
||||
|
||||
### Self.SetHitbox(x, y, w, h int)
|
||||
|
||||
Configure the "solid hitbox" of this doodad.
|
||||
|
||||
The X and Y coordinates are relative to the doodad's sprite: (0,0) is the top
|
||||
left pixel of the doodad. The W and H are the width and height of the hitbox
|
||||
starting at those coordinates.
|
||||
|
||||
When another doodad enters the area of your doodad's sprite (for example, the
|
||||
player character has entered the square shape of your doodad sprite) your script
|
||||
begins to receive OnCollide events from the approaching actor.
|
||||
|
||||
The OnCollide event tells you if the invading doodad is inside your custom
|
||||
hitbox which you define here (`InHitbox`) making it easy to make choices based
|
||||
on that status.
|
||||
|
||||
Here's an example script for a hypothetical "locked door" doodad that acts
|
||||
solid but only on a thin rectangle in the middle of its sprite:
|
||||
|
||||
```javascript
|
||||
// Example script for a "locked door"
|
||||
function main() {
|
||||
// Suppose the doodad's sprite size is 64x64 pixels square.
|
||||
// The door is in side profile where the door itself ranges from pixels
|
||||
// (20, 0) to (24, 64)
|
||||
Self.SetHitbox(20, 0, 24, 64)
|
||||
|
||||
// OnCollide handlers.
|
||||
Events.OnCollide(function(e) {
|
||||
// The convenient e.InHitbox tells you if the colliding actor is
|
||||
// inside the hitbox we defined.
|
||||
if (e.InHitbox) {
|
||||
// Return false to protest the collision (act solid).
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Self.SetVelocity(Velocity)
|
||||
|
||||
Set the doodad's velocity. Velocity is a type that can be created with the
|
||||
Velocity() constructor, which takes an X and Y value:
|
||||
|
||||
```javascript
|
||||
Self.SetVelocity( Velocity(3.2, 7.0) );
|
||||
```
|
||||
|
||||
A positive X velocity propels the doodad to the right. A positive Y velocity
|
||||
propels the doodad downward.
|
||||
|
||||
### Self.SetMobile(bool)
|
||||
|
||||
Call `SetMobile(true)` if the doodad will move on its own.
|
||||
|
||||
This is for mobile doodads such as the player character and enemy mobs.
|
||||
Stationary doodads like buttons, doors, and trapdoors do not mark themselves
|
||||
as mobile.
|
||||
|
||||
Mobile doodads incur extra work for the game doing collision checking so only
|
||||
set this to `true` if your doodad will move (i.e. changes its Velocity or
|
||||
Position).
|
||||
|
||||
```javascript
|
||||
Self.SetMobile(true);
|
||||
```
|
||||
|
||||
### Self.SetGravity(bool)
|
||||
|
||||
Set whether gravity applies to this doodad. By default doodads are stationary
|
||||
and do not fall downwards. The player character and some mobile enemies that
|
||||
want to be affected by gravity should opt in to this.
|
||||
|
||||
```javascript
|
||||
Self.SetGravity(true);
|
||||
```
|
||||
|
||||
### Self.ShowLayer(index int)
|
||||
|
||||
Switch the active layer of the doodad to the layer at this index.
|
||||
|
||||
A doodad file can contain multiple layers, or images. The first and default
|
||||
layer is at index zero, the second layer at index 1, and so on.
|
||||
|
||||
```javascript
|
||||
Self.ShowLayer(0); // 0 is the first and default layer
|
||||
Self.ShowLayer(1); // show the second layer instead
|
||||
```
|
||||
|
||||
### Self.ShowLayerNamed(name string)
|
||||
|
||||
Switch the active layer by name instead of index.
|
||||
|
||||
Each layer has an arbitrary name that it can be addressed by instead of needing
|
||||
to keep track of the layer index.
|
||||
|
||||
Doodads created by the command-line `doodad` tool will have their layers named
|
||||
automatically by their file name. The layer **indexes** will retain the same
|
||||
order of file names passed in, with 0 being the first file:
|
||||
|
||||
```bash
|
||||
# Doodad tool-created doodads have layers named after their file names.
|
||||
# example "open-1.png" will be named "open-1"
|
||||
doodad convert door.png open-1.png open-2.png open-3.png my-door.doodad
|
||||
```
|
||||
|
||||
### Self.AddAnimation(name string, interval int, layers list)
|
||||
|
||||
Register a named animation for your doodad. `interval` is the time in
|
||||
milliseconds before going to the next frame. `layers` is an array of layer
|
||||
names or indexes to be used for the animation.
|
||||
|
||||
Doodads can animate by having multiple frames (images) in the same file.
|
||||
Layers are ordered (layer 0 is the first, then increments from there) and
|
||||
each has a name. This function can take either identifier to specify
|
||||
which layers are part of the animation.
|
||||
|
||||
```javascript
|
||||
// Animation named "open" using named layers, 100ms delay between frames.
|
||||
Self.AddAnimation("open", 100, ["open-1", "open-2", "open-3"]);
|
||||
|
||||
// Animation named "close" using layers by index.
|
||||
Self.AddAnimation("close", 100, [3, 2, 1]);
|
||||
```
|
||||
|
||||
### Self.PlayAnimation(name string, callback func())
|
||||
|
||||
This starts playing the named animation. The callback function will be called
|
||||
when the animation has completed.
|
||||
|
||||
```javascript
|
||||
Self.PlayAnimation("open", function() {
|
||||
console.log("I've finished opening!");
|
||||
|
||||
// The callback is optional; use null if you don't need it.
|
||||
Self.PlayAnimation("close", null);
|
||||
});
|
||||
```
|
||||
|
||||
### Self.IsAnimating() bool
|
||||
|
||||
Returns true if an animation is currently being played.
|
||||
|
||||
### Self.StopAnimation()
|
||||
|
||||
Stops any currently playing animation.
|
||||
|
||||
Self holds information about the current doodad. The full surface area of
|
||||
the Self object is subject to change, but some useful things you can access
|
||||
from it include:
|
||||
|
||||
* Self.Doodad(): a pointer to the doodad's file data.
|
||||
* Self.Doodad().Title: get the title of the doodad file.
|
||||
|
@ -68,63 +287,278 @@ from it include:
|
|||
* Self.Doodad().GameVersion: the version of {{ app_name }} that was used
|
||||
when the doodad was created.
|
||||
|
||||
### Events
|
||||
### Self.Destroy()
|
||||
|
||||
### Message
|
||||
This destroys the current instance of the doodad as it appears in a level.
|
||||
|
||||
## Global Functions
|
||||
For example, a Key destroys itself when it's picked up so that it disappears
|
||||
from the level and can't be picked up again. Call this function when the
|
||||
doodad instance should be destroyed and removed from the active level.
|
||||
|
||||
The following useful functions are also available globally:
|
||||
-----
|
||||
|
||||
### Timers and Intervals
|
||||
## Console Logging
|
||||
|
||||
Doodad scripts implement setTimeout() and setInterval() functions similar
|
||||
to those found in web browsers.
|
||||
|
||||
```javascript
|
||||
// Call a function after 5 seconds.
|
||||
setTimeout(function() {
|
||||
console.log("I've been called!");
|
||||
}, 5000);
|
||||
```
|
||||
|
||||
setTimeout() and setInterval() return an ID number for the timer created.
|
||||
If you wish to cancel a timer before it has finished, or to stop an interval
|
||||
from running, you need to pass its ID number into `clearTimeout()` or
|
||||
`clearInterval()`, respectively.
|
||||
|
||||
```javascript
|
||||
// Start a 1-second interval
|
||||
var id = setInterval(function() {
|
||||
console.log("Tick...");
|
||||
}, 1000);
|
||||
|
||||
// Cancel it after 30 seconds.
|
||||
setTimeout(function() {
|
||||
clearInterval(id);
|
||||
}, 30000);
|
||||
```
|
||||
|
||||
### Console Logging
|
||||
|
||||
Doodad scripts also implement the `console.log()` and similar functions as
|
||||
found in web browser APIs. They support "printf" style variable placeholders.
|
||||
Like in node.js and the web browser, `console.log` and friends are available
|
||||
for logging from a doodad script. Logs are emitted to the same place as the
|
||||
game's logs are.
|
||||
|
||||
```javascript
|
||||
console.log("Hello world!");
|
||||
console.error("The answer is %d!", 42);
|
||||
console.warn("Actor '%s' has collided with us!", e.Actor.ID());
|
||||
console.debug("This only logs when the game is in debug mode!");
|
||||
console.log("Interpolate strings '%s' and numbers '%d'", "string", 123);
|
||||
console.debug("Debug messages shown when the game is in debug mode");
|
||||
console.warn("Warning-level messages");
|
||||
console.error("Error-level messages");
|
||||
```
|
||||
|
||||
-----
|
||||
|
||||
## Timers and Intervals
|
||||
|
||||
Like in a web browser, functions such as setTimeout and setInterval are
|
||||
supported in doodad scripts.
|
||||
|
||||
### setTimeout(function, milliseconds int) int
|
||||
|
||||
setTimeout calls your function after the specified number of milliseconds.
|
||||
|
||||
1000ms are in one second.
|
||||
|
||||
Returns an integer "timeout ID" that you'll need if you want to cancel the
|
||||
timeout with clearTimeout.
|
||||
|
||||
### setInterval(function, milliseconds int) int
|
||||
|
||||
setInterval calls your function repeatedly after every specified number of
|
||||
milliseconds.
|
||||
|
||||
Returns an integer "interval ID" that you'll need if you want to cancel the
|
||||
interval with clearInterval.
|
||||
|
||||
### clearTimeout(id int)
|
||||
|
||||
Cancels the timeout with the given ID.
|
||||
|
||||
### clearInterval(id int)
|
||||
|
||||
Cancels the interval with the given ID.
|
||||
|
||||
-----
|
||||
|
||||
## Type Constructors
|
||||
|
||||
Some methods may need data of certain native types that aren't available in
|
||||
JavaScript. These global functions will initialize data of the correct types:
|
||||
|
||||
### RGBA(red, green, blue, alpha uint8)
|
||||
|
||||
RGBA initializes a Color variable using the game's native Color type. May
|
||||
be useful for certain game APIs that take color values.
|
||||
|
||||
Example: RGBA(255, 0, 255, 255) creates an opaque magenta color.
|
||||
Creates a Color type from red, green, blue and alpha values (integers between
|
||||
0 and 255).
|
||||
|
||||
### Point(x, y int)
|
||||
|
||||
Returns a Point object which refers to a location in the game world. This
|
||||
type is required for certain game APIs.
|
||||
Creates a Point object with X and Y coordinates.
|
||||
|
||||
### Vector(x, y float64)
|
||||
|
||||
Creates a Vector object with X and Y dimensions.
|
||||
|
||||
-----
|
||||
|
||||
## Global Functions
|
||||
|
||||
Some useful globally available functions:
|
||||
|
||||
### EndLevel()
|
||||
|
||||
This ends the current level, i.e. to be used by the goal flag.
|
||||
|
||||
### Flash(message string, args...)
|
||||
|
||||
Flash a message on screen to the user.
|
||||
|
||||
Flashed messages appear at the bottom of the screen and fade out after a few
|
||||
moments. If multiple messages are flashed at the same time, they stack from the
|
||||
bottom of the window with the newest message on bottom.
|
||||
|
||||
Don't abuse this feature as spamming it may annoy the player.
|
||||
|
||||
### GetTick() uint64
|
||||
|
||||
Returns the current game tick. This value started at zero when the game was
|
||||
launched and increments every frame while running.
|
||||
|
||||
### time.Now() time.Time
|
||||
|
||||
This exposes the Go standard library function `time.Now()` that returns the
|
||||
current date and time as a Go time.Time value.
|
||||
|
||||
### time.Add(t time.Time, milliseconds int64) time.Time
|
||||
|
||||
Add a number of milliseconds to a Go Time value.
|
||||
|
||||
--------
|
||||
|
||||
## Event Handlers
|
||||
|
||||
Doodad scripts can respond to certain events using functions on the global
|
||||
`Events` variable.
|
||||
|
||||
### Events.OnCollide( func(event) )
|
||||
|
||||
OnCollide is called when another actor is colliding with your doodad's sprite
|
||||
box. The function is given a CollideEvent object which has the following
|
||||
attributes:
|
||||
|
||||
* Actor: the doodad which is colliding with your doodad.
|
||||
* Overlap (Rect): a rectangle of where the two doodads' boxes are overlapping,
|
||||
relative to your doodad sprite's box. That is, if the Actor was moving in from
|
||||
the left side of your doodad, the X value would be zero and W would be the
|
||||
number of pixels of overlap.
|
||||
* InHitbox (bool): true if the colliding actor's hitbox is intersecting with
|
||||
the hitbox you defined with SetHitbox().
|
||||
* Settled (bool): This is `false` when the game is trying to move the colliding
|
||||
doodad and is sussing out whether or not your doodad will act solid and
|
||||
protest its movement. When the game has settled the location of the colliding
|
||||
doodad it will call OnCollide a final time with Settled=true. If your doodad
|
||||
has special behavior when touched (i.e. a button that presses in), you should
|
||||
wait until Settled=true before running your handler for that.
|
||||
|
||||
### Events.OnLeave( func(event) )
|
||||
|
||||
Called when an actor that _was_ colliding with your doodad is no longer
|
||||
colliding (or has left your doodad's sprite box).
|
||||
|
||||
### Events.RunKeypress( func(event) )
|
||||
|
||||
Handle a keypress. `event` is an `event.State` from the render engine.
|
||||
|
||||
TODO: document that.
|
||||
|
||||
-----
|
||||
|
||||
## Pub/Sub Communication
|
||||
|
||||
Doodads in a level are able to send and receive messages to other doodads,
|
||||
either those that they are **linked** to or those that listen on a more
|
||||
'broadcast' frequency.
|
||||
|
||||
> **Linking** is when the level author connected two doodads together with
|
||||
> the Link Tool. The two doodads' scripts can communicate with each other
|
||||
> in-game over that link.
|
||||
|
||||
For example, if the level author links a Button to an Electric Door, the button
|
||||
can send a "power" event to the door so that it can open when a player touches
|
||||
the button.
|
||||
|
||||
Doodads communicate in a "publisher/subscriber" model: one doodad publishes an
|
||||
event with a name and data, and other doodads subscribe to the named event to
|
||||
receive that data.
|
||||
|
||||
### Official, Standard Pub/Sub Messages
|
||||
|
||||
The following message names and data types are used by the game's default
|
||||
doodads. You're free to use these in your own custom doodads.
|
||||
|
||||
If extending this list with your own custom events, be careful to choose a
|
||||
unique namespace to prevent collision with other users' custom doodads and
|
||||
their custom event names.
|
||||
|
||||
| Name | Data Type | Description |
|
||||
|------|-----------|--------------|
|
||||
| power | boolean | Communicates a "powered" (true) or "not powered" state, as in a Button to an Electric Door. |
|
||||
| broadcast:state-change | boolean | An "ON/OFF" button was hit and all state blocks should flip. |
|
||||
|
||||
### Message.Publish(name string, data...)
|
||||
|
||||
Publish a named message to all of your **linked** doodads.
|
||||
|
||||
`data` is a list of arbitrary arguments to send with the message.
|
||||
|
||||
```javascript
|
||||
// Example button doodad that emits a "power" (bool) state to linked doodads
|
||||
// that subscribe to this event.
|
||||
function main() {
|
||||
// When an actor collides with the button, emit a powered state.
|
||||
Events.OnCollide(function(e) {
|
||||
Message.Publish("power", true);
|
||||
});
|
||||
|
||||
// When the actor leaves the button, turn off the power.
|
||||
Events.OnLeave(function(e) {
|
||||
Message.Publish("power", false);
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Message.Subscribe(name string, function)
|
||||
|
||||
Subscribe to a named message from any **linked** doodads.
|
||||
|
||||
The function receives the data that was passed with the message. Its data type
|
||||
is arbitrary and will depend on the type of message.
|
||||
|
||||
```javascript
|
||||
// Example electronic device doodad that responds to power from linked buttons.
|
||||
function main() {
|
||||
// Boolean to store if our electric device has juice.
|
||||
var powered = false;
|
||||
|
||||
// Do something while powered
|
||||
setInterval(function() {
|
||||
if (powered) {
|
||||
console.log("Brmm...")
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Subscribe to the `power` event by a linked button or other power source.
|
||||
Message.Subscribe("power", function(boolValue) {
|
||||
console.log("Powered %s!", boolValue === true ? "on" : "off");
|
||||
powered = boolValue;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Message.Broadcast(name string, data...)
|
||||
|
||||
This publishes a named message to **every** doodad in the level, whether it
|
||||
was linked to the broadcaster or not.
|
||||
|
||||
For example the "ON/OFF" button globally toggles a boolean state in every
|
||||
state block that subscribes to the `broadcast:state-change` event.
|
||||
|
||||
If you were to broadcast an event like `power` it would activate every single
|
||||
power-sensitive doodad on the level.
|
||||
|
||||
```javascript
|
||||
// Example two-state block that globally receives the state-change broadcast.
|
||||
function main() {
|
||||
var myState = false;
|
||||
Message.Subscribe("broadcast:state-change", function(boolValue) {
|
||||
// Most two-state blocks just flip their own state, ignoring the
|
||||
// boolValue passed with this message.
|
||||
myState = !myState;
|
||||
});
|
||||
}
|
||||
|
||||
// Example ON/OFF block that emits the state-change broadcast. It also
|
||||
// subscribes to the event to keep its own state in sync with all the other
|
||||
// ON/OFF blocks on the level when they get hit.
|
||||
function main() {
|
||||
var myState = false;
|
||||
|
||||
// Listen for other ON/OFF button activations to keep our state in
|
||||
// sync with theirs.
|
||||
Message.Subscribe("broadcast:state-change", function(boolValue) {
|
||||
myState = boolValue;
|
||||
});
|
||||
|
||||
// When collided with, broadcast the state toggle to all state blocks.
|
||||
Events.OnCollide(function(e) {
|
||||
if (e.Settled) {
|
||||
myState = !!myState;
|
||||
Message.Broadcast("broadcast:state-change", myState);
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
|
|
@ -7,6 +7,14 @@ import (
|
|||
|
||||
// Theme and appearance variables.
|
||||
var (
|
||||
// Title Screen Font
|
||||
TitleScreenFont = render.Text{
|
||||
Size: 46,
|
||||
Color: render.Pink,
|
||||
Stroke: render.SkyBlue,
|
||||
Shadow: render.Black,
|
||||
}
|
||||
|
||||
// Window and panel styles.
|
||||
TitleConfig = ui.Config{
|
||||
Background: render.MustHexColor("#FF9900"),
|
||||
|
|
62
pkg/campaign/campaign.go
Normal file
62
pkg/campaign/campaign.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
// Package campaign contains types and functions for the single player campaigns.
|
||||
package campaign
|
||||
|
||||
// Campaign structure for the JSON campaign files.
|
||||
type Campaign struct {
|
||||
Version int `json:"version"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
Levels []Level `json:"levels"`
|
||||
}
|
||||
|
||||
// Level is the "levels" object of the JSON file.
|
||||
type Level struct {
|
||||
Filename string `json:"filename"`
|
||||
}
|
||||
|
||||
// LoadFile reads a campaign file from disk, checking a few locations.
|
||||
// func LoadFile(filename string) (*Level, error) {
|
||||
// // Search the system and user paths for this level.
|
||||
// filename, err := filesystem.FindFile(filename)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// // Do we have the file in bindata?
|
||||
// if jsonData, err := bindata.Asset(filename); err == nil {
|
||||
// log.Info("loaded from embedded bindata")
|
||||
// return FromJSON(filename, jsonData)
|
||||
// }
|
||||
//
|
||||
// // WASM: try the file from localStorage or HTTP ajax request.
|
||||
// if runtime.GOOS == "js" {
|
||||
// if result, ok := wasm.GetSession(filename); ok {
|
||||
// log.Info("recall level data from localStorage")
|
||||
// return FromJSON(filename, []byte(result))
|
||||
// }
|
||||
//
|
||||
// // Ajax request.
|
||||
// jsonData, err := wasm.HTTPGet(filename)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// return FromJSON(filename, jsonData)
|
||||
// }
|
||||
//
|
||||
// // Try the binary format.
|
||||
// if level, err := LoadBinary(filename); err == nil {
|
||||
// return level, nil
|
||||
// } else {
|
||||
// log.Warn(err.Error())
|
||||
// }
|
||||
//
|
||||
// // Then the JSON format.
|
||||
// if level, err := LoadJSON(filename); err == nil {
|
||||
// return level, nil
|
||||
// } else {
|
||||
// log.Warn(err.Error())
|
||||
// }
|
||||
//
|
||||
// return nil, errors.New("invalid file type")
|
||||
// }
|
60
pkg/campaign/files.go
Normal file
60
pkg/campaign/files.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
// Package campaign contains types and functions for the single player campaigns.
|
||||
package campaign
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
|
||||
"git.kirsle.net/apps/doodle/pkg/bindata"
|
||||
"git.kirsle.net/apps/doodle/pkg/filesystem"
|
||||
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||
)
|
||||
|
||||
// List returns the list of available campaign JSONs.
|
||||
//
|
||||
// It searches in:
|
||||
// - The embedded bindata for built-in scenarios
|
||||
// - Scenarios on disk at the assets/campaigns folder.
|
||||
// - User-made scenarios at ~/doodle/campaigns.
|
||||
func List() ([]string, error) {
|
||||
var names []string
|
||||
|
||||
// List built-in bindata campaigns.
|
||||
if files, err := bindata.AssetDir("assets/campaigns"); err == nil {
|
||||
names = append(names, files...)
|
||||
}
|
||||
|
||||
// WASM: only built-in campaigns, no filesystem access.
|
||||
if runtime.GOOS == "js" {
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// Read system-level doodads first. Ignore errors, if the system path is
|
||||
// empty we still go on to read the user directory.
|
||||
files, _ := ioutil.ReadDir(filesystem.SystemCampaignsPath)
|
||||
for _, file := range files {
|
||||
name := file.Name()
|
||||
if filepath.Ext(name) == ".json" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
|
||||
// Append user campaigns.
|
||||
userFiles, err := userdir.ListCampaigns()
|
||||
names = append(names, userFiles...)
|
||||
|
||||
// Deduplicate names.
|
||||
var uniq = map[string]interface{}{}
|
||||
var result []string
|
||||
for _, name := range names {
|
||||
if _, ok := uniq[name]; !ok {
|
||||
uniq[name] = nil
|
||||
result = append(result, name)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(result)
|
||||
return result, err
|
||||
}
|
|
@ -26,8 +26,9 @@ const (
|
|||
|
||||
// Paths to system-level assets bundled with the application.
|
||||
var (
|
||||
SystemDoodadsPath = filepath.Join("assets", "doodads")
|
||||
SystemLevelsPath = filepath.Join("assets", "levels")
|
||||
SystemDoodadsPath = filepath.Join("assets", "doodads")
|
||||
SystemLevelsPath = filepath.Join("assets", "levels")
|
||||
SystemCampaignsPath = filepath.Join("assets", "campaigns")
|
||||
)
|
||||
|
||||
// MakeHeader creates the binary file header.
|
||||
|
|
|
@ -50,12 +50,7 @@ func (s *MainScene) Setup(d *Doodle) error {
|
|||
// Main title label
|
||||
s.labelTitle = ui.NewLabel(ui.Label{
|
||||
Text: branding.AppName,
|
||||
Font: render.Text{
|
||||
Size: 46,
|
||||
Color: render.Pink,
|
||||
Stroke: render.SkyBlue,
|
||||
Shadow: render.Black,
|
||||
},
|
||||
Font: balance.TitleScreenFont,
|
||||
})
|
||||
s.labelTitle.Compute(d.Engine)
|
||||
|
||||
|
@ -101,6 +96,10 @@ func (s *MainScene) Setup(d *Doodle) error {
|
|||
Name string
|
||||
Func func()
|
||||
}{
|
||||
// {
|
||||
// Name: "Story Mode",
|
||||
// Func: d.GotoStoryMenu,
|
||||
// },
|
||||
{
|
||||
Name: "Play a Level",
|
||||
Func: d.GotoPlayMenu,
|
||||
|
|
|
@ -189,9 +189,6 @@ func (s *MenuScene) Loop(d *Doodle, ev *event.State) error {
|
|||
|
||||
// Draw the pixels on this frame.
|
||||
func (s *MenuScene) Draw(d *Doodle) error {
|
||||
// Clear the canvas and fill it with white.
|
||||
d.Engine.Clear(render.White)
|
||||
|
||||
// Draw the background canvas.
|
||||
s.canvas.Present(d.Engine, render.Origin)
|
||||
|
||||
|
|
55
pkg/scripting/js_api.go
Normal file
55
pkg/scripting/js_api.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package scripting
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/apps/doodle/pkg/physics"
|
||||
"git.kirsle.net/apps/doodle/pkg/shmem"
|
||||
"git.kirsle.net/go/render"
|
||||
)
|
||||
|
||||
// JSProxy offers a function API interface to expose to Doodad javascripts.
|
||||
// These methods safely give the JS access to important attributes and functions
|
||||
// without exposing unintended API surface area in the process.
|
||||
type JSProxy map[string]interface{}
|
||||
|
||||
// NewJSProxy initializes the API structure for JavaScript binding.
|
||||
func NewJSProxy(vm *VM) JSProxy {
|
||||
return JSProxy{
|
||||
// Console logging.
|
||||
"console": map[string]interface{}{
|
||||
"log": log.Info,
|
||||
"debug": log.Debug,
|
||||
"warn": log.Warn,
|
||||
"error": log.Error,
|
||||
},
|
||||
|
||||
// Type constructors.
|
||||
"RGBA": render.RGBA,
|
||||
"Point": render.NewPoint,
|
||||
"Vector": physics.NewVector,
|
||||
|
||||
// Useful types and functions.
|
||||
"Flash": shmem.Flash,
|
||||
"GetTick": func() uint64 {
|
||||
return shmem.Tick
|
||||
},
|
||||
"time": map[string]interface{}{
|
||||
"Now": time.Now,
|
||||
"Add": func(t time.Time, ms int64) time.Time {
|
||||
return t.Add(time.Duration(ms) * time.Millisecond)
|
||||
},
|
||||
},
|
||||
|
||||
// Bindings into the VM.
|
||||
"Events": vm.Events,
|
||||
"setTimeout": vm.SetTimeout,
|
||||
"setInterval": vm.SetInterval,
|
||||
"clearTimeout": vm.ClearTimer,
|
||||
"clearInterval": vm.ClearTimer,
|
||||
|
||||
// Self for an actor to inspect themselves.
|
||||
"Self": vm.Self,
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
// Package scripting manages the JavaScript VMs for Doodad
|
||||
// scripts.
|
||||
// Package scripting manages the JavaScript VMs for Doodad scripts.
|
||||
package scripting
|
||||
|
||||
import (
|
||||
|
|
|
@ -2,14 +2,9 @@ package scripting
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/apps/doodle/pkg/physics"
|
||||
"git.kirsle.net/apps/doodle/pkg/shmem"
|
||||
"git.kirsle.net/go/render"
|
||||
"github.com/robertkrimen/otto"
|
||||
)
|
||||
|
||||
|
@ -69,32 +64,7 @@ func (vm *VM) Set(name string, v interface{}) error {
|
|||
// RegisterLevelHooks registers accessors to the level hooks
|
||||
// and Doodad API for Play Mode.
|
||||
func (vm *VM) RegisterLevelHooks() error {
|
||||
bindings := map[string]interface{}{
|
||||
"log": log.Logger,
|
||||
"Flash": shmem.Flash,
|
||||
"RGBA": render.RGBA,
|
||||
"Point": render.NewPoint,
|
||||
"Vector": physics.NewVector,
|
||||
"Self": vm.Self, // i.e., the uix.Actor object
|
||||
"Events": vm.Events,
|
||||
"GetTick": func() uint64 {
|
||||
return shmem.Tick
|
||||
},
|
||||
|
||||
"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,
|
||||
"clearTimeout": vm.ClearTimer,
|
||||
"clearInterval": vm.ClearTimer,
|
||||
}
|
||||
bindings := NewJSProxy(vm)
|
||||
for name, v := range bindings {
|
||||
err := vm.vm.Set(name, v)
|
||||
if err != nil {
|
||||
|
@ -103,15 +73,6 @@ func (vm *VM) RegisterLevelHooks() error {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Alias the console.log functions to the logger.
|
||||
vm.vm.Run(`
|
||||
console = {};
|
||||
console.log = log.Info;
|
||||
console.debug = log.Debug;
|
||||
console.warn = log.Warn;
|
||||
console.error = log.Error;
|
||||
`)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
144
pkg/story_scene.go
Normal file
144
pkg/story_scene.go
Normal file
|
@ -0,0 +1,144 @@
|
|||
package doodle
|
||||
|
||||
import (
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/campaign"
|
||||
"git.kirsle.net/apps/doodle/pkg/level"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/apps/doodle/pkg/uix"
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/render/event"
|
||||
"git.kirsle.net/go/ui"
|
||||
)
|
||||
|
||||
// StoryScene manages the "Story Mode" menu selection screen.
|
||||
type StoryScene struct {
|
||||
// Private variables.
|
||||
d *Doodle
|
||||
running bool
|
||||
|
||||
// Background wallpaper canvas.
|
||||
canvas *uix.Canvas
|
||||
|
||||
// UI widgets.
|
||||
supervisor *ui.Supervisor
|
||||
campaignFrame *ui.Frame // Select a Campaign screen
|
||||
levelSelectFrame *ui.Frame // Select a level in the campaign screen
|
||||
|
||||
// Pointer to the currently active frame.
|
||||
activeFrame *ui.Frame
|
||||
}
|
||||
|
||||
// Name of the scene.
|
||||
func (s *StoryScene) Name() string {
|
||||
return "Story"
|
||||
}
|
||||
|
||||
// GotoStoryMenu initializes the story menu scene.
|
||||
func (d *Doodle) GotoStoryMenu() {
|
||||
log.Info("Loading Story Scene")
|
||||
scene := &StoryScene{}
|
||||
d.Goto(scene)
|
||||
}
|
||||
|
||||
// Setup the play scene.
|
||||
func (s *StoryScene) Setup(d *Doodle) error {
|
||||
s.d = d
|
||||
|
||||
// Set up the background wallpaper canvas.
|
||||
s.canvas = uix.NewCanvas(100, false)
|
||||
s.canvas.Resize(render.NewRect(d.width, d.height))
|
||||
s.canvas.LoadLevel(d.Engine, &level.Level{
|
||||
Chunker: level.NewChunker(100),
|
||||
Palette: level.NewPalette(),
|
||||
PageType: level.Bounded,
|
||||
Wallpaper: "notebook.png",
|
||||
})
|
||||
|
||||
s.supervisor = ui.NewSupervisor()
|
||||
|
||||
// Set up the sub-screens of this scene.
|
||||
s.campaignFrame = s.setupCampaignFrame()
|
||||
s.levelSelectFrame = s.setupLevelSelectFrame()
|
||||
|
||||
s.activeFrame = s.campaignFrame
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupCampaignFrame sets up the Campaign List screen.
|
||||
func (s *StoryScene) setupCampaignFrame() *ui.Frame {
|
||||
var frame = ui.NewFrame("List Frame")
|
||||
frame.SetBackground(render.RGBA(0, 0, 255, 20))
|
||||
|
||||
// Title label
|
||||
labelTitle := ui.NewLabel(ui.Label{
|
||||
Text: "Select a Story",
|
||||
Font: balance.TitleScreenFont,
|
||||
})
|
||||
labelTitle.Compute(s.d.Engine)
|
||||
frame.Place(labelTitle, ui.Place{
|
||||
Top: 120,
|
||||
Center: true,
|
||||
})
|
||||
|
||||
// Buttons for campaign selection.
|
||||
{
|
||||
campaignFiles, err := campaign.List()
|
||||
if err != nil {
|
||||
log.Error("campaign.List: %s", err)
|
||||
}
|
||||
|
||||
_ = campaignFiles
|
||||
// for _, file := range campaignFiles {
|
||||
//
|
||||
// }
|
||||
}
|
||||
|
||||
frame.Resize(render.NewRect(s.d.width, s.d.height))
|
||||
frame.Compute(s.d.Engine)
|
||||
return frame
|
||||
}
|
||||
|
||||
// setupLevelSelectFrame sets up the Level Select screen.
|
||||
func (s *StoryScene) setupLevelSelectFrame() *ui.Frame {
|
||||
var frame = ui.NewFrame("List Frame")
|
||||
|
||||
return frame
|
||||
}
|
||||
|
||||
// Loop the story scene.
|
||||
func (s *StoryScene) Loop(d *Doodle, ev *event.State) error {
|
||||
s.supervisor.Loop(ev)
|
||||
|
||||
// Has the window been resized?
|
||||
if ev.WindowResized {
|
||||
w, h := d.Engine.WindowSize()
|
||||
if w != d.width || h != d.height {
|
||||
d.width = w
|
||||
d.height = h
|
||||
s.canvas.Resize(render.NewRect(d.width, d.height))
|
||||
s.activeFrame.Resize(render.NewRect(d.width, d.height))
|
||||
s.activeFrame.Compute(d.Engine)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Draw the pixels on this frame.
|
||||
func (s *StoryScene) Draw(d *Doodle) error {
|
||||
// Draw the background canvas.
|
||||
s.canvas.Present(d.Engine, render.Origin)
|
||||
|
||||
// Draw the active screen.
|
||||
s.activeFrame.Present(d.Engine, render.Origin)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroy the scene.
|
||||
func (s *StoryScene) Destroy() error {
|
||||
return nil
|
||||
}
|
|
@ -55,7 +55,28 @@ func (w *Canvas) InstallScripts() error {
|
|||
|
||||
for _, actor := range w.actors {
|
||||
vm := w.scripting.To(actor.ID())
|
||||
vm.Self = actor
|
||||
|
||||
// Security: expose a selective API to the actor to the JS engine.
|
||||
vm.Self = map[string]interface{}{
|
||||
"Filename": actor.Doodad().Filename,
|
||||
"Title": actor.Doodad().Title,
|
||||
|
||||
// functions
|
||||
"ID": actor.ID,
|
||||
"GetTag": actor.Doodad().Tag,
|
||||
"Position": actor.Position,
|
||||
"SetHitbox": actor.SetHitbox,
|
||||
"SetVelocity": actor.SetVelocity,
|
||||
"SetMobile": actor.SetMobile,
|
||||
"SetGravity": actor.SetGravity,
|
||||
"AddAnimation": actor.AddAnimation,
|
||||
"IsAnimating": actor.IsAnimating,
|
||||
"PlayAnimation": actor.PlayAnimation,
|
||||
"StopAnimation": actor.StopAnimation,
|
||||
"ShowLayer": actor.ShowLayer,
|
||||
"ShowLayerNamed": actor.ShowLayerNamed,
|
||||
"Destroy": actor.Destroy,
|
||||
}
|
||||
vm.Set("Self", vm.Self)
|
||||
|
||||
if _, err := vm.Run(actor.Doodad().Script); err != nil {
|
||||
|
|
|
@ -15,9 +15,10 @@ import (
|
|||
var (
|
||||
ConfigDirectoryName = "doodle"
|
||||
|
||||
ProfileDirectory string
|
||||
LevelDirectory string
|
||||
DoodadDirectory string
|
||||
ProfileDirectory string
|
||||
LevelDirectory string
|
||||
DoodadDirectory string
|
||||
CampaignDirectory string
|
||||
|
||||
CacheDirectory string
|
||||
FontDirectory string
|
||||
|
@ -34,6 +35,7 @@ func init() {
|
|||
ProfileDirectory = configdir.LocalConfig(ConfigDirectoryName)
|
||||
LevelDirectory = configdir.LocalConfig(ConfigDirectoryName, "levels")
|
||||
DoodadDirectory = configdir.LocalConfig(ConfigDirectoryName, "doodads")
|
||||
CampaignDirectory = configdir.LocalConfig(ConfigDirectoryName, "campaigns")
|
||||
|
||||
// Cache directory to extract font files to.
|
||||
CacheDirectory = configdir.LocalCache(ConfigDirectoryName)
|
||||
|
@ -44,6 +46,7 @@ func init() {
|
|||
if runtime.GOOS != "js" {
|
||||
configdir.MakePath(LevelDirectory)
|
||||
configdir.MakePath(DoodadDirectory)
|
||||
configdir.MakePath(CampaignDirectory)
|
||||
configdir.MakePath(FontDirectory)
|
||||
}
|
||||
}
|
||||
|
@ -121,6 +124,30 @@ func ListLevels() ([]string, error) {
|
|||
return names, nil
|
||||
}
|
||||
|
||||
// ListCampaigns returns a listing of all available campaigns.
|
||||
func ListCampaigns() ([]string, error) {
|
||||
var names []string
|
||||
|
||||
// WASM: list from localStorage.
|
||||
if runtime.GOOS == "js" {
|
||||
return wasm.StorageKeys(CampaignDirectory + "/"), nil
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(CampaignDirectory)
|
||||
if err != nil {
|
||||
return names, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
name := file.Name()
|
||||
if filepath.Ext(name) == ".json" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// resolvePath is the inner logic for LevelPath and DoodadPath.
|
||||
func resolvePath(directory, filename, extension string) string {
|
||||
if strings.Contains(filename, string(filepath.Separator)) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user