From 7142c76b8693c950a3cae0d20829c3100a98ac7a Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Mon, 9 Mar 2020 22:21:59 -0700 Subject: [PATCH] Initial Guidebook code --- .gitignore | 2 + dev-assets/doodads/doors/electric.gif | Bin 0 -> 10568 bytes dev-assets/doodads/trapdoors/down.gif | Bin 0 -> 9157 bytes dev-assets/guidebook/build.py | 73 +++++++++++ dev-assets/guidebook/build.sh | 16 +++ dev-assets/guidebook/compiled/index.html | 28 +++++ dev-assets/guidebook/pages/DoodadScripts.md | 130 ++++++++++++++++++++ dev-assets/guidebook/pages/Home.md | 9 ++ dev-assets/guidebook/pages/index.html | 28 +++++ dev-assets/guidebook/pages/markdown.html | 15 +++ dev-assets/guidebook/pages/res/main.css | 66 ++++++++++ dev-assets/guidebook/pages/res/markdown.css | 25 ++++ dev-assets/guidebook/pages/res/syntax.css | 69 +++++++++++ dev-assets/guidebook/requirements.txt | 2 + 14 files changed, 463 insertions(+) create mode 100644 dev-assets/doodads/doors/electric.gif create mode 100644 dev-assets/doodads/trapdoors/down.gif create mode 100644 dev-assets/guidebook/build.py create mode 100755 dev-assets/guidebook/build.sh create mode 100644 dev-assets/guidebook/compiled/index.html create mode 100644 dev-assets/guidebook/pages/DoodadScripts.md create mode 100644 dev-assets/guidebook/pages/Home.md create mode 100644 dev-assets/guidebook/pages/index.html create mode 100644 dev-assets/guidebook/pages/markdown.html create mode 100644 dev-assets/guidebook/pages/res/main.css create mode 100644 dev-assets/guidebook/pages/res/markdown.css create mode 100644 dev-assets/guidebook/pages/res/syntax.css create mode 100644 dev-assets/guidebook/requirements.txt diff --git a/.gitignore b/.gitignore index 50160e5..e8b2308 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ fonts/ maps/ bin/ dist/ +dev-assets/guidebook/venv +dev-assets/guidebook/compiled/pages wasm/assets/ *.wasm *.doodad diff --git a/dev-assets/doodads/doors/electric.gif b/dev-assets/doodads/doors/electric.gif new file mode 100644 index 0000000000000000000000000000000000000000..c280b2d05257900f62a766f5cfef2c4cc6a96895 GIT binary patch literal 10568 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 {}".format(filename, htmlname)) + + with codecs.open(md, 'r', 'utf-8') as fh: + data = fh.read() + rendered = markdown.markdown(data, + extensions=["codehilite", "fenced_code"], + ) + html = html_wrapper.replace("$CONTENT", rendered) + html = render_template(html, + title=title_from_markdown(data), + ) + + with open(os.path.join("compiled", "pages", htmlname), "w") as outfh: + outfh.write(html) + +jinja_env = jinja2.Environment() + +def render_template(input, *args, **kwargs): + templ = jinja_env.from_string(input) + return templ.render( + app_name="Project: Doodle", + app_version=get_app_version(), + *args, **kwargs + ) + +def title_from_markdown(text): + """Retrieve the title from the first Markdown header.""" + for line in text.split("\n"): + if line.startswith("# "): + return line[2:] + +def get_app_version(): + """Get the app version from pkg/branding/branding.go in Doodle""" + ver = re.compile(r'Version\s*=\s*"(.+?)"') + with codecs.open("../../pkg/branding/branding.go", "r", "utf-8") as fh: + text = fh.read() + for line in text.split("\n"): + m = ver.search(line) + if m: + return m[1] + +if __name__ == "__main__": + main() diff --git a/dev-assets/guidebook/build.sh b/dev-assets/guidebook/build.sh new file mode 100755 index 0000000..11d620b --- /dev/null +++ b/dev-assets/guidebook/build.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +if [[ ! -d "./venv" ]]; then + echo Creating Python virtualenv... + python3 -m venv ./venv + source ./venv/bin/activate + pip install -r requirements.txt +else + source ./venv/bin/activate +fi + +python build.py + +# Copy static files in. +mkdir -p compiled/pages/res +cp -r pages/res/*.* compiled/pages/res/ diff --git a/dev-assets/guidebook/compiled/index.html b/dev-assets/guidebook/compiled/index.html new file mode 100644 index 0000000..cabb96c --- /dev/null +++ b/dev-assets/guidebook/compiled/index.html @@ -0,0 +1,28 @@ + + + + Project: Doodle Guidebook + + + + + + + + +
+
+ +
+
+ + + \ No newline at end of file diff --git a/dev-assets/guidebook/pages/DoodadScripts.md b/dev-assets/guidebook/pages/DoodadScripts.md new file mode 100644 index 0000000..c4dece8 --- /dev/null +++ b/dev-assets/guidebook/pages/DoodadScripts.md @@ -0,0 +1,130 @@ +# Doodad Scripts + +Doodads are programmed using JavaScript which gives them their behavior +and ability to interact with the player and other doodads. + +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. +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); + + // If our doodad has 'solid' parts that should prohibit movement, + // define the hitbox here. Coordinates are relative so 0,0 is the + // top-left pixel of the doodad's sprite. + Self.SetHitbox(0, 0, 64, 12); + + // Handle a collision when another doodad (or player) has entered + // the space of our doodad. + Events.OnCollide(function(e) { + // The `e` object holds information about the event. + console.log("Actor %s has entered our hitbox!", e.Actor.ID()); + + // InHitbox is `true` if we defined a hitbox for ourselves, and + // the colliding actor is inside of the hitbox we defined. + if (e.InHitbox) { + // To prohibit movement, return false from the OnCollide handler. + // If you don't return false, the actor is allowed to keep on + // moving through. + return false; + } + }); + + // OnLeave is called when an actor, who was previously colliding with + // us, is no longer doing so. + Events.OnLeave(function(e) { + console.log("Actor %s has stopped colliding!", e.Actor.ID()); + }) +} +``` + +# JavaScript API + +## Global Variables + +The following global variables are available to all Doodad scripts. + +### Self + +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. + * Self.Doodad.Author: the name of the author who wrote the doodad. + * Self.Doodad.Script: the doodad's JavaScript source code. Note that + modifying this won't have any effect in-game, as the script had already + been loaded into the interpreter. + * Self.Doodad.GameVersion: the version of {{ app_name }} that was used + when the doodad was created. + +### Events + +### Message + +## Global Functions + +The following useful functions are also available globally: + +### Timers and Intervals + +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. + +```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!"); +``` + +### 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. + +### 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. diff --git a/dev-assets/guidebook/pages/Home.md b/dev-assets/guidebook/pages/Home.md new file mode 100644 index 0000000..c7be728 --- /dev/null +++ b/dev-assets/guidebook/pages/Home.md @@ -0,0 +1,9 @@ +# Guidebook to {{ app_name }} + +This is the users manual to {{ app_name }}, a drawing-based maze game. + +## Creating Custom Content + +* [Doodad Scripts](DoodadScripts.html) + +v{{ app_version }} diff --git a/dev-assets/guidebook/pages/index.html b/dev-assets/guidebook/pages/index.html new file mode 100644 index 0000000..bb332e4 --- /dev/null +++ b/dev-assets/guidebook/pages/index.html @@ -0,0 +1,28 @@ + + + + {{ app_name }} Guidebook + + + + + + + + +
+
+ +
+
+ + + diff --git a/dev-assets/guidebook/pages/markdown.html b/dev-assets/guidebook/pages/markdown.html new file mode 100644 index 0000000..0ab3a1c --- /dev/null +++ b/dev-assets/guidebook/pages/markdown.html @@ -0,0 +1,15 @@ + + + + {{ title }} + + + + + + + +$CONTENT + + + diff --git a/dev-assets/guidebook/pages/res/main.css b/dev-assets/guidebook/pages/res/main.css new file mode 100644 index 0000000..4aa5246 --- /dev/null +++ b/dev-assets/guidebook/pages/res/main.css @@ -0,0 +1,66 @@ +body,html { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + overflow: hidden; +} +body { + background-color: #EEE; + font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; + font-size: 16px; + color: #000; + line-height: 1.4em; +} + +div#sidebar { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 220px; + background-color: #000066; + color: #EEE; +} + +nav { + padding: 1.5rem; +} + +nav ul { + margin: 0; + padding: 0; + list-style: none; +} +nav ul li { + display: block; +} +nav a:link, nav a:visited { + color: #EEE; + text-decoration: none; +} +nav a:hover, nav a:active { + color: #F9F; + text-decoration: underline; +} + +div#content { + position: absolute; + top: 0; + left: 220px; + right: 0; + bottom: 0; +} + +div#content div { + position: relative; + width: 100%; + height: 100%; +} + +div#content iframe { + width: 100%; + height: 100%; + border: 0; + overflow: auto; +} diff --git a/dev-assets/guidebook/pages/res/markdown.css b/dev-assets/guidebook/pages/res/markdown.css new file mode 100644 index 0000000..5a193aa --- /dev/null +++ b/dev-assets/guidebook/pages/res/markdown.css @@ -0,0 +1,25 @@ +body { + background-color: #EEE; + font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; + font-size: 16px; + color: #000; + line-height: 1.4em; + margin: 2rem; +} + +a:link, a:visited { + color: #006699; + text-decoration: underline; +} +a:hover, a:active { + color: #996600; +} + +.codehilite { + border: 1px solid #222; +} +pre { + font-family: "Lucida Console", "DejaVu LGC Sans Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Ubuntu Mono", monospace; + margin: 0; + padding: 8px; +} diff --git a/dev-assets/guidebook/pages/res/syntax.css b/dev-assets/guidebook/pages/res/syntax.css new file mode 100644 index 0000000..bf82aed --- /dev/null +++ b/dev-assets/guidebook/pages/res/syntax.css @@ -0,0 +1,69 @@ +.codehilite .hll { background-color: #ffffcc } +.codehilite { background: #f8f8f8; } +.codehilite .c { color: #408080; font-style: italic } /* Comment */ +.codehilite .err { border: 1px solid #FF0000 } /* Error */ +.codehilite .k { color: #008000; font-weight: bold } /* Keyword */ +.codehilite .o { color: #666666 } /* Operator */ +.codehilite .ch { color: #408080; font-style: italic } /* Comment.Hashbang */ +.codehilite .cm { color: #408080; font-style: italic } /* Comment.Multiline */ +.codehilite .cp { color: #BC7A00 } /* Comment.Preproc */ +.codehilite .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */ +.codehilite .c1 { color: #408080; font-style: italic } /* Comment.Single */ +.codehilite .cs { color: #408080; font-style: italic } /* Comment.Special */ +.codehilite .gd { color: #A00000 } /* Generic.Deleted */ +.codehilite .ge { font-style: italic } /* Generic.Emph */ +.codehilite .gr { color: #FF0000 } /* Generic.Error */ +.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.codehilite .gi { color: #00A000 } /* Generic.Inserted */ +.codehilite .go { color: #888888 } /* Generic.Output */ +.codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.codehilite .gs { font-weight: bold } /* Generic.Strong */ +.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.codehilite .gt { color: #0044DD } /* Generic.Traceback */ +.codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ +.codehilite .kp { color: #008000 } /* Keyword.Pseudo */ +.codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.codehilite .kt { color: #B00040 } /* Keyword.Type */ +.codehilite .m { color: #666666 } /* Literal.Number */ +.codehilite .s { color: #BA2121 } /* Literal.String */ +.codehilite .na { color: #7D9029 } /* Name.Attribute */ +.codehilite .nb { color: #008000 } /* Name.Builtin */ +.codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */ +.codehilite .no { color: #880000 } /* Name.Constant */ +.codehilite .nd { color: #AA22FF } /* Name.Decorator */ +.codehilite .ni { color: #999999; font-weight: bold } /* Name.Entity */ +.codehilite .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +.codehilite .nf { color: #0000FF } /* Name.Function */ +.codehilite .nl { color: #A0A000 } /* Name.Label */ +.codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +.codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */ +.codehilite .nv { color: #19177C } /* Name.Variable */ +.codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.codehilite .w { color: #bbbbbb } /* Text.Whitespace */ +.codehilite .mb { color: #666666 } /* Literal.Number.Bin */ +.codehilite .mf { color: #666666 } /* Literal.Number.Float */ +.codehilite .mh { color: #666666 } /* Literal.Number.Hex */ +.codehilite .mi { color: #666666 } /* Literal.Number.Integer */ +.codehilite .mo { color: #666666 } /* Literal.Number.Oct */ +.codehilite .sa { color: #BA2121 } /* Literal.String.Affix */ +.codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */ +.codehilite .sc { color: #BA2121 } /* Literal.String.Char */ +.codehilite .dl { color: #BA2121 } /* Literal.String.Delimiter */ +.codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.codehilite .s2 { color: #BA2121 } /* Literal.String.Double */ +.codehilite .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +.codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */ +.codehilite .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +.codehilite .sx { color: #008000 } /* Literal.String.Other */ +.codehilite .sr { color: #BB6688 } /* Literal.String.Regex */ +.codehilite .s1 { color: #BA2121 } /* Literal.String.Single */ +.codehilite .ss { color: #19177C } /* Literal.String.Symbol */ +.codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */ +.codehilite .fm { color: #0000FF } /* Name.Function.Magic */ +.codehilite .vc { color: #19177C } /* Name.Variable.Class */ +.codehilite .vg { color: #19177C } /* Name.Variable.Global */ +.codehilite .vi { color: #19177C } /* Name.Variable.Instance */ +.codehilite .vm { color: #19177C } /* Name.Variable.Magic */ +.codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */ diff --git a/dev-assets/guidebook/requirements.txt b/dev-assets/guidebook/requirements.txt new file mode 100644 index 0000000..f27036f --- /dev/null +++ b/dev-assets/guidebook/requirements.txt @@ -0,0 +1,2 @@ +markdown +jinja2