giant freakin documentation and reorganization pass, it also uses cmake because all the building was getting too complicated for shell scripts
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
<?xml version="1.0" standalone="yes"?>
|
||||
<xs:schema id="UpgradeRules" targetNamespace="seagull:rules/upgrades" xmlns="seagull:rules/upgrades" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" attributeFormDefault="unqualified" elementFormDefault="qualified">
|
||||
<xs:element name="UpgradeRules" msdata:IsDataSet="true" msdata:UseCurrentLocale="true">
|
||||
<xs:element name="UpgradeRules">
|
||||
<xs:complexType>
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="TreeData" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:choice maxOccurs="unbounded">
|
||||
<xs:element name="Name" maxOccurs="1" type="xs:string" />
|
||||
<xs:element name="PrimaryColor" maxOccurs="1" type="xs:string" />
|
||||
</xs:choice>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="Upgrade">
|
||||
<xs:complexType>
|
||||
<xs:choice maxOccurs="unbounded">
|
||||
@@ -23,6 +31,13 @@
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:element name="Requirements" maxOccurs="1" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="Require" maxOccurs="unbounded" type="xs:string" />
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:choice>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
5
app/basepak/static/js/desktop-structuredclone.js
Normal file
5
app/basepak/static/js/desktop-structuredclone.js
Normal file
@@ -0,0 +1,5 @@
|
||||
function structuredClone(val) {
|
||||
var output = JSON.parse(JSON.stringify(val));
|
||||
if (window.pywebview.api.debug_mode) { console.log(("structuredClone:" + val) + " => " + outval); }
|
||||
return output;
|
||||
}
|
164
app/basepak/static/js/konami.js
Normal file
164
app/basepak/static/js/konami.js
Normal file
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Konami-JS ~
|
||||
* :: Now with support for touch events and multiple instances for
|
||||
* :: those situations that call for multiple easter eggs!
|
||||
* Code: https://github.com/georgemandis/konami-js
|
||||
* Copyright (c) 2009 George Mandis (https://george.mand.is)
|
||||
* Version: 1.7.0 (09/03/2024)
|
||||
* Licensed under the MIT License (http://opensource.org/licenses/MIT)
|
||||
* Tested in: Safari 4+, Google Chrome 4+, Firefox 3+, IE7+, Mobile Safari 2.2.1+ and Android
|
||||
*/
|
||||
|
||||
var Konami = function (callback) {
|
||||
var konami = {
|
||||
addEvent: function (obj, type, fn, ref_obj) {
|
||||
if (obj.addEventListener) obj.addEventListener(type, fn, false);
|
||||
else if (obj.attachEvent) {
|
||||
// IE
|
||||
obj["e" + type + fn] = fn;
|
||||
obj[type + fn] = function () {
|
||||
obj["e" + type + fn](window.event, ref_obj);
|
||||
};
|
||||
obj.attachEvent("on" + type, obj[type + fn]);
|
||||
}
|
||||
},
|
||||
removeEvent: function (obj, eventName, eventCallback) {
|
||||
if (obj.removeEventListener) {
|
||||
obj.removeEventListener(eventName, eventCallback);
|
||||
} else if (obj.attachEvent) {
|
||||
obj.detachEvent(eventName);
|
||||
}
|
||||
},
|
||||
input: "",
|
||||
pattern: "38384040373937396665",
|
||||
keydownHandler: function (e, ref_obj) {
|
||||
if (ref_obj) {
|
||||
konami = ref_obj;
|
||||
} // IE
|
||||
konami.input += e ? e.keyCode : event.keyCode;
|
||||
if (konami.input.length > konami.pattern.length) {
|
||||
konami.input = konami.input.substr(
|
||||
konami.input.length - konami.pattern.length,
|
||||
);
|
||||
}
|
||||
if (konami.input === konami.pattern) {
|
||||
konami.code(konami._currentLink);
|
||||
konami.input = "";
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
load: function (link) {
|
||||
this._currentLink = link;
|
||||
this.addEvent(document, "keydown", this.keydownHandler, this);
|
||||
this.iphone.load(link);
|
||||
},
|
||||
unload: function () {
|
||||
this.removeEvent(document, "keydown", this.keydownHandler);
|
||||
this.iphone.unload();
|
||||
},
|
||||
code: function (link) {
|
||||
window.location = link;
|
||||
},
|
||||
iphone: {
|
||||
start_x: 0,
|
||||
start_y: 0,
|
||||
stop_x: 0,
|
||||
stop_y: 0,
|
||||
tap: false,
|
||||
capture: false,
|
||||
orig_keys: "",
|
||||
keys: [
|
||||
"UP",
|
||||
"UP",
|
||||
"DOWN",
|
||||
"DOWN",
|
||||
"LEFT",
|
||||
"RIGHT",
|
||||
"LEFT",
|
||||
"RIGHT",
|
||||
"TAP",
|
||||
"TAP",
|
||||
],
|
||||
input: [],
|
||||
code: function (link) {
|
||||
konami.code(link);
|
||||
},
|
||||
touchmoveHandler: function (e) {
|
||||
if (e.touches.length === 1 && konami.iphone.capture === true) {
|
||||
var touch = e.touches[0];
|
||||
konami.iphone.stop_x = touch.pageX;
|
||||
konami.iphone.stop_y = touch.pageY;
|
||||
konami.iphone.tap = false;
|
||||
konami.iphone.capture = false;
|
||||
}
|
||||
},
|
||||
touchendHandler: function () {
|
||||
konami.iphone.input.push(konami.iphone.check_direction());
|
||||
|
||||
if (konami.iphone.input.length > konami.iphone.keys.length)
|
||||
konami.iphone.input.shift();
|
||||
|
||||
if (konami.iphone.input.length === konami.iphone.keys.length) {
|
||||
var match = true;
|
||||
for (var i = 0; i < konami.iphone.keys.length; i++) {
|
||||
if (konami.iphone.input[i] !== konami.iphone.keys[i]) {
|
||||
match = false;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
konami.iphone.code(konami._currentLink);
|
||||
}
|
||||
}
|
||||
},
|
||||
touchstartHandler: function (e) {
|
||||
konami.iphone.start_x = e.changedTouches[0].pageX;
|
||||
konami.iphone.start_y = e.changedTouches[0].pageY;
|
||||
konami.iphone.tap = true;
|
||||
konami.iphone.capture = true;
|
||||
},
|
||||
load: function (link) {
|
||||
this.orig_keys = this.keys;
|
||||
konami.addEvent(document, "touchmove", this.touchmoveHandler);
|
||||
konami.addEvent(document, "touchend", this.touchendHandler, false);
|
||||
konami.addEvent(document, "touchstart", this.touchstartHandler);
|
||||
},
|
||||
unload: function () {
|
||||
konami.removeEvent(document, "touchmove", this.touchmoveHandler);
|
||||
konami.removeEvent(document, "touchend", this.touchendHandler);
|
||||
konami.removeEvent(document, "touchstart", this.touchstartHandler);
|
||||
},
|
||||
check_direction: function () {
|
||||
var x_magnitude = Math.abs(this.start_x - this.stop_x);
|
||||
var y_magnitude = Math.abs(this.start_y - this.stop_y);
|
||||
var x = this.start_x - this.stop_x < 0 ? "RIGHT" : "LEFT";
|
||||
var y = this.start_y - this.stop_y < 0 ? "DOWN" : "UP";
|
||||
|
||||
var result =
|
||||
this.tap === true ? "TAP" : x_magnitude > y_magnitude ? x : y;
|
||||
|
||||
return result;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
typeof callback === "string" && konami.load(callback);
|
||||
if (typeof callback === "function") {
|
||||
konami.code = callback;
|
||||
konami.load();
|
||||
}
|
||||
|
||||
return konami;
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
|
||||
module.exports = Konami;
|
||||
} else {
|
||||
if (typeof define === "function" && define.amd) {
|
||||
define([], function () {
|
||||
return Konami;
|
||||
});
|
||||
} else {
|
||||
window.Konami = Konami;
|
||||
}
|
||||
}
|
14
app/basepak/static/js/mermaid.esm.min.mjs
Normal file
14
app/basepak/static/js/mermaid.esm.min.mjs
Normal file
File diff suppressed because one or more lines are too long
@@ -1,5 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
## \file desktop.py
|
||||
# \brief Entrypoint file for desktop versions of the Seagull Game.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import pathlib
|
||||
@@ -9,8 +12,19 @@ import webview
|
||||
|
||||
import flask
|
||||
import fs.tree
|
||||
try:
|
||||
import pyi_splash
|
||||
except ImportError:
|
||||
class NullModule():
|
||||
def update_text(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
from pylocal import core, actions, desktop, dev, gamedata, items, tick
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
pyi_splash = NullModule()
|
||||
|
||||
from pylocal import core, actions, desktop, dev, gamedata, items, tick, upgrades
|
||||
|
||||
core.desktop_mode = True
|
||||
sig_exit = threading.Event()
|
||||
@@ -23,13 +37,14 @@ argo = argp.parse_args()
|
||||
def index():
|
||||
if not core.base_context_live:
|
||||
core.render_base_context()
|
||||
core.base_context["scripts"].insert(0, core.app.url_for("static", filename="js/desktop-structuredclone.js"))
|
||||
core.base_context["scripts"].insert(1, core.app.url_for("static", filename="js/seagull-desktop.js"))
|
||||
core.base_context["scripts"].insert(0, (core.app.url_for("static", filename="js/desktop-structuredclone.js"), False))
|
||||
core.base_context["scripts"].insert(1, (core.app.url_for("static", filename="js/seagull-desktop.js"), False))
|
||||
gamedata.vfs.copy_out("templates/main_page.j2", dest=core.path_appdir.as_posix())
|
||||
return flask.render_template("main_page.j2", **core.base_context)
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
pyi_splash.update_text("Determining storage directory")
|
||||
if sys.platform.startswith("win"):
|
||||
storage_dir = pathlib.Path(os.environ["APPDATA"]) / "seagull"
|
||||
elif sys.platform.startswith("darwin"): # macos
|
||||
@@ -39,13 +54,16 @@ if __name__ == "__main__":
|
||||
storage_dir = pathlib.Path(os.environ["HOME"]) / ".local/share/seagull"
|
||||
desktop.path_storagedir = storage_dir
|
||||
|
||||
pyi_splash.update_text("Loading VFS")
|
||||
gamedata.vfs.load_data_source("basepak")
|
||||
gamedata.vfs.load_data_source("seagull.pak", proto="zip")
|
||||
|
||||
if argo.debug:
|
||||
desktop.api.debug_mode = True
|
||||
pyi_splash.update_text("Starting browser shell")
|
||||
storage_dir.mkdir(exist_ok=True, parents=True)
|
||||
webview.create_window("Seagull Game", core.app, js_api=desktop.api)
|
||||
webview.create_window("Seagull Game", core.app, js_api=desktop.api, width=1280, height=720)
|
||||
pyi_splash.close()
|
||||
webview.start(private_mode=False, storage_path=storage_dir.as_posix(), debug=True if argo.debug else False)
|
||||
except KeyboardInterrupt:
|
||||
core.log.info("Goodnight, moon ...")
|
||||
|
@@ -5,6 +5,15 @@ import flask
|
||||
|
||||
from . import core
|
||||
|
||||
## \brief Rolls a random integer between `min` and `max`, and applies all described `modifiers`.
|
||||
# \param min The lowest possible number. Default is 0.
|
||||
# \param max The highest possible number. Default is 100.
|
||||
# \param modifiers
|
||||
# \return The result of the dice roll, after modifiers.
|
||||
#
|
||||
# Dice roll is actually a slight misnomer; the game primarily uses percentages for chance
|
||||
# calculations. However, the recognized term for these chance calculations, "dice rolls",
|
||||
# is D&D old, so is used here for mental ease.
|
||||
def dice_roll(min=0, max=100, modifiers=[]):
|
||||
result = random.randint(min, max)
|
||||
for _, mod in modifiers:
|
||||
@@ -12,12 +21,29 @@ def dice_roll(min=0, max=100, modifiers=[]):
|
||||
|
||||
return result
|
||||
|
||||
def calculate_speed(agl):
|
||||
return 3+(agl * 1.5)
|
||||
|
||||
## \brief Attempts to steal a given resource from a target.
|
||||
# \param resource The resource to steal.
|
||||
# \param target The class of target to steal from.
|
||||
# \api{POST} /act/steal/`<resource>`/`<target>`
|
||||
# \apidata Gamestate.
|
||||
@core.app.route("/act/steal/<resource>/<target>", methods=["POST"])
|
||||
def steal_resource(resource, target):
|
||||
core.log.debug(flask.request.get_data())
|
||||
gamestate = flask.request.get_json()["gamestate"]
|
||||
agl = gamestate["agility"]
|
||||
roll = dice_roll()
|
||||
speed = calculate_speed(agl)
|
||||
result = (roll + speed) + (agl/4)
|
||||
return flask.Response(json.dumps({
|
||||
"success": (dice_roll() >= 50)
|
||||
"success": (result >= 50)
|
||||
}), status=200, content_type="application/json")
|
||||
|
||||
## \brief Attempts to recruit a seagull.
|
||||
# \api{POST} /act/recruit
|
||||
# \apidata Gamestate.
|
||||
@core.app.route("/act/recruit", methods=["POST"])
|
||||
def recruit():
|
||||
return flask.Response(json.dumps({
|
||||
|
@@ -4,6 +4,7 @@ import pathlib
|
||||
import sys
|
||||
|
||||
import flask
|
||||
from flask_cors import CORS
|
||||
|
||||
log = logging.getLogger()
|
||||
pipe_stderr = logging.StreamHandler(sys.stderr)
|
||||
@@ -16,11 +17,16 @@ else:
|
||||
path_appdir = pathlib.Path.cwd()
|
||||
log.critical(path_appdir)
|
||||
|
||||
## \internal
|
||||
# \brief Signals whether we are a desktop application (as opposed to a Docker container).
|
||||
desktop_mode = False
|
||||
|
||||
from . import gamedata
|
||||
|
||||
## \internal
|
||||
# \brief The Flask instance. See <a href="https://flask.palletsprojects.com/en/stable/api/">Flask documentation</a>.
|
||||
app = flask.Flask("seagull-game", root_path=path_appdir, template_folder="templates", static_folder="static")
|
||||
CORS(app)
|
||||
orig_url_for = app.url_for
|
||||
|
||||
xml_namespaces = {
|
||||
@@ -34,6 +40,10 @@ xml_namespaces = {
|
||||
#REDIS_PASS="i am not a real password"
|
||||
#state_cache = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, username=REDIS_USER, password=REDIS_PASS)
|
||||
|
||||
# ham5 standing by...
|
||||
|
||||
## \internal
|
||||
# \brief Returns CDN URLs for files not present within the container itself, or copies VFS data out so Flask etc can use it.
|
||||
def url_for_override(endpoint, *posargs, _anchor=None, _method=None, _scheme=None, _external=None, self=app, **values):
|
||||
if endpoint == "static":
|
||||
if not gamedata.vfs.exists(f"static/{values["filename"]}"):
|
||||
@@ -48,17 +58,25 @@ def url_for_override(endpoint, *posargs, _anchor=None, _method=None, _scheme=Non
|
||||
|
||||
app.url_for = url_for_override
|
||||
|
||||
## \internal
|
||||
# \brief Base Flask rendering context. Generated with render_base_context().
|
||||
base_context = {}
|
||||
base_context_live = False
|
||||
|
||||
## \brief Renders a dialog template and sends it to the client.
|
||||
# \param dialog The dialog to render.
|
||||
# \api{GET} /dialog/`<dialog>`
|
||||
@app.route("/dialog/<dialog>")
|
||||
def render_dialog(dialog):
|
||||
if gamedata.vfs.exists(f"templates/{dialog}.j2"):
|
||||
gamedata.vfs.copy_out(f"templates/{dialog}.j2", dest=path_appdir.as_posix())
|
||||
if gamedata.vfs.exists(f"static/js/dlg-{dialog}.js"):
|
||||
gamedata.vfs.copy_out(f"static/js/dlg-{dialog}.js", dest=path_appdir.as_posix())
|
||||
return flask.render_template(f"{dialog}.j2")
|
||||
else:
|
||||
return "", 404
|
||||
|
||||
## \brief Prepares the base rendering context for Flask to serve our content.
|
||||
def render_base_context():
|
||||
global base_context
|
||||
global base_context_live
|
||||
@@ -68,19 +86,32 @@ def render_base_context():
|
||||
domain_components = flask.request.host.split(".")
|
||||
base_domain = ".".join(domain_components[-2:])
|
||||
|
||||
gamedata.vfs.copy_out("static/js/mermaid.esm.min.mjs", dest=path_appdir.as_posix())
|
||||
|
||||
# all this wind up for...
|
||||
if base_domain == "otl-hga.net": # production, use assets from S3
|
||||
base_context["styles"] = ["https://cdn.otl-hga.net/seagull/css/seagull.css"]
|
||||
base_context["scripts"] = ["https://cdn.otl-hga.net/seagull/js/seagull.js"]
|
||||
base_context["scripts"] = ["https://cdn.otl-hga.net/seagull/js/seagull.js", "https://cdn.otl-hga.net/seagull/js/konami.js"]
|
||||
base_context["seagull_pic"] = "https://cdn.otl-hga.net/seagull/image/seagull.jpg"
|
||||
else: # dev, serve files from here
|
||||
#print(base_domain)
|
||||
base_context["styles"] = [app.url_for("static", filename="css/seagull.css")]
|
||||
base_context["scripts"] = [app.url_for("static", filename="js/seagull.js")]
|
||||
base_context["scripts"] = [(app.url_for("static", filename="js/konami.js"), True), (app.url_for("static", filename="js/seagull.js"), True)]
|
||||
base_context["seagull_pic"] = app.url_for("static", filename="image/seagull.jpg")
|
||||
|
||||
base_context_live = True
|
||||
|
||||
## \brief Returns OK. Useful for health checks.
|
||||
# \api{GET} /core/ping
|
||||
@app.route("/core/ping")
|
||||
def healthcheck_ping():
|
||||
return flask.Response("OK", content_type="text/plain")
|
||||
|
||||
## \brief Informs the game we're about to request a file from JavaScript.
|
||||
# \internal
|
||||
# \api{POST} /core/ready_file
|
||||
# \apidata Plaintext path to the intended file.
|
||||
@app.route("/core/ready_file", methods=["POST"])
|
||||
def ready_file():
|
||||
gamedata.vfs.copy_out(flask.request.data)
|
||||
return flask.Response("OK", content_type="text/plain")
|
@@ -2,14 +2,29 @@ import pathlib
|
||||
|
||||
from . import core
|
||||
|
||||
## \brief A <a href="https://docs.python.org/3/library/pathlib.html#pathlib.Path">standard Path object</a> pointing to where the user's local storage is.
|
||||
#
|
||||
# Exactly where this is depends on your operating system:
|
||||
# - On Windows, this is defined as `%%APPDATA%\seagull`, which typically evaluates to something like `"C:\Users\YourNameHere\AppData\Roaming\seagull"`
|
||||
# - On MacOS, this is defined as `~/Library/Application Support/seagull`, where ~ is your home directory.
|
||||
# - On Linux, this is defined as `~/.local/share/seagull`.
|
||||
# \note On Linux, the XDG specification for defining alternative data directories is not currently respected. This will likely change in a future version.
|
||||
path_storagedir = pathlib.Path()
|
||||
|
||||
## \internal
|
||||
# \brief Defines JavaScript API functions for pywebview.
|
||||
class JS_API:
|
||||
# \brief Whether or not we're in debug mode.
|
||||
debug_mode = False
|
||||
|
||||
def __init__(self, debug_mode=False):
|
||||
self.debug_mode = debug_mode
|
||||
|
||||
## \brief Loads data stored under a specific "key" in the local filesystem.
|
||||
# \param key The key to load.
|
||||
# \return File contents, or `None`.
|
||||
# In the current implementation, key is just a raw filename loaded relative to path_storagedir,
|
||||
# which is calculated as part of main thread startup.
|
||||
def load_data(self, key):
|
||||
if not (path_storagedir / key).exists():
|
||||
return None
|
||||
@@ -21,6 +36,11 @@ class JS_API:
|
||||
core.log.error(f"problem loading {key} (from {path_storagedir}): {exc}")
|
||||
return None
|
||||
|
||||
## \brief Saves data stored under a specific "key" in the local filesystem.
|
||||
# \param key The key to save to.
|
||||
# \param data The data to write.
|
||||
# In the current implementation, key is just a raw filename loaded relative to path_storagedir,
|
||||
# which is calculated as part of main thread startup.
|
||||
def save_data(self, key, data):
|
||||
with open(path_storagedir / key, "w") as fd_datafile:
|
||||
try:
|
||||
@@ -28,6 +48,8 @@ class JS_API:
|
||||
except Exception as exc:
|
||||
core.log.error(f"problem saving {key} (to {path_storagedir}): {exc}")
|
||||
|
||||
## \brief Deletes all data stored at key.
|
||||
# \param key The key to delete.
|
||||
def delete_data(self, key):
|
||||
if (path_storagedir / key).exists():
|
||||
(path_storagedir / key).unlink()
|
||||
|
@@ -1,7 +1,11 @@
|
||||
import flask
|
||||
|
||||
from . import core
|
||||
from . import core, gamedata
|
||||
|
||||
## \brief Renders the dev toolbox for the client
|
||||
# \api{GET} /dev/get-toolbox
|
||||
@core.app.route("/dev/get-toolbox")
|
||||
def dev_toolbox():
|
||||
gamedata.vfs.copy_out("static/sound/open_dev_toolkit.wav", dest=core.path_appdir.as_posix())
|
||||
gamedata.vfs.copy_out("templates/dev_toolbox.j2", dest=core.path_appdir.as_posix())
|
||||
return flask.render_template("dev_toolbox.j2", ipaddr=flask.request.remote_addr, desktop=core.desktop_mode)
|
@@ -12,6 +12,8 @@ import fs.osfs
|
||||
|
||||
from . import core
|
||||
|
||||
## \brief Handler for virtual filesystem.
|
||||
# \internal
|
||||
class GameVFSHandler(object):
|
||||
vfs = None
|
||||
log = logging.getLogger().getChild("vfs")
|
||||
@@ -43,6 +45,10 @@ class GameVFSHandler(object):
|
||||
except:
|
||||
raise
|
||||
|
||||
## \brief Load files from a given data source.
|
||||
# \param source The data source. This can be a pyfilesystem2 object, or any string or path-like
|
||||
# object that can reasonably be interpreted as a directory or zip file.
|
||||
# \param proto The <a href="https://pyfilesystem2.readthedocs.io/en/latest/builtin.html">PyFilesystem2 filesystem protocol</a> to use. Defaults to `"osfs"`, which loads directories.
|
||||
def load_data_source(self, source: pathlib.Path | fs.base.FS | typing.Text, proto="osfs"):
|
||||
print(f"data source: {source} ({proto})")
|
||||
assert self.vfs is not None
|
||||
@@ -63,6 +69,10 @@ class GameVFSHandler(object):
|
||||
self.log.info(f"loading vfs source: {fs_source} (pyfilesystem2 handler)")
|
||||
fs.copy.copy_fs(fs_source, self.vfs)
|
||||
|
||||
## \brief Copies a file out of VFS into the real filesystem.
|
||||
# \param filepath The source file path to copy out.
|
||||
# \param dest The path to write the file to. Defaults to temporary directory.
|
||||
# \return The full destination file path.
|
||||
def copy_out(self, filepath, dest=None):
|
||||
if not dest:
|
||||
self.osfs_temp.makedirs(pathlib.Path(filepath).parent.as_posix(), recreate=True)
|
||||
@@ -76,4 +86,6 @@ class GameVFSHandler(object):
|
||||
fs.copy.copy_file(self.vfs, filepath, dest, filepath)
|
||||
return (pth_dest / filepath).as_posix()
|
||||
|
||||
## \brief Primary VFS handler.
|
||||
# \internal
|
||||
vfs = GameVFSHandler()
|
@@ -10,6 +10,8 @@ valid_resources = [
|
||||
"food", "shinies", "psi" # early game
|
||||
]
|
||||
|
||||
## \internal
|
||||
# \brief The environment variable map to run Rant with.
|
||||
rant_env = os.environ.copy()
|
||||
rant_env["RANT_MODULES_PATH"] = (core.path_appdir / "basepak/rant").as_posix()
|
||||
|
||||
@@ -18,6 +20,12 @@ doc_item_schema = xmltree.parse(pth_item_schema.as_posix())
|
||||
item_schema = xmltree.XMLSchema(doc_item_schema)
|
||||
item_schema_parser = xmltree.XMLParser(schema=item_schema)
|
||||
|
||||
## \brief Generates an absolutely reasonable description of a given item that a target might have.
|
||||
# \param resource The resource to generate a description for.
|
||||
# \param target The poor soul carrying the item.
|
||||
# \return An absolutely reasonable description of an item.
|
||||
#
|
||||
# \include{doc,local} res/doc/python/items.generate_item_description.mdpart
|
||||
def generate_item_description(resource, target):
|
||||
if core.desktop_mode:
|
||||
rant_path = core.path_appdir / "opt/rant/bin/rant"
|
||||
@@ -29,6 +37,13 @@ def generate_item_description(resource, target):
|
||||
core.log.warning("rant is throwing up:\n" + proc_rant.stderr.decode())
|
||||
return proc_rant.stdout.decode().strip()
|
||||
|
||||
## \brief Generates a list of items worth `min`-`max` `resource` that `target` would reasonably have.
|
||||
# \param resource The resource to generate a description for.
|
||||
# \param target The poor soul carrying the item.
|
||||
# \param min The lowest possible value, per item.
|
||||
# \param max The highest possible value, per item.
|
||||
# \param storybeat Inform the rule parsing engine where we are in the story.
|
||||
# \return A list of TickItem instances.
|
||||
def generate_item_list(resource, target, min, max, storybeat=0):
|
||||
count = random.randint(min, max)
|
||||
result = []
|
||||
@@ -43,12 +58,12 @@ def generate_item_list(resource, target, min, max, storybeat=0):
|
||||
maxdata = res_rule.xpath("./items:Max", namespaces=core.xml_namespaces)[0]
|
||||
resource_rules.append((res_rule, int(mindata.text), int(maxdata.text)))
|
||||
for i in range(0, count):
|
||||
core.log.warning("TODO: we don't know which rule this parses yet")
|
||||
core.log.warning(f"{resource} vs humans: {resource_rules[0]}")
|
||||
core.log.warning(f"{resource} vs humans: {resource_rules[0][1]} <-> {resource_rules[0][2]} (sb: {resource_rules[0][0].get("StoryBeat", "0")})")
|
||||
result.append(TickItem(resource, round(random.uniform(resource_rules[0][1], resource_rules[0][2]), 2), target))
|
||||
|
||||
return result
|
||||
|
||||
## \brief A tick item.
|
||||
class TickItem(object):
|
||||
def __init__(self, resource, amount, target):
|
||||
if resource not in valid_resources:
|
||||
|
@@ -2,6 +2,8 @@ import json
|
||||
|
||||
from . import items
|
||||
|
||||
## \brief Extended JSON encoder object to handle seagull-specific items.
|
||||
# \internal
|
||||
class JSONizer(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, items.TickItem):
|
||||
|
@@ -5,7 +5,7 @@ import subprocess
|
||||
|
||||
import flask
|
||||
|
||||
from . import core, items, jsonizer
|
||||
from . import core, gamedata, items, jsonizer
|
||||
|
||||
rant_env = os.environ.copy()
|
||||
rant_env["RANT_MODULES_PATH"] = (core.path_appdir / "basepak/rant").as_posix()
|
||||
@@ -15,7 +15,8 @@ def generate_flavor_text():
|
||||
rant_path = core.path_appdir / "opt/rant/bin/rant"
|
||||
else:
|
||||
rant_path = "rant" # rely on OS PATH
|
||||
proc_rant = subprocess.run([rant_path, (core.path_appdir / "basepak/rant/flavor.rant").as_posix()], env=rant_env, capture_output=True)
|
||||
flavor_file = gamedata.vfs.copy_out("rant/flavor.rant")
|
||||
proc_rant = subprocess.run([rant_path, flavor_file], env=rant_env, capture_output=True)
|
||||
return proc_rant.stdout.decode()
|
||||
|
||||
class TickEvent(object):
|
||||
@@ -30,6 +31,9 @@ tick_event_list.append(TickEvent(1, 1, "FLAVOR")) # procedurally generated
|
||||
tick_event_list.append(TickEvent(10, 2, "ENCHUMAN")) # encounter: human
|
||||
tick_event_list.append(TickEvent(11, 2, "ENCGULL"))
|
||||
|
||||
## \brief Returns the results of a game tick.
|
||||
# \api{GET} /tick
|
||||
# \return Game tick events and results.
|
||||
@core.app.route("/tick")
|
||||
def tick():
|
||||
#return random.choices([json.dumps({"code": 200, "event_type": 0}), json.dumps({"code": 200, "event_type": 1, "log": generate_flavor_text()})], weights=[16, 1])[0]
|
||||
@@ -67,10 +71,28 @@ def tick():
|
||||
|
||||
return flask.Response(json.dumps(result, cls=jsonizer.JSONizer), status=200, content_type="application/json")
|
||||
|
||||
## \brief Returns the results of a colony tick.
|
||||
# \api{GET} /tick/colony
|
||||
# \return The result of a colony tick.
|
||||
@core.app.route("/tick/colony", methods=["POST"])
|
||||
def tick_colony() -> flask.Response:
|
||||
req = flask.request.json
|
||||
if not req:
|
||||
return flask.make_response("Bad request", 400)
|
||||
|
||||
colony = req["colony"]
|
||||
modifiers = req["modifiers"]
|
||||
inc_food = req["avg_food"]
|
||||
inc_shinies = req["avg_shinies"]
|
||||
|
||||
out_food = (inc_food * 0.35) * (colony / 2)
|
||||
out_shinies = (inc_shinies * 0.25) * (colony / 3)
|
||||
|
||||
result = {
|
||||
"success": True,
|
||||
"food": out_food,
|
||||
"shinies": out_shinies
|
||||
}
|
||||
|
||||
return flask.Response(json.dumps(result, cls=jsonizer.JSONizer), status=200, content_type="application/json")
|
||||
|
93
app/pylocal/upgrades.py
Normal file
93
app/pylocal/upgrades.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import io
|
||||
import logging
|
||||
import textwrap
|
||||
from collections import namedtuple
|
||||
|
||||
import flask
|
||||
import lxml.etree as xmltree
|
||||
import mermaid
|
||||
|
||||
from . import core, gamedata
|
||||
|
||||
pth_upgrade_schema = core.path_appdir / "basepak/rules/schemas/upgrades.xsd"
|
||||
doc_upgrade_schema = xmltree.parse(pth_upgrade_schema.as_posix())
|
||||
upgrade_schema = xmltree.XMLSchema(doc_upgrade_schema)
|
||||
upgrade_schema_parser = xmltree.XMLParser(schema=upgrade_schema)
|
||||
|
||||
UpgradeData = namedtuple("UpgradeData", ["id", "name", "desc", "requires"])
|
||||
|
||||
## \brief Renders an upgrade tree.
|
||||
# \param tree The upgrade tree to retrieve.
|
||||
# \api{GET} /upgrades/`<tree>`
|
||||
# \return An SVG.
|
||||
@core.app.route("/upgrades/<tree>")
|
||||
def get_upgrade_tree(tree):
|
||||
buf_mmd = io.StringIO()
|
||||
rulefile = xmltree.parse(gamedata.vfs.open(f"/rules/upgrades/{tree}.xml"), upgrade_schema_parser)
|
||||
ruleset = rulefile.getroot()
|
||||
|
||||
hnd_treedata = ruleset.xpath("./upgrades:TreeData", namespaces=core.xml_namespaces)[0]
|
||||
hnd_name = hnd_treedata.xpath("./upgrades:Name", namespaces=core.xml_namespaces)[0]
|
||||
hnd_primary_color = hnd_treedata.xpath("./upgrades:PrimaryColor", namespaces=core.xml_namespaces)[0]
|
||||
|
||||
buf_mmd.write(textwrap.dedent(f"""
|
||||
----
|
||||
title: {hnd_name.text}
|
||||
theme: base
|
||||
themeVariables:
|
||||
primaryColor: {hnd_primary_color.text}
|
||||
----
|
||||
flowchart LR
|
||||
"""))
|
||||
|
||||
tree_upgrades = []
|
||||
for hnd_upgrade in ruleset.iter("{seagull:rules/upgrades}Upgrade"):
|
||||
hnd_id = hnd_upgrade.xpath("./upgrades:Id", namespaces=core.xml_namespaces)[0]
|
||||
hnd_upgrade_name = hnd_upgrade.xpath("./upgrades:Name", namespaces=core.xml_namespaces)[0]
|
||||
hnd_desc = hnd_upgrade.xpath("./upgrades:Desc", namespaces=core.xml_namespaces)[0]
|
||||
try:
|
||||
hnd_requires = hnd_upgrade.xpath("./upgrades:Requirements", namespaces=core.xml_namespaces)[0]
|
||||
require_list = [elem.text for elem in hnd_requires.iter("{seagull:rules/upgrades}Require")]
|
||||
except IndexError:
|
||||
require_list = []
|
||||
|
||||
upgrade = UpgradeData(
|
||||
id=hnd_id.text,
|
||||
name=hnd_upgrade_name.text,
|
||||
desc=hnd_desc.text,
|
||||
requires=require_list
|
||||
)
|
||||
|
||||
tree_upgrades.append(upgrade)
|
||||
|
||||
tiers = {}
|
||||
dependency_lines = []
|
||||
for upgrade in tree_upgrades:
|
||||
deptier = 0
|
||||
buf_mmd.write(f" {upgrade.id}@{{label: \"{upgrade.name}\"}}\n")
|
||||
collected_tiers = []
|
||||
for require in upgrade.requires:
|
||||
if require in tiers:
|
||||
collected_tiers.append(tiers[require])
|
||||
|
||||
dependency_lines.append(f" {require} --> {upgrade.id}\n")
|
||||
|
||||
if len(collected_tiers) > 0:
|
||||
deptier = max(collected_tiers)
|
||||
|
||||
tiers[upgrade.id] = deptier + 1
|
||||
|
||||
for line in dependency_lines:
|
||||
buf_mmd.write(line)
|
||||
|
||||
buf_mmd.seek(0)
|
||||
print(buf_mmd.read())
|
||||
return get_upgrade_tree_mmd(tree) # TEMP
|
||||
|
||||
def get_upgrade_tree_mmd(tree):
|
||||
if not gamedata.vfs.exists(f"upgrades/{tree}.mmd"):
|
||||
return flask.make_response("No Upgrade Tree", 404)
|
||||
|
||||
with gamedata.vfs.open(f"upgrades/{tree}.mmd") as fd_upgradetree:
|
||||
mmd_upgradetree = mermaid.Mermaid(fd_upgradetree.read(), height=400)
|
||||
return mmd_upgradetree.svg_response.content
|
@@ -1,29 +0,0 @@
|
||||
@require "wordlist"
|
||||
|
||||
[$title: str] @text {
|
||||
<$split_str = [split: <str>]>
|
||||
[cat: [upper: <split_str/0>] [lower: [join: <split_str/1..>]]]
|
||||
}
|
||||
|
||||
{
|
||||
You meet {
|
||||
a `[pick: <wordlist/nouns/birds>] |
|
||||
[title: `{
|
||||
[pick: <wordlist/names/people/butlers>] |
|
||||
[pick: <wordlist/names/people/computing>] |
|
||||
[pick: <wordlist/names/people/founders>] |
|
||||
[pick: <wordlist/names/streets/chicago>] |
|
||||
[pick: <wordlist/names/streets/newyork>] |
|
||||
[pick: <wordlist/names/surnames/english>] |
|
||||
[pick: <wordlist/names/surnames/irish>] |
|
||||
[pick: <wordlist/names/surnames/scottish>]
|
||||
}] the `[pick: <wordlist/nouns/birds>]
|
||||
}. {
|
||||
It completely ignores you. |
|
||||
You have a polite conversation about birdly affairs. |
|
||||
It scoffs and flies away.
|
||||
}
|
||||
} |
|
||||
{
|
||||
A nearby {`[pick: <wordlist/nouns/birds>]|colony of `[pick: <wordlist/nouns/birds>]s} seems to be harassing a human.
|
||||
}
|
@@ -1,55 +0,0 @@
|
||||
@require "wordlist"
|
||||
[$desc_food] @text {
|
||||
{
|
||||
[pick: <wordlist/adjectives/food>] @weight 1.25 |
|
||||
[pick: <wordlist/adjectives/taste>] @weight 1.1 |
|
||||
[pick: <wordlist/names/cities/united_states>] @weight 1.1 |
|
||||
[pick: <wordlist/names/cities/canada>] @weight 0.9 |
|
||||
[pick: <wordlist/names/cities/spain>] @weight 0.75 |
|
||||
[pick: <wordlist/names/cities/alpha>] @weight 0.5 |
|
||||
{ # stuffed/filled/covered
|
||||
`{
|
||||
[pick: <wordlist/nouns/fruit>] @weight 1 |
|
||||
[pick: <wordlist/nouns/meat>] @weight 1 |
|
||||
[pick: <wordlist/nouns/food>] @weight 1 |
|
||||
[pick: <wordlist/nouns/cheese>] @weight 1 |
|
||||
[pick: <wordlist/nouns/condiments>] @weight 1 |
|
||||
[pick: <wordlist/nouns/music_theory>] @weight 0.5 |
|
||||
[pick: <wordlist/nouns/music_production>] @weight 0.5 |
|
||||
[pick: <wordlist/nouns/set_theory>] @weight 0.25 |
|
||||
[pick: <wordlist/nouns/ghosts>] @weight 0.33 |
|
||||
[pick: <wordlist/nouns/web_development>] @weight 0.25
|
||||
} `{stuffed|filled|covered|dipped|coated}
|
||||
} @weight 1 # stuffed/filled/covered
|
||||
}
|
||||
}
|
||||
|
||||
[$get_entree] @text {
|
||||
{
|
||||
[pick: <wordlist/nouns/food>] |
|
||||
[pick: <wordlist/nouns/fast_food>]
|
||||
}
|
||||
}
|
||||
|
||||
##
|
||||
[$mod_order] @text {
|
||||
{
|
||||
add | no | sub | extra |
|
||||
half | left | right | side
|
||||
} `{
|
||||
[pick: <wordlist/nouns/condiments>] |
|
||||
[pick: <wordlist/nouns/cheese>] |
|
||||
[pick: <wordlist/nouns/food>] |
|
||||
[pick: <wordlist/nouns/seasonings>] |
|
||||
[pick: <wordlist/nouns/plants>]
|
||||
}
|
||||
}
|
||||
##
|
||||
|
||||
{
|
||||
a piece of `[pick: <wordlist/nouns/cheese>] cheese |
|
||||
a `{
|
||||
[if: [maybe]{[desc_food]}] [get_entree] |
|
||||
[pick: <wordlist/nouns/fruit>]
|
||||
}
|
||||
}
|
@@ -1,55 +0,0 @@
|
||||
@require "wordlist"
|
||||
[$desc_food] @text {
|
||||
{
|
||||
[pick: <wordlist/adjectives/food>] @weight 1.25 |
|
||||
[pick: <wordlist/adjectives/taste>] @weight 1.1 |
|
||||
[pick: <wordlist/names/cities/united_states>] @weight 1.1 |
|
||||
[pick: <wordlist/names/cities/canada>] @weight 0.9 |
|
||||
[pick: <wordlist/names/cities/spain>] @weight 0.75 |
|
||||
[pick: <wordlist/names/cities/alpha>] @weight 0.5 |
|
||||
{ # stuffed/filled/covered
|
||||
`{
|
||||
[pick: <wordlist/nouns/fruit>] @weight 1 |
|
||||
[pick: <wordlist/nouns/meat>] @weight 1 |
|
||||
[pick: <wordlist/nouns/food>] @weight 1 |
|
||||
[pick: <wordlist/nouns/cheese>] @weight 1 |
|
||||
[pick: <wordlist/nouns/condiments>] @weight 1 |
|
||||
[pick: <wordlist/nouns/music_theory>] @weight 0.5 |
|
||||
[pick: <wordlist/nouns/music_production>] @weight 0.5 |
|
||||
[pick: <wordlist/nouns/set_theory>] @weight 0.25 |
|
||||
[pick: <wordlist/nouns/ghosts>] @weight 0.33 |
|
||||
[pick: <wordlist/nouns/web_development>] @weight 0.25
|
||||
} `{stuffed|filled|covered|dipped|coated}
|
||||
} @weight 1 # stuffed/filled/covered
|
||||
}
|
||||
}
|
||||
|
||||
[$get_entree] @text {
|
||||
{
|
||||
[pick: <wordlist/nouns/food>] |
|
||||
[pick: <wordlist/nouns/fast_food>]
|
||||
}
|
||||
}
|
||||
|
||||
##
|
||||
[$mod_order] @text {
|
||||
{
|
||||
add | no | sub | extra |
|
||||
half | left | right | side
|
||||
} `{
|
||||
[pick: <wordlist/nouns/condiments>] |
|
||||
[pick: <wordlist/nouns/cheese>] |
|
||||
[pick: <wordlist/nouns/food>] |
|
||||
[pick: <wordlist/nouns/seasonings>] |
|
||||
[pick: <wordlist/nouns/plants>]
|
||||
}
|
||||
}
|
||||
##
|
||||
|
||||
{
|
||||
a piece of `[pick: <wordlist/nouns/cheese>] cheese |
|
||||
part of a `{
|
||||
[if: [maybe]{[desc_food]}] [get_entree] |
|
||||
[pick: <wordlist/nouns/fruit>]
|
||||
}
|
||||
}
|
@@ -1,4 +0,0 @@
|
||||
{
|
||||
a watch |
|
||||
a bracelet
|
||||
}
|
@@ -1,4 +0,0 @@
|
||||
{
|
||||
a watch |
|
||||
a bracelet
|
||||
}
|
@@ -1,3 +1,3 @@
|
||||
pyqt6>=6.9.1
|
||||
pyqtwebengine>=5.15.7
|
||||
pywebview[qt]>=5.4
|
||||
pyqt6-webengine>=6.9.0
|
||||
pywebview[qt6]>=5.4
|
@@ -1,4 +1,6 @@
|
||||
Flask>=3.1.1
|
||||
flask-cors>=6.0.1
|
||||
gevent>=25.5.1
|
||||
lxml>=6.0.0
|
||||
fs>=2.4.16
|
||||
fs>=2.4.16
|
||||
mermaid-py==0.8.0
|
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ItemRules xmlns="seagull:rules/items">
|
||||
<Food>
|
||||
<Min>1</Min>
|
||||
<Max>10</Max>
|
||||
</Food>
|
||||
<Shinies>
|
||||
<Min>1</Min>
|
||||
<Max>20</Max>
|
||||
</Shinies>
|
||||
<Food StoryBeat="3">
|
||||
<Min>1</Min>
|
||||
<Max>20</Max>
|
||||
</Food>
|
||||
<Shinies StoryBeat="3">
|
||||
<Min>0</Min>
|
||||
<Max>50</Max>
|
||||
</Shinies>
|
||||
<Psi StoryBeat="3">
|
||||
<Min>0</Min>
|
||||
<Max>15</Max>
|
||||
</Psi>
|
||||
</ItemRules>
|
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ItemRules xmlns="seagull:rules/items">
|
||||
<Food>
|
||||
<Min>1</Min>
|
||||
<Max>5</Max>
|
||||
</Food>
|
||||
<Shinies>
|
||||
<Min>1</Min>
|
||||
<Max>10</Max>
|
||||
</Shinies>
|
||||
<Food StoryBeat="3">
|
||||
<Min>5</Min>
|
||||
<Max>20</Max>
|
||||
</Food>
|
||||
<Shinies StoryBeat="3">
|
||||
<Min>5</Min>
|
||||
<Max>50</Max>
|
||||
</Shinies>
|
||||
<Psi StoryBeat="3">
|
||||
<Min>0</Min>
|
||||
<Max>15</Max>
|
||||
</Psi>
|
||||
</ItemRules>
|
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<UpgradeRules xmlns="seagull:rules/upgrades">
|
||||
<Upgrade>
|
||||
<Id>speed1</Id>
|
||||
<Name>Speedier Seagull</Name>
|
||||
<Modifiers>
|
||||
<Mod Id="up_speed1" Name="Upgrade: Speedier Seagull" Speed="5" />
|
||||
</Modifiers>
|
||||
<Desc>You become just a little bit faster, which makes it easier to steal things before your prey's previous owners notice you're coming.</Desc>
|
||||
</Upgrade>
|
||||
<Upgrade>
|
||||
<Id>theft_chance1</Id>
|
||||
<Name>Swooping Techniques</Name>
|
||||
<Modifiers>
|
||||
<Mod Id="up_theft_chance1" Name="Upgrade: Swooping Techniques" ChanceSteal="10" />
|
||||
</Modifiers>
|
||||
<Desc>It's all in the neck. The wings are just the steering wheel. You gain a bonus on all dice rolls for stealing.</Desc>
|
||||
</Upgrade>
|
||||
</UpgradeRules>
|
@@ -1,20 +0,0 @@
|
||||
<div id="charsheet-leftside">
|
||||
<div class="attr" id="attr-agility">
|
||||
Agility: <span id="lbl-attr-agility">0</span>
|
||||
</div>
|
||||
<div class="attr" id="attr-instinct">
|
||||
<span id="lbl-attr-instinct-txt">Instinct</span>: <span id="lbl-attr-instinct">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="charsheet-rightside">
|
||||
<div id="charsheet-upgrade-tabbar">
|
||||
<nav id="nav-upgrades"><ul>
|
||||
<li><button id="btn-upgrade-agility">Agility Upgrades</button></li>
|
||||
<li><button id="btn-upgrade-instinct">Instinct Upgrades</button></li>
|
||||
</ul></nav>
|
||||
</div>
|
||||
<div id="charsheet-upgrade-tree">
|
||||
<pre class="mermaid" id="upgrade-tree">
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
@@ -1,2 +0,0 @@
|
||||
{% if not desktop %}IP: {{ipaddr}}<br />{% endif %}
|
||||
<button id="dev-reset" onClick="reset_game()">Reset Game</button>
|
@@ -1,65 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- Your server today is {{ svchost }} -->
|
||||
<html>
|
||||
<head>
|
||||
<title>Seagull Game</title>
|
||||
{%- for style in styles -%}
|
||||
<link rel="stylesheet" href="{{ style }}">
|
||||
{%- endfor -%}
|
||||
{%- for script in scripts -%}
|
||||
<script src="{{ script }}"></script>
|
||||
{%- endfor -%}
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<div style="background:yellow;border:2px red;">
|
||||
<h1>This doesn't work without JavaScript.</h1><br />
|
||||
<h2>You're probably using a browser extension or privacy tool that disables it.</h2>
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="root">
|
||||
|
||||
<div id="main-sidebar">
|
||||
<div id="side-seagull-image"> <img width="256" src={{ seagull_pic }}> </div>
|
||||
<div id="side-seagull-name"><span id="lbl-seagull-name">Nameless</span> <a href="javascript:change_seagull_name()">✏️</a></div>
|
||||
<div id="side-seagull-name-editor"><input type="text" id="edt-seagull-name"> <a href="javascript:confirm_seagull_name()">✅</a><a href="javascript:cancel_seagull_name()">❌</a></div>
|
||||
<div id="side-seagull-stats">
|
||||
<p id="side-seagull-lvl">Lv <span id="lbl-seagull-lvl">1</span> <span id="lbl-seagull-class">LoadError</span></p>
|
||||
<p id="side-seagull-xp">XP: <span id="lbl-seagull-xp-current">0</span>/<span id="lbl-seagull-xp-next">0</span></p>
|
||||
<p id="side-seagull-misc">
|
||||
Colony: <span id="lbl-seagull-colony">1337</span><br />
|
||||
Shinies: <span id="lbl-seagull-shinies">420</span><br />
|
||||
Food: <span id="lbl-seagull-food">69</span>
|
||||
</p>
|
||||
</div>
|
||||
<hr />
|
||||
<div id="side-action-bar">
|
||||
<p>Human encounters: <select id="menu-enc-human">
|
||||
<option value="pause">Stop and ask</option>
|
||||
<option value="steal-food">Steal food</option>
|
||||
<option value="steal-shinies">Steal shiny things</option>
|
||||
</select></p>
|
||||
<p>Seagull encounters: <select id="menu-enc-seagull">
|
||||
<option value="pause">Stop and ask</option>
|
||||
<option value="recruit">Attempt recruiting</option>
|
||||
<option value="steal-food">Steal food</option>
|
||||
<option value="steal-shinies">Steal shiny things</option>
|
||||
</select></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="main-content">
|
||||
<div id="main-header">
|
||||
<div id="main-day-stats">It has been <span id="main-day-counter">a cosmically unknowable number of</span> days.</div>
|
||||
<div id="main-button-bar">
|
||||
<button id="button-charsheet" class="main-bar">📊</button>
|
||||
<button id="button-settings" class="main-bar">⚙️</button>
|
||||
<button id="button-about" class="main-bar">ℹ️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main-log"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@@ -1,12 +0,0 @@
|
||||
---
|
||||
title: Agility Upgrades
|
||||
---
|
||||
flowchart LR
|
||||
speed1["Speedier Seagull"]
|
||||
speed2["Greased Wings"]
|
||||
theft_chance1["Swooping Techniques"]
|
||||
theft_chance2["The Element of Surprise"]
|
||||
|
||||
speed1-->speed2
|
||||
theft_chance1-->speed1
|
||||
theft_chance1-->theft_chance2
|
Reference in New Issue
Block a user