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:
2025-09-29 20:31:42 -07:00
parent a5f837189b
commit 0edb4b50d2
71 changed files with 4895 additions and 127 deletions

3
.gitignore vendored
View File

@@ -7,5 +7,8 @@ seagull.pak
build/**
build_cache/**
# autogenerated
doc/**
**/__pycache__/**
**.pyc

View File

@@ -1,3 +1,4 @@
app/**
ext/imsky/wordlists/**
static/image/splash.png
seagull.spec

16
.woodpecker/Dockerfile Normal file
View File

@@ -0,0 +1,16 @@
FROM python:3.13-alpine AS build-rust
# install rust environment (for rant)
ENV RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/cargo PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin RUST_VERSION=1.61.0
RUN --mount=type=cache,target=/var/cache/apk apk add --update-cache rustup gcc musl-dev rsync
RUN rustup-init -y --profile minimal
# install rant
RUN --mount=type=cache,target=/root/.cargo \
--mount=type=cache,target=/usr/local/cargo/git/db \
--mount=type=cache,target=/usr/local/cargo/registry \
cargo install --color=never rant --version 4.0.0-alpha.33 --root /opt/rant --features cli
FROM python:3.13-alpine AS buildenv
ENV RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/cargo PATH=/opt/rant/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin RUST_VERSION=1.61.0
COPY --from=build-rust /opt/rant /opt/rant

View File

@@ -0,0 +1,12 @@
when:
- event: push
branch: main
path: .woodpecker/Dockerfile
steps:
- name: build
image: woodpeckerci/plugin-docker-buildx
settings:
repo: vcs.otl-hga.net/nicole/seagull-game
registry: vcs.otl-hga.net
dockerfile: .woodpecker/Dockerfile
tag: buildenv

17
.woodpecker/build.yaml Normal file
View File

@@ -0,0 +1,17 @@
when:
- event: push
branch: trunk
steps:
- name: build linux
image: vcs.otl-hga.net/nicole/seagull-game:buildenv
commands:
- mkdir build && cd build
- cargo install rant --version 4.0.0-alpha.33 --features cli --root $(pwd)/opt/rant
- mkdir -p app/rant
- python ../render-wordlists.py -i ../ext/imsky/wordlists -o rant ./app/rant/wordlist.rant
- rsync -r ../app .
- pip install -r app/requirements.txt
- pip install -r app/requirements-build-desktop.txt
- pip install -r app/requirements-desktop.txt
- pip install -r app/requirements-desktop-linux.txt
- pyinstaller seagull.spec

72
CMakeLists.txt Normal file
View File

@@ -0,0 +1,72 @@
cmake_minimum_required(VERSION 4.1)
project(seagull)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
find_package(Python3 REQUIRED COMPONENTS Interpreter)
set(sys_python ${Python3_EXECUTABLE})
find_package(Cargo)
find_program(RSYNC NAMES rsync)
find_program(ZIP NAMES zip)
file(GLOB_RECURSE pydepends RELATIVE "${CMAKE_SOURCE_DIR}"
"${CMAKE_SOURCE_DIR}/app/*.py")
file(GLOB_RECURSE basepakdepends RELATIVE "${CMAKE_SOURCE_DIR}"
"${CMAKE_SOURCE_DIR}/app/basepak/*")
file(GLOB_RECURSE pakargs RELATIVE "${CMAKE_SOURCE_DIR}/pak"
"${CMAKE_SOURCE_DIR}/pak/*")
file(GLOB_RECURSE pakdepends RELATIVE "${CMAKE_SOURCE_DIR}"
"${CMAKE_SOURCE_DIR}/pak/*")
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dist")
add_custom_target(pak ALL
DEPENDS ${pakdepends}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/pak"
COMMENT "Creating seagull.pak"
COMMAND ${ZIP} -7r "${CMAKE_CURRENT_BINARY_DIR}/dist/seagull.pak" .)
# wordlists
set(WORDLIST_SOURCE_PATH "${CMAKE_SOURCE_DIR}/ext/imsky/wordlists" CACHE PATH "Source location of wordlists to use.")
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/basepak/rant")
add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/basepak/rant/wordlist.rant"
COMMENT "Rendering wordlists"
COMMAND "${sys_python}" "${CMAKE_SOURCE_DIR}/render-wordlists.py" -i "${WORDLIST_SOURCE_PATH}" -o rant "${CMAKE_CURRENT_BINARY_DIR}/basepak/rant/wordlist.rant")
# build rant
set(rant_path "${CMAKE_CURRENT_BINARY_DIR}/opt/rant")
add_custom_command(OUTPUT "${rant_path}"
COMMENT "Compiling rant"
COMMAND "${CARGO_EXECUTABLE}" install rant --version 4.0.0-alpha.33 --root "${rant_path}" --features cli)
# venv
set(venv_path "${CMAKE_CURRENT_BINARY_DIR}/pyvenv")
execute_process(COMMAND "${Python3_EXECUTABLE}" -m venv "${venv_path}" --upgrade-deps
COMMAND_ERROR_IS_FATAL ANY)
set(ENV{VIRTUAL_ENV} "${venv_path}")
set(Python3_FIND_VIRTUALENV ONLY)
unset(Python3_EXECUTABLE)
# re-find python to use venv
find_package(Python3 REQUIRED COMPONENTS Interpreter)
if (sys_python STREQUAL Python3_EXECUTABLE)
message(FATAL_ERROR "Still using Python3 after venv activation")
endif()
execute_process(COMMAND "${Python3_EXECUTABLE}" -m pip install -r "${CMAKE_SOURCE_DIR}/app/requirements.txt")
execute_process(COMMAND "${Python3_EXECUTABLE}" -m pip install -r "${CMAKE_SOURCE_DIR}/app/requirements-desktop.txt")
execute_process(COMMAND "${Python3_EXECUTABLE}" -m pip install -r "${CMAKE_SOURCE_DIR}/app/requirements-build-desktop.txt")
execute_process(COMMAND "${Python3_EXECUTABLE}" -m pip install -r "${CMAKE_SOURCE_DIR}/app/requirements-desktop-linux.txt")
find_program(PYINSTALLER NAMES pyinstaller HINTS "${venv_path}/bin" REQUIRED)
if (WIN32)
set(binname "seagull.exe")
else()
set(binname "seagull")
endif()
configure_file("seagull.spec.in" "seagull.spec")
add_custom_command(OUTPUT "dist/${binname}"
DEPENDS "${rant_path}" "${CMAKE_CURRENT_BINARY_DIR}/basepak/rant/wordlist.rant" ${pydepends} ${basepakdepends}
COMMENT "Building ${binname}"
COMMAND ${PYINSTALLER} --distpath "${CMAKE_CURRENT_BINARY_DIR}/dist" --workpath "${CMAKE_CURRENT_BINARY_DIR}/pyinstaller" seagull.spec)
add_custom_target(desktop ALL DEPENDS "dist/${binname}" pak)

2995
Doxyfile Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,30 @@
# seagull-game
# Seagull Game
Seagull game built on AWS, designed to learn cloud stuff.
A long time ago, the first versions of Seagull Game were a prototype, just enough of a workload
to serve as an example while on my AWS Solutions Architect apprenticeship. But with the economic
crash of 2025 came a loss of my job and a lot of excess free time, so I'm making this into a
real deal game that you can play, in an attempt to preserve my rapidly dwindling sanity.
## Build
### Prerequisites
* Python (>= 3.11 oughta do it)
* TCL/Tk (for `tkinter` Python module, used by PyInstaller splash screen)
* A working Rust toolchain, incl. `cargo`, to build [Rant]
* rsync (or rclone in Windows)
* On Linux, `tar` and `zstd` are used to cache intermediate build steps into archives. `zip` is also used to generate `.pak` files.
### Linux
Simply run `./build-desktop.sh`, followed by `./build-pak.sh` to build seagull.pak.
### Windows
A script is in progess to automate this, but is not yet fully ready. Here's the broad strokes routine:
* Install [Python from python.org][cpython-win]. Non standard variants such as ActivePython are not supported.
* Install [rclone] and [Rust][rust-downloads]. For Rust, scroll down to the bottom of the first table: you probably want `x86_64-pc-windows-msvc`, as an MSI file.
* Create a folder called "build" wherever you checked out this repository, and open a PowerShell terminal there.
* `python -m venv pyvenv`, then `Scripts\activate.ps1`
[Rant]: https://github.com/rant-lang/rant
[cpython-win]: https://www.python.org/downloads/windows/
[rclone]: https://rclone.org/downloads/
[rust-downloads]: https://forge.rust-lang.org/infra/other-installation-methods.html

View File

@@ -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>

View 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;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -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 ...")

View File

@@ -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({

View File

@@ -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")

View File

@@ -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()

View File

@@ -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)

View File

@@ -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()

View File

@@ -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:

View File

@@ -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):

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

39
cmake/FindCargo.cmake Normal file
View File

@@ -0,0 +1,39 @@
# Find Cargo, possibly in ~/.cargo. Make sure to check in any `bin` subdirectories
# on the program search path
# TODO: Remove the Unix-ism ($ENV{HOME}) and replace it with something platform-agnostic.
find_program(CARGO_EXECUTABLE cargo PATHS "$ENV{HOME}/.cargo" PATH_SUFFIXES bin)
set(CARGO_VERSION "")
set(CARGO_CHANNEL "stable")
# If we found it, see if we can get its version.
if(CARGO_EXECUTABLE)
execute_process(COMMAND ${CARGO_EXECUTABLE} -V OUTPUT_VARIABLE CARGO_VERSION_OUTPUT OUTPUT_STRIP_TRAILING_WHITESPACE)
if(CARGO_VERSION_OUTPUT MATCHES "cargo ([0-9]+\\.[0-9]+\\.[0-9]+).*")
set(CARGO_VERSION ${CMAKE_MATCH_1})
endif()
execute_process(COMMAND ${CARGO_EXECUTABLE} -V OUTPUT_VARIABLE CARGO_CHANNEL_OUTPUT OUTPUT_STRIP_TRAILING_WHITESPACE)
if(CARGO_CHANNEL_OUTPUT MATCHES "cargo [0-9]+\\.[0-9]+\\.[0-9]+-([a-zA-Z]*).*")
set(CARGO_CHANNEL ${CMAKE_MATCH_1})
endif()
endif()
# Hides the CARGO_EXECUTABLE variable unless advanced variables are requested
mark_as_advanced(CARGO_EXECUTABLE)
# Require that we find both the executable and the version. Otherwise error out.
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(
Cargo
REQUIRED_VARS
CARGO_VERSION
CARGO_CHANNEL
CARGO_EXECUTABLE
VERSION_VAR
CARGO_VERSION
)

110
libexec/doxygen.js Executable file
View File

@@ -0,0 +1,110 @@
#!/usr/bin/env node
// https://gist.github.com/dimitarcl/3767879
var fs = require('fs')
var functionName = /^\s+\/\/\/\s+@function\s+(.*)$/;
var type = /^(\s+)\/\/\/\s+@param\s+{(\w*)}\s+(.+?)(\s+.*)$/;
var param = /^(\s+)\/\/\/\s+@param\s+(.+?)\s/;
var resultType = /^(\s+)\/\/\/\s+@return\s+{(\w+)}(\s+.*)$/;
function Section()
{
this.name = '';
this.result = 'undefined';
this.args = [];
this.comments = [];
this.namespaces = [];
}
Section.prototype.handle_function = function (line) {
this.namespaces = line.match(functionName)[1].split('.') || [];
this.name = this.namespaces.pop();
};
Section.prototype.handle_param = function (line) {
var paramType = 'Object';
var name = '';
var m = line.match(type);
var r = line;
if (m) {
paramType = m[2];
name = m[3];
r = m[1] + '/// @param ' + name + m[4];
}
else {
m = line.match(param);
name = m[2];
}
this.args.push({name: name, type: paramType});
this.comments.push(r);
};
Section.prototype.handle_return = function (line) {
this.result = 'undefined';
var m = line.match(resultType);
var r = line;
if (m) {
this.result = m[2];
r = m[1] + '/// @return ' + m[3];
}
this.comments.push(r);
};
Section.prototype.Generate = function () {
var doc = [];
this.namespaces.forEach(function (namespace) {
doc.push('namespace ' + namespace + ' {\n');
});
this.comments.forEach(function (c) {
doc.push(c);
});
var args = [];
this.args.forEach(function (argument) {
args.push(argument.type + ' ' + argument.name);
});
if (this.name) {
doc.push(this.result + ' ' + this.name + '(' + args.join(', ') + ');');
}
this.namespaces.forEach(function (namespace) {
doc.push('}\n');
});
return doc.join('\n');
};
Section.prototype.handle_line = function (line) {
this.comments.push(line);
};
function writeLine(line) {
process.stdout.write(line + '\n');
}
fs.readFile(process.argv[2], 'utf8', function (err, data) {
var lines = data.split('\n');
var comment = /^\s*\/\/\//;
var directive = /@(\w+)\s+(.*)$/;
var inside = false;
var section = new Section();
lines.forEach(function(line) {
if (line.match(comment)) {
var d = line.match(directive);
if (d) {
var handle = Section.prototype['handle_' + d[1]] || Section.prototype.handle_line;
handle.call(section, line);
} else {
section.handle_line(line);
}
inside = true;
} else if (inside) {
writeLine(section.Generate());
inside = false;
section = new Section();
}
});
});

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<UpgradeRules xmlns="seagull:rules/upgrades">
<TreeData>
<Name>Agility Upgrades</Name>
<PrimaryColor>#00aa00</PrimaryColor>
</TreeData>
<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>speed2</Id>
<Name>Greased Wings</Name>
<Desc>Applying a thin coat of old french fry oil makes you much faster. Why do humans throw this stuff out?</Desc>
<Requirements>
<Require>speed1</Require>
</Requirements>
<Modifiers>
<Mod Id="up_speed2" Name="Upgrade: Greased Wings" Speed="10" />
</Modifiers>
</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>
<Upgrade>
<Id>theft_chance2</Id>
<Name>The Element of Surprise</Name>
<Desc>It's a lot easier to steal things if the previous owner doesn't see you coming. This technique gives you a bigger bonus on stealing rolls.</Desc>
<Requirements>
<Require>theft_chance1</Require>
</Requirements>
<Modifiers>
<Mod Id="up_theft_chance2" Name="Upgrade: The Element of Surprise" ChanceSteal="15" />
</Modifiers>
</Upgrade>
</UpgradeRules>

View File

@@ -1,5 +1,7 @@
html, body { height: 100% }
/** MAIN GAME **/
div#root {
display: flex;
width: 100%;
@@ -50,6 +52,7 @@ div#main-day-stats {
margin-top: auto;
margin-bottom: auto;
vertical-align: middle;
font-size: large;
}
div#main-button-bar {
@@ -101,37 +104,129 @@ div#charsheet {
background-color: rgb(240, 240, 240);
}
div#charsheet-leftside {
display: flex;
flex-direction: column;
}
div#charsheet-rightside {
display: flex;
flex-direction: column;
}
div.attr {
font-size: larger;
}
/** MODAL **/
div#modal-background {
z-index: -10;
background-color: rgba(0, 0, 0, 168);
visibility: hidden;
font-family: sans-serif;
background-color: rgba(0, 0, 0, 0.6);
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
}
div#modal {
width: 90%;
height: 90%;
border: 1.25em double rgba(192, 192, 192, 255);
width: 75%;
height: 75%;
margin: auto;
margin-top: 50px;
border: 0.25em dotted rgba(192, 192, 192, 255);
background-color: rgba(255, 255, 255, 255);
padding: 0.3em
}
button.main-bar {
width: 2.5em;
height: 2.5em;
margin: 2.5px;
background-color: rgba(0,0,0,0);
border: 1px solid black;
font-size: 2em;
}
button#button-modal-close {
position: fixed;
top: 0;
right: 0;
width: 2.5em;
height: 2.5em;
margin: 2.5px;
background-color: rgba(0,0,0,0);
border: 0px;
font-size: 2em;
}
/** CHARSHEET **/
div#charsheet-root {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
}
div#charsheet-leftside {
display: flex;
flex-direction: column;
width: 33%;
height: 100%;
}
div.attr {
/* common to all attribute blocks */
font-size: x-large;
color: #ffffff;
width: 100%;
height: 30%;
vertical-align: middle;
margin-top: auto;
margin-bottom: auto;
text-align: center;
margin-left: auto;
margin-right: auto;
}
div#attr-points {
font-size: x-large;
width: 100%;
height: 10%;
vertical-align: middle;
margin-top: auto;
margin-bottom: auto;
text-align: center;
margin-left: auto;
margin-right: auto;
}
div#attr-agility {
background: linear-gradient(to right, rgb(0,170,0), rgb(0,99,0));
}
div#attr-instinct {
background: linear-gradient(to right, rgb(170,0,255), rgb(90,0,135));
}
div#attr-leadership {
background: linear-gradient(to right, rgb(255,170,0), rgb(139,93,0));
}
div#charsheet-rightside {
display: flex;
flex-direction: column;
width: 67%;
height: 100%;
overflow-x: scroll;
}
nav#nav-upgrades {
display: flex;
flex-direction: row;
}
nav#nav-upgrades li {
display: inline;
}
div#charsheet-upgrade-tree {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
width: max-content;
}

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

BIN
pak/static/image/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

View File

@@ -0,0 +1,32 @@
var charsheet_elements = {}
charsheet_elements["lbl_agility"] = document.getElementById("lbl-attr-agility");
charsheet_elements["lbl_instinct"] = document.getElementById("lbl-attr-instinct");
charsheet_elements["lbl_leadership"] = document.getElementById("lbl-attr-leadership");
charsheet_elements["lbl_instinct_txt"] = document.getElementById("lbl-attr-instinct-txt");
charsheet_elements["btn_upgrade_agility"] = document.getElementById("btn-upgrade-agility");
charsheet_elements["btn_upgrade_instinct"] = document.getElementById("btn-upgrade-instinct");
charsheet_elements["btn_upgrade_leadership"] = document.getElementById("btn-upgrade-leadership");
charsheet_elements["blk_tree"] = document.getElementById("charsheet-upgrade-tree");
function update_charsheet() {
charsheet_elements["lbl_agility"].innerHTML = gamestate["agility"];
charsheet_elements["lbl_instinct"].innerHTML = gamestate["instinct"];
charsheet_elements["lbl_leadership"].innerHTML = gamestate["leadership"];
if (gamestate["story_beat"] >= 3) {
charsheet_elements["lbl_instinct_txt"].innerHTML = "Intelligence";
charsheet_elements["btn_upgrade_instinct"].innerHTML = "Intelligence Upgrades";
}
}
async function display_tree(tree) {
var upgrade_tree = await fetch(`/upgrades/${tree}`)
.then(res => res.text())
console.log(upgrade_tree)
charsheet_elements["blk_tree"].innerHTML = upgrade_tree
}
charsheet_elements["btn_upgrade_agility"].addEventListener("click", (ev) => {display_tree("agility")});
charsheet_elements["btn_upgrade_instinct"].addEventListener("click", (ev) => {display_tree("instinct")});
charsheet_elements["btn_upgrade_leadership"].addEventListener("click", (ev) => {display_tree("leadership")});

View File

@@ -1,5 +1,4 @@
var desktop_mode = true;
var tick_meter_running = false;
globalThis.desktop_mode = true;
async function prepare_gamestate() {
var gamestate_loaded = null;

View File

@@ -1,6 +1,6 @@
var desktop_mode = false;
globalThis.desktop_mode = false;
function prepare_gamestate() {
function load_gamestate() {
var gamestate_loaded = window.localStorage.getItem("gamestate");
if (gamestate_loaded == null) {

View File

@@ -2,13 +2,16 @@ const ver_numeric = 0;
const ver_string = "pre alpha";
const sleep = ms => new Promise(r => setTimeout(r, ms)); // sleep(int ms)
const avg = input => input.reduce((a,b) => a+b) / input.length; // avg([1,2,3...])
const urlExists = async url => (await fetch(url)).ok
var page_elements = {};
var gamestate = {};
globalThis.gamestate = {};
globalThis.tick_meter_running = false;
var ticks_since_last_save = 0;
const gamestate_default = {
globalThis.gamestate_default = {
"statever": "1",
"tick": 1,
"name": "Nameless",
@@ -25,11 +28,28 @@ const gamestate_default = {
"enc_seagull": "pause",
"agility": 0,
"instinct": 0,
"leadership": 0
"leadership": 0,
"income": {
"last_food": Array(10).fill(0),
"last_shinies": Array(10).fill(0),
"calc_food": 0,
"calc_shinies": 0
},
"modifiers": {
"speed": [],
"chancesteal": []
},
"upgrades": []
};
const tickdiffs_reset = {
"food": 0,
"shinies": 0
}
var tickdiffs = {}
var bool_log_alt = false
function record_log(text) {
globalThis.record_log = function (text) {
const div_logrow = document.createElement("div");
if (bool_log_alt) { div_logrow.className = "log-line"; }
else { div_logrow.className = "log-line-alt"; }
@@ -87,37 +107,91 @@ function record_log_with_choices() {
tick_meter_running = false;
}
var modal_dialog_open = false;
globalThis.modal_dialog_open = false;
globalThis.modal_dialog_scripted = false;
globalThis.modal_dialog_name = "";
var dialog_queue = [];
function modal_no_prop(event) { event.stopPropagation(); }
async function open_modal_dialog(dialog) {
var modal_background = document.getElementById("modal-background");
if (!modal_background) {
modal_background = document.createElement("div");
modal_background.setAttribute("id", "modal-background");
}
var modal_root = document.getElementById("modal");
if (!modal_root) {
modal_root = document.createElement("div");
modal_root.setAttribute("id", "modal");
modal_root.onclick = modal_no_prop;
modal_background.appendChild(modal_root);
}
if (!modal_dialog_open) {
tick_meter_running = false;
modal_dialog_open = true;
modal_background.style.zIndex = "10 !important";
modal_background.style.visibility = "visible !important";
modal_dialog_name = dialog;
var modal_background = document.createElement("div");
modal_background.setAttribute("id", "modal-background");
modal_background.style.zIndex = "10";
modal_background.style.visibility = "visible";
modal_background = document.body.appendChild(modal_background);
var modal_close = document.createElement("button");
modal_close.setAttribute("id", "button-modal-close");
modal_close.innerHTML = "❌";
modal_close.addEventListener("click", (ev) => {close_modal_dialog()});
modal_close = modal_background.appendChild(modal_close);
var modal_root = document.createElement("div");
modal_root.setAttribute("id", "modal");
modal_root.onclick = modal_no_prop;
modal_root = modal_background.appendChild(modal_root);
var dialog_data = await fetch(`/dialog/${dialog}`)
.then(res => { return res.text(); });
modal_root.innerHTML = dialog_data;
if (urlExists(`/static/js/dlg-${dialog}.js`)) {
//*
var script = document.createElement("script");
script.setAttribute("id", "dialog-script");
script.src = `/static/js/dlg-${dialog}.js`;
document.head.appendChild(script);
modal_dialog_scripted = true;
}
} else {
var dialog_data = await fetch(`/dialog/${dialog}`)
.then(res => { return res.text(); });
var dialog_script = null;
if (urlExists(`/static/js/dlg-${dialog}.js`)) {
dialog_script = `/static/js/dlg-${dialog}.js`;
}
dialog_queue.push([dialog_data, dialog_script, dialog]);
}
}
async function close_modal_dialog() {
if (!modal_dialog_open) { return; }
var modal_background = document.getElementById("modal-background");
var modal_root = document.getElementById("modal-root");
var dialog_script = document.getElementById("dialog-script");
if (dialog_script) {
document.head.removeChild(dialog_script);
}
dialog_data = await fetch(`/dialog/${dialog}`)
.then(res => { return res.text(); });
if (dialog_queue.length == 0) {
modal_background.style.zIndex = "-10";
modal_background.style.visibility = "hidden";
document.body.removeChild(modal_background);
modal_root.innerHTML = dialog_data;
tick_meter_running = true;
modal_dialog_open = false;
modal_dialog_name = "";
modal_dialog_scripted = false;
} else {
next_dialog = dialog_queue.pop();
modal_root.innerHTML = next_dialog[0];
modal_dialog_name = next_dialog[2];
if (next_dialog[1]) {
script = document.createElement("script");
script.setAttribute("id", "dialog-script");
script.src = next_dialog[1];
document.head.appendChild(script);
modal_dialog_scripted = true;
} else { modal_dialog_scripted = false; }
}
}
function update_ui() {
@@ -126,6 +200,8 @@ function update_ui() {
page_elements["lbl_colony"].innerHTML = gamestate["colony"];
page_elements["lbl_shinies"].innerHTML = gamestate["shinies"].toFixed(2);
page_elements["lbl_food"].innerHTML = gamestate["food"].toFixed(2);
page_elements["lbl_inc_food"].innerHTML = gamestate["income"]["calc_food"].toFixed(2);
page_elements["lbl_inc_shinies"].innerHTML = gamestate["income"]["calc_shinies"].toFixed(2);
page_elements["lbl_class"].innerHTML = gamestate["class"];
page_elements["lbl_xp"].innerHTML = gamestate["xp"];
page_elements["lbl_xp_next"].innerHTML = gamestate["xp_next"];
@@ -160,10 +236,11 @@ function dev_toolbox(open) {
function reward_xp(amount) {
gamestate["xp"] += amount;
if (gamestate["xp"] >= gamestate["xp_next"]) {
old_xp_next = gamestate["xp_next"];
var old_xp_next = gamestate["xp_next"];
gamestate["xp"] -= old_xp_next;
gamestate["level"] += 1;
gamestate["xp_next"] = (old_xp_next * 1.5) + (gamestate["level"] * 5);
record_log(`You have advanced to level ${gamestate["level"]}.`);
if (gamestate["level"] == 2) {
gamestate["story_beat"] = 1;
@@ -176,21 +253,22 @@ function reward_xp(amount) {
}
}
async function steal_resource(resource, target, amount, itemstr) {
globalThis.steal_resource = async function (resource, target, amount, itemstr) {
var items = itemstr.split(",")
var stealdata = await fetch(`/act/steal/${resource}/${target}`, {method: "POST", body: JSON.stringify({gamestate: gamestate})})
var stealdata = await fetch(`/act/steal/${resource}/${target}`, {method: "POST", headers: {"Content-Type": "application/json"},body: JSON.stringify({gamestate: gamestate})})
.then(res => { return res.json(); })
.catch(e => { throw e; });
if (stealdata["success"] && amount > 0) {
gamestate[resource] += amount;
tickdiffs[resource] += amount;
reward_xp(2);
record_log(`Stole ${resource} from a ${target}: ${items.join(", ")}`);
}
else { record_log(`Didn't steal ${resource} from a ${target}`); }
}
async function recruit(amount) {
globalThis.recruit = async function (amount) {
if (gamestate["shinies"] < amount) {
record_log("You do not have enough shinies to recruit this seagull.");
return;
@@ -208,13 +286,53 @@ async function recruit(amount) {
else { record_log("The other gull wasn't impressed. Recruiting failed."); }
}
const hnd_devtoolkit = new Konami(() => {
if (modal_dialog_name == "about") {
close_modal_dialog();
dev_toolbox(true);
var snd = new Audio("/static/sound/open_dev_toolkit.wav");
snd.play();
}
})
async function game_tick() {
gamestate["tick"] += 1;
ticks_since_last_save += 1;
page_elements["lbl_tick"].innerHTML = gamestate["tick"];
if (gamestate["tick"] % 5 == 0) {
var colony_tickdata = await fetch("/tick/colony", {
method: "POST",
body: JSON.stringify({
colony: gamestate["colony"] - 1,
modifiers: [],
avg_food: gamestate["income"]["calc_food"],
avg_shinies: gamestate["income"]["calc_shinies"]
}),
headers: {
"Content-Type": "application/json"
}
})
.then(res => {
var json = res.json()
console.log(json)
return json
})
.catch(e => {throw e;});
if (colony_tickdata["success"]) {
gamestate["food"] += colony_tickdata["food"];
tickdiffs["food"] += colony_tickdata["food"];
gamestate["shinies"] += colony_tickdata["shinies"];
tickdiffs["shinies"] += colony_tickdata["shinies"];
record_log(`Your colony provides you with ${colony_tickdata["food"].toFixed(2)} food and ${colony_tickdata["shinies"].toFixed(2)} shinies.`);
}
}
var tickdata = await fetch("/tick")
.then(res => {
json = res.json()
var json = res.json()
console.log(json)
return json
})
@@ -352,6 +470,15 @@ async function game_tick() {
ticks_since_last_save = 0;
}
gamestate["income"]["last_food"].shift()
gamestate["income"]["last_food"].push(tickdiffs["food"])
gamestate["income"]["last_shinies"].shift()
gamestate["income"]["last_shinies"].push(tickdiffs["shinies"])
tickdiffs = structuredClone(tickdiffs_reset);
gamestate["income"]["calc_food"] = avg(gamestate["income"]["last_food"])
gamestate["income"]["calc_shinies"] = avg(gamestate["income"]["last_shinies"])
update_ui();
}
@@ -374,6 +501,8 @@ function update_action(enc, value) {
}
target.addEventListener(start_event, function (ev) {
tickdiffs = structuredClone(tickdiffs_reset);
page_elements["div_log"] = document.querySelector("#main-log");
page_elements["div_sidebar"] = document.querySelector("#main-sidebar");
page_elements["div_name"] = document.querySelector("#side-seagull-name");
@@ -383,6 +512,8 @@ target.addEventListener(start_event, function (ev) {
page_elements["lbl_colony"] = document.querySelector("#lbl-seagull-colony");
page_elements["lbl_shinies"] = document.querySelector("#lbl-seagull-shinies");
page_elements["lbl_food"] = document.querySelector("#lbl-seagull-food");
page_elements["lbl_inc_food"] = document.querySelector("#lbl-seagull-food-income");
page_elements["lbl_inc_shinies"] = document.querySelector("#lbl-seagull-shinies-income");
page_elements["edt_name"] = document.querySelector("#edt-seagull-name");
page_elements["lbl_tick"] = document.querySelector("#main-day-counter");
page_elements["lbl_xp"] = document.querySelector("#lbl-seagull-xp-current");
@@ -397,6 +528,7 @@ target.addEventListener(start_event, function (ev) {
page_elements["menu_enc_human"].addEventListener("change", (ev) => {update_action("human", ev.target.value)});
page_elements["menu_enc_seagull"].addEventListener("change", (ev) => {update_action("seagull", ev.target.value)});
page_elements["btn_charsheet"].addEventListener("click", (ev) => {open_modal_dialog("charsheet")});
page_elements["btn_about"].addEventListener("click", (ev) => {open_modal_dialog("about")});
prepare_gamestate();

Binary file not shown.

16
pak/templates/about.j2 Normal file
View File

@@ -0,0 +1,16 @@
<div id="about-root">
<h1>Seagull Game</h1>
<p>© 2025 Nicole O'Connor.</p>
<p>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache.org</a>.
</p>
<p>
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
</p>
</div>

View File

@@ -0,0 +1,27 @@
<div id="charsheet-root">
<div id="charsheet-leftside">
<div id "attr-points">
Available Points: <span id="lbl-attr-points">0</span>
</div>
<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 class="attr" id="attr-leadership">
Leadership: <span id="lbl-attr-leadership">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>
<li><button id="btn-upgrade-leadership">Leadership Upgrades</button></li>
</ul></nav>
</div>
<div id="charsheet-upgrade-tree" class="mermaid">
</div>
</div>
</div>

View File

@@ -7,7 +7,11 @@
<link rel="stylesheet" href="{{ style }}">
{%- endfor -%}
{%- for script in scripts -%}
<script src="{{ script }}"></script>
{%- if script[1] -%}
<script type="module" src="{{ script[0] }}"></script>
{%- else -%}
<script src="{{ script[0] }}"></script>
{%- endif -%}
{%- endfor -%}
</head>
<body>
@@ -28,8 +32,8 @@
<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>
Shinies: <span id="lbl-seagull-shinies">420</span> <i>(+<span id="lbl-seagull-shinies-income">0</span>/day)</i><br />
Food: <span id="lbl-seagull-food">69</span> <i>(+<span id="lbl-seagull-food-income">0</span>/day)</i>
</p>
</div>
<hr />

24
pak/upgrades/agility.mmd Normal file
View File

@@ -0,0 +1,24 @@
---
title: Agility Upgrades
theme: base
themeVariables:
primaryColor: "#00aa00"
---
flowchart LR
speed1@{label: "Speedier Seagull"}
speed2@{label: "Greased Wings"}
theft_chance1@{label: "Swooping Techniques"}
theft_chance2@{label: "The Element of Surprise"}
passive_shinies_income1@{label: "Get On The Floor"}
passive_shinies_income2@{label: "Open The Door"}
passive_shinies_income3@{label: "Walk Like A Dinosaur"}
speed1-->speed2
theft_chance1-->speed1
theft_chance1-->theft_chance2
theft_chance1-->passive_shinies_income1
-->passive_shinies_income2
-->passive_shinies_income3

34
pak/upgrades/instinct.mmd Normal file
View File

@@ -0,0 +1,34 @@
---
title: Instinct Upgrades
theme: base
themeVariables:
primaryColor: "#aa00ff"
---
flowchart LR
xp_bonus1@{label: "Stop and Smell"};
xp_bonus2@{label: "Ponder and Deliberate"};
xp_bonus3@{label: "Plan and Strategize"};
theft_results1@{label: "Go For The Pockets"};
theft_results2@{label: "Use The Winds"};
passive_food_income1@{label: "Gone Fishin'"};
passive_food_income2@{label: "Gone Farmin'"};
passive_food_income3@{label: "Gone Clickin'"};
passive_food_income4@{label: "Gone Agin'"};
passive_food_income5@{label: "Gone Minin'"};
passive_food_income6@{label: "Gone Factorin'"};
passive_food_income7@{label: "Gone Bankin'"};
passive_food_income1-->passive_food_income2
-->passive_food_income3
-->passive_food_income4
-->passive_food_income5
-->passive_food_income6
-->passive_food_income7;
xp_bonus1-->xp_bonus2
-->xp_bonus3;
theft_results1-->theft_results2
-->xp_bonus3;

View File

@@ -0,0 +1,22 @@
---
title: Leadership Upgrades
theme: base
themeVariables:
primaryColor: "#ffaa00"
---
flowchart LR
offline_gen@{label: "Tireless Colony"}
offline_gen_bonus1@{label: "Swoop Where He's Unprepared"}
offline_gen_bonus2@{label: "Fly Where You're Unexpected"}
recruit_chance1@{label: "Squawk Softly"}
recruit_chance2@{label: "Wink"}
recruit_chance3@{label: "Sea Tzu's Art of Swoop"}
recruit_chance1-->recruit_chance2
recruit_chance2-->recruit_chance3
recruit_chance3-->offline_gen_bonus1
offline_gen---->offline_gen_bonus1
offline_gen_bonus1-->offline_gen_bonus2

269
res/doc/DoxygenLayout.xml Normal file
View File

@@ -0,0 +1,269 @@
<?xml version="1.0" encoding="UTF-8"?>
<doxygenlayout version="2.0">
<!-- Generated by doxygen 1.14.0 -->
<!-- Navigation index tabs for HTML output -->
<navindex>
<tab type="mainpage" visible="yes" title=""/>
<tab type="pages" visible="yes" title="" intro=""/>
<tab type="topics" visible="no" title="" intro=""/>
<tab type="modules" visible="yes" title="" intro="">
<tab type="modulelist" visible="yes" title="" intro=""/>
<tab type="modulemembers" visible="yes" title="" intro=""/>
</tab>
<tab type="namespaces" visible="yes" title="">
<tab type="namespacelist" visible="yes" title="" intro=""/>
<tab type="namespacemembers" visible="yes" title="" intro=""/>
</tab>
<tab type="concepts" visible="yes" title="">
</tab>
<tab type="interfaces" visible="yes" title="">
<tab type="interfacelist" visible="yes" title="" intro=""/>
<tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
<tab type="interfacehierarchy" visible="yes" title="" intro=""/>
</tab>
<tab type="classes" visible="yes" title="">
<tab type="classlist" visible="yes" title="" intro=""/>
<tab type="classindex" visible="$ALPHABETICAL_INDEX" title=""/>
<tab type="hierarchy" visible="yes" title="" intro=""/>
<tab type="classmembers" visible="yes" title="" intro=""/>
</tab>
<tab type="structs" visible="yes" title="">
<tab type="structlist" visible="yes" title="" intro=""/>
<tab type="structindex" visible="$ALPHABETICAL_INDEX" title=""/>
</tab>
<tab type="exceptions" visible="yes" title="">
<tab type="exceptionlist" visible="yes" title="" intro=""/>
<tab type="exceptionindex" visible="$ALPHABETICAL_INDEX" title=""/>
<tab type="exceptionhierarchy" visible="yes" title="" intro=""/>
</tab>
<tab type="files" visible="yes" title="">
<tab type="filelist" visible="yes" title="" intro=""/>
<tab type="globals" visible="yes" title="" intro=""/>
</tab>
<tab type="examples" visible="yes" title="" intro=""/>
</navindex>
<!-- Layout definition for a class page -->
<class>
<briefdescription visible="yes"/>
<detaileddescription visible="yes" title=""/>
<includes visible="$SHOW_HEADERFILE"/>
<inheritancegraph visible="yes"/>
<collaborationgraph visible="yes"/>
<memberdecl>
<nestedclasses visible="yes" title=""/>
<publictypes visible="yes" title=""/>
<services visible="yes" title=""/>
<interfaces visible="yes" title=""/>
<publicslots visible="yes" title=""/>
<signals visible="yes" title=""/>
<publicmethods visible="yes" title=""/>
<publicstaticmethods visible="yes" title=""/>
<publicattributes visible="yes" title=""/>
<publicstaticattributes visible="yes" title=""/>
<protectedtypes visible="yes" title=""/>
<protectedslots visible="yes" title=""/>
<protectedmethods visible="yes" title=""/>
<protectedstaticmethods visible="yes" title=""/>
<protectedattributes visible="yes" title=""/>
<protectedstaticattributes visible="yes" title=""/>
<packagetypes visible="yes" title=""/>
<packagemethods visible="yes" title=""/>
<packagestaticmethods visible="yes" title=""/>
<packageattributes visible="yes" title=""/>
<packagestaticattributes visible="yes" title=""/>
<properties visible="yes" title=""/>
<events visible="yes" title=""/>
<privatetypes visible="yes" title=""/>
<privateslots visible="yes" title=""/>
<privatemethods visible="yes" title=""/>
<privatestaticmethods visible="yes" title=""/>
<privateattributes visible="yes" title=""/>
<privatestaticattributes visible="yes" title=""/>
<friends visible="yes" title=""/>
<related visible="yes" title="" subtitle=""/>
<membergroups visible="yes"/>
</memberdecl>
<memberdef>
<inlineclasses visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<enums visible="yes" title=""/>
<services visible="yes" title=""/>
<interfaces visible="yes" title=""/>
<constructors visible="yes" title=""/>
<functions visible="yes" title=""/>
<related visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
<events visible="yes" title=""/>
</memberdef>
<allmemberslink visible="yes"/>
<usedfiles visible="$SHOW_USED_FILES"/>
<authorsection visible="yes"/>
</class>
<!-- Layout definition for a namespace page -->
<namespace>
<briefdescription visible="no"/>
<detaileddescription visible="yes" title=""/>
<memberdecl>
<nestednamespaces visible="yes" title=""/>
<constantgroups visible="yes" title=""/>
<interfaces visible="yes" title=""/>
<classes visible="yes" title=""/>
<concepts visible="yes" title=""/>
<structs visible="yes" title=""/>
<exceptions visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
<membergroups visible="yes"/>
</memberdecl>
<memberdef>
<inlineclasses visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
</memberdef>
<authorsection visible="yes"/>
</namespace>
<!-- Layout definition for a concept page -->
<concept>
<briefdescription visible="no"/>
<detaileddescription visible="yes" title=""/>
<includes visible="$SHOW_HEADERFILE"/>
<definition visible="yes" title=""/>
<authorsection visible="yes"/>
</concept>
<!-- Layout definition for a file page -->
<file>
<briefdescription visible="no"/>
<includes visible="$SHOW_INCLUDE_FILES"/>
<includegraph visible="yes"/>
<includedbygraph visible="yes"/>
<sourcelink visible="yes"/>
<memberdecl>
<interfaces visible="yes" title=""/>
<classes visible="yes" title=""/>
<structs visible="yes" title=""/>
<exceptions visible="yes" title=""/>
<namespaces visible="yes" title=""/>
<concepts visible="yes" title=""/>
<constantgroups visible="yes" title=""/>
<defines visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
<membergroups visible="yes"/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
<memberdef>
<inlineclasses visible="yes" title=""/>
<defines visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
</memberdef>
<authorsection/>
</file>
<!-- Layout definition for a group page -->
<group>
<briefdescription visible="no"/>
<detaileddescription visible="yes" title=""/>
<groupgraph visible="yes"/>
<memberdecl>
<nestedgroups visible="yes" title=""/>
<modules visible="yes" title=""/>
<dirs visible="yes" title=""/>
<files visible="yes" title=""/>
<namespaces visible="yes" title=""/>
<concepts visible="yes" title=""/>
<classes visible="yes" title=""/>
<defines visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<enumvalues visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<signals visible="yes" title=""/>
<publicslots visible="yes" title=""/>
<protectedslots visible="yes" title=""/>
<privateslots visible="yes" title=""/>
<events visible="yes" title=""/>
<properties visible="yes" title=""/>
<friends visible="yes" title=""/>
<membergroups visible="yes"/>
</memberdecl>
<memberdef>
<pagedocs/>
<inlineclasses visible="yes" title=""/>
<defines visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<enumvalues visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<signals visible="yes" title=""/>
<publicslots visible="yes" title=""/>
<protectedslots visible="yes" title=""/>
<privateslots visible="yes" title=""/>
<events visible="yes" title=""/>
<properties visible="yes" title=""/>
<friends visible="yes" title=""/>
</memberdef>
<authorsection visible="yes"/>
</group>
<!-- Layout definition for a C++20 module page -->
<module>
<briefdescription visible="no"/>
<detaileddescription visible="yes" title=""/>
<exportedmodules visible="yes"/>
<memberdecl>
<concepts visible="yes" title=""/>
<classes visible="yes" title=""/>
<enums visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<membergroups visible="yes" title=""/>
</memberdecl>
<memberdecl>
<files visible="yes"/>
</memberdecl>
</module>
<!-- Layout definition for a directory page -->
<directory>
<briefdescription visible="no"/>
<directorygraph visible="yes"/>
<memberdecl>
<dirs visible="yes"/>
<files visible="yes"/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
</directory>
</doxygenlayout>

18
res/doc/footer.html Normal file
View File

@@ -0,0 +1,18 @@
<!-- HTML footer for doxygen 1.14.0-->
<!-- start footer part -->
<!--BEGIN GENERATE_TREEVIEW-->
<div id="nav-path" class="navpath"><!-- id is needed for treeview function! -->
<ul>
$navpath
<li class="footer">$generatedby <a href="https://www.doxygen.org/index.html"><img class="footer" src="$relpath^doxygen.svg" width="104" height="31" alt="doxygen"/></a> $doxygenversion </li>
</ul>
</div>
<!--END GENERATE_TREEVIEW-->
<!--BEGIN !GENERATE_TREEVIEW-->
<hr class="footer"/><address class="footer"><small>
$generatedby&#160;<a href="https://www.doxygen.org/index.html"><img class="footer" src="$relpath^doxygen.svg" width="104" height="31" alt="doxygen"/></a> $doxygenversion
</small></address>
</div><!-- doc-content -->
<!--END !GENERATE_TREEVIEW-->
</body>
</html>

76
res/doc/header.html Normal file
View File

@@ -0,0 +1,76 @@
<!-- HTML header for doxygen 1.14.0-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="$langISO" class="light-mode">
<head>
<meta http-equiv="Content-Type" content="text/xhtml;charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=11"/>
<meta name="generator" content="Doxygen $doxygenversion"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<!--BEGIN PROJECT_NAME--><title>$projectname: $title</title><!--END PROJECT_NAME-->
<!--BEGIN !PROJECT_NAME--><title>$title</title><!--END !PROJECT_NAME-->
<!--BEGIN PROJECT_ICON-->
<link rel="icon" href="$relpath^$projecticon" type="image/x-icon" />
<!--END PROJECT_ICON-->
<link href="$relpath^tabs.css" rel="stylesheet" type="text/css"/>
<!--BEGIN FULL_SIDEBAR-->
<script type="text/javascript">var page_layout=1;</script>
<!--END FULL_SIDEBAR-->
<script type="text/javascript" src="$relpath^jquery.js"></script>
<script type="text/javascript" src="$relpath^dynsections.js"></script>
<!--BEGIN COPY_CLIPBOARD-->
<script type="text/javascript" src="$relpath^clipboard.js"></script>
<!--END COPY_CLIPBOARD-->
$treeview
$search
$mathjax
$darkmode
<link href="$relpath^$stylesheet" rel="stylesheet" type="text/css" />
$extrastylesheet
</head>
<body>
<!--BEGIN FULL_SIDEBAR-->
<div id="side-nav" class="ui-resizable side-nav-resizable"><!-- do not remove this div, it is closed by doxygen! -->
<!--END FULL_SIDEBAR-->
<div id="top"><!-- do not remove this div, it is closed by doxygen! -->
<!--BEGIN TITLEAREA-->
<div id="titlearea">
<table cellspacing="0" cellpadding="0">
<tbody>
<tr id="projectrow">
<!--BEGIN PROJECT_LOGO-->
<td id="projectlogo"><img alt="Logo" src="$relpath^$projectlogo"$logosize/></td>
<!--END PROJECT_LOGO-->
<!--BEGIN PROJECT_NAME-->
<td id="projectalign">
<div id="projectname">$projectname<!--BEGIN PROJECT_NUMBER--><span id="projectnumber">&#160;$projectnumber</span><!--END PROJECT_NUMBER-->
</div>
<!--BEGIN PROJECT_BRIEF--><div id="projectbrief">$projectbrief</div><!--END PROJECT_BRIEF-->
</td>
<!--END PROJECT_NAME-->
<!--BEGIN !PROJECT_NAME-->
<!--BEGIN PROJECT_BRIEF-->
<td>
<div id="projectbrief">$projectbrief</div>
</td>
<!--END PROJECT_BRIEF-->
<!--END !PROJECT_NAME-->
<!--BEGIN DISABLE_INDEX-->
<!--BEGIN SEARCHENGINE-->
<!--BEGIN !FULL_SIDEBAR-->
<td>$searchbox</td>
<!--END !FULL_SIDEBAR-->
<!--END SEARCHENGINE-->
<!--END DISABLE_INDEX-->
</tr>
<!--BEGIN SEARCHENGINE-->
<!--BEGIN FULL_SIDEBAR-->
<tr><td colspan="2">$searchbox</td></tr>
<!--END FULL_SIDEBAR-->
<!--END SEARCHENGINE-->
</tbody>
</table>
</div>
<!--END TITLEAREA-->
<!-- end header part -->

6
res/doc/index.md Normal file
View File

@@ -0,0 +1,6 @@
Seagull Game {#mainpage}
============
This is a game about a seagull.
The rest is up to you.

11
res/doc/play/index.md Normal file
View File

@@ -0,0 +1,11 @@
How to Play {#play}
===========
How to play the Seagull Game:
* Steal food and shinies.
* That's it.
### Subpages
* \subpage play-upgrades
* \subpage math

15
res/doc/play/math.md Normal file
View File

@@ -0,0 +1,15 @@
Mathematical Calculations Used In Game {#math}
======================================
For more information on how dice rolls are calculated, see actions.dice_roll()
## Calculated Attributes
Attribute | Formula
----------|--------
Speed | \f(\LARGE\(3+(AGL * 1.5)\)\f)
## Action Success Chances
Action | Chance Calculation
-------------------|-------------------
Stealing resources | \f(\LARGE\((roll+speed)+(\frac{AGL}{4}) \geq 50\)\f)
Recruiting | \f(\LARGE\(roll+(LDR*\frac{R_{shinies}}{Colony}) \geq 65\)\f)

4
res/doc/play/upgrades.md Normal file
View File

@@ -0,0 +1,4 @@
Upgrades {#play-upgrades}
========
You can buy upgrades in this game!

View File

@@ -0,0 +1,3 @@
\namespace actions
\brief Defines game logic functions for in-game actions.

2
res/doc/python/core.md Normal file
View File

@@ -0,0 +1,2 @@
\namespace core
\brief Bare minimum needed for a Seagull-game-like environment.

View File

@@ -0,0 +1,5 @@
\namespace desktop
\brief Implementation code for desktop specific needs.
This module also includes a rudimentary "local storage" system within JS_API, since pywebview's local storage APIs
seemed non-functional last time I tried to use them.

View File

@@ -0,0 +1,3 @@
\namespace gamedata
\internal
\brief Contains everything related to the virtual filesystem.

View File

@@ -0,0 +1,2 @@
This uses <a href="https://rant-lang.org">Rant</a>, a procedural generation language, along with a compiled in
wordlist module from a third party wordlist, to generate a unique and occasionally silly item description.

9
res/doc/python/items.md Normal file
View File

@@ -0,0 +1,9 @@
\namespace items
\brief Game logic related to items and resources.
This module handles generating of items, each worth a given amount of resources. The values are first calculated
according to game rules defined in XML files, then any given modifiers are applied.
\internal
\note This module is almost fully VFS aware.
\endinternal

2
res/doc/python/tick.md Normal file
View File

@@ -0,0 +1,2 @@
\namespace tick
\brief All logic related to game ticks.

View File

@@ -0,0 +1,2 @@
\namespace upgrades
\brief Upgrade tree related functions.

224
res/doc/style.css Normal file
View File

@@ -0,0 +1,224 @@
html {
--primary-color: #17b9c4;
--primary-dark-color: #337d80;
--primary-light-color: #70d9e9;
--on-primary-color: #ffffff;
/* page base colors */
--page-background-color: #ffffff;
--page-foreground-color: #2f4153;
--page-secondary-foreground-color: #6f7e8e;
/* color for all separators on the website: hr, borders, ... */
--separator-color: #dedede;
/* border radius for all rounded components. Will affect many components, like dropdowns, memitems, codeblocks, ... */
--border-radius-large: 8px;
--border-radius-small: 4px;
--border-radius-medium: 6px;
/* default spacings. Most components reference these values for spacing, to provide uniform spacing on the page. */
--spacing-small: 5px;
--spacing-medium: 10px;
--spacing-large: 16px;
/* default box shadow used for raising an element above the normal content. Used in dropdowns, search result, ... */
--box-shadow: 0 2px 8px 0 rgba(0,0,0,.075);
--odd-color: rgba(0,0,0,.028);
/* font-families. will affect all text on the website
* font-family: the normal font for text, headlines, menus
* font-family-monospace: used for preformatted text in memtitle, code, fragments
*/
--font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;
--font-family-monospace: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
/* font sizes */
--page-font-size: 15.6px;
--navigation-font-size: 14.4px;
--toc-font-size: 13.4px;
--code-font-size: 14px; /* affects code, fragment */
--title-font-size: 22px;
/* content text properties. These only affect the page content, not the navigation or any other ui elements */
--content-line-height: 27px;
/* The content is centered and constraint in it's width. To make the content fill the whole page, set the variable to auto.*/
--content-maxwidth: 1050px;
--table-line-height: 24px;
--toc-sticky-top: var(--spacing-medium);
--toc-width: 200px;
--toc-max-height: calc(100vh - 2 * var(--spacing-medium) - 85px);
/* colors for various content boxes: @warning, @note, @deprecated @bug */
--warning-color: #faf3d8;
--warning-color-dark: #f3a600;
--warning-color-darker: #5f4204;
--note-color: #e4f3ff;
--note-color-dark: #1879C4;
--note-color-darker: #274a5c;
--todo-color: #e4dafd;
--todo-color-dark: #5b2bdd;
--todo-color-darker: #2a0d72;
--deprecated-color: #ecf0f3;
--deprecated-color-dark: #5b6269;
--deprecated-color-darker: #43454a;
--bug-color: #f8d1cc;
--bug-color-dark: #b61825;
--bug-color-darker: #75070f;
--invariant-color: #d8f1e3;
--invariant-color-dark: #44b86f;
--invariant-color-darker: #265532;
/* blockquote colors */
--blockquote-background: #f8f9fa;
--blockquote-foreground: #636568;
/* table colors */
--tablehead-background: #f1f1f1;
--tablehead-foreground: var(--page-foreground-color);
/* menu-display: block | none
* Visibility of the top navigation on screens >= 768px. On smaller screen the menu is always visible.
* `GENERATE_TREEVIEW` MUST be enabled!
*/
--menu-display: block;
--menu-focus-foreground: var(--on-primary-color);
--menu-focus-background: var(--primary-color);
--menu-selected-background: rgba(0,0,0,.05);
--header-background: var(--page-background-color);
--header-foreground: var(--page-foreground-color);
/* searchbar colors */
--searchbar-background: var(--side-nav-background);
--searchbar-foreground: var(--page-foreground-color);
/* searchbar size
* (`searchbar-width` is only applied on screens >= 768px.
* on smaller screens the searchbar will always fill the entire screen width) */
--searchbar-height: 33px;
--searchbar-width: 210px;
--searchbar-border-radius: var(--searchbar-height);
/* code block colors */
--code-background: #f5f5f5;
--code-foreground: var(--page-foreground-color);
/* fragment colors */
--fragment-background: #F8F9FA;
--fragment-foreground: #37474F;
--fragment-keyword: #bb6bb2;
--fragment-keywordtype: #8258b3;
--fragment-keywordflow: #d67c3b;
--fragment-token: #438a59;
--fragment-comment: #969696;
--fragment-link: #5383d6;
--fragment-preprocessor: #46aaa5;
--fragment-linenumber-color: #797979;
--fragment-linenumber-background: #f4f4f5;
--fragment-linenumber-border: #e3e5e7;
--fragment-lineheight: 20px;
/* sidebar navigation (treeview) colors */
--side-nav-background: #fbfbfb;
--side-nav-foreground: var(--page-foreground-color);
--side-nav-arrow-opacity: 0;
--side-nav-arrow-hover-opacity: 0.9;
--toc-background: var(--side-nav-background);
--toc-foreground: var(--side-nav-foreground);
/* height of an item in any tree / collapsible table */
--tree-item-height: 30px;
--memname-font-size: var(--code-font-size);
--memtitle-font-size: 18px;
--webkit-scrollbar-size: 7px;
--webkit-scrollbar-padding: 4px;
--webkit-scrollbar-color: var(--separator-color);
--animation-duration: .12s
}
span.api-method {
padding: 3px;
font-weight: bold;
border-radius: 5px;
}
span.api-method-GET {
background-color: #c4eecf;
color: #10491f;
border: 1px #48d96f dashed;
}
span.api-method-POST {
background-color: #eee3c4;
color: #493e10;
border: 1px #d9bc48 dashed;
}
@media (prefers-color-scheme: dark) {
html:not(.light-mode) {
color-scheme: dark;
--primary-color: #19c9d2;
--primary-dark-color: #86c0c4;
--primary-light-color: #478eac;
--box-shadow: 0 2px 8px 0 rgba(0,0,0,.35);
--odd-color: rgba(100,100,100,.06);
--menu-selected-background: rgba(0,0,0,.4);
--page-background-color: #1C1D1F;
--page-foreground-color: #d2dbde;
--page-secondary-foreground-color: #859399;
--separator-color: #38393b;
--side-nav-background: #252628;
--code-background: #2a2c2f;
--tablehead-background: #2a2c2f;
--blockquote-background: #222325;
--blockquote-foreground: #7e8c92;
--warning-color: #3b2e04;
--warning-color-dark: #f1b602;
--warning-color-darker: #ceb670;
--note-color: #163750;
--note-color-dark: #1982D2;
--note-color-darker: #dcf0fa;
--todo-color: #2a2536;
--todo-color-dark: #7661b3;
--todo-color-darker: #ae9ed6;
--deprecated-color: #2e323b;
--deprecated-color-dark: #738396;
--deprecated-color-darker: #abb0bd;
--bug-color: #2e1917;
--bug-color-dark: #ad2617;
--bug-color-darker: #f5b1aa;
--invariant-color: #303a35;
--invariant-color-dark: #76ce96;
--invariant-color-darker: #cceed5;
--fragment-background: #282c34;
--fragment-foreground: #dbe4eb;
--fragment-keyword: #cc99cd;
--fragment-keywordtype: #ab99cd;
--fragment-keywordflow: #e08000;
--fragment-token: #7ec699;
--fragment-comment: #999999;
--fragment-link: #98c0e3;
--fragment-preprocessor: #65cabe;
--fragment-linenumber-color: #cccccc;
--fragment-linenumber-background: #35393c;
--fragment-linenumber-border: #1f1f1f;
}
}

View File

@@ -2,12 +2,12 @@
a = Analysis(
['app/desktop.py'],
['@CMAKE_SOURCE_DIR@/app/desktop.py'],
pathex=[],
binaries=[],
datas=[
('app/rant', 'basepak/rant'),
('app/rules/schemas', 'basepak/rules/schemas'),
('@CMAKE_SOURCE_DIR@/app/basepak', 'basepak'),
('@CMAKE_CURRENT_BINARY_DIR@/basepak', 'basepak'),
('opt', './opt')
],
hiddenimports=[],
@@ -20,8 +20,14 @@ a = Analysis(
)
pyz = PYZ(a.pure)
splash = Splash("@CMAKE_SOURCE_DIR@/pak/static/image/splash.png",
binaries=a.binaries, datas=a.datas,
text_pos=(0,0), text_size=12, text_color="white")
exe = EXE(
pyz,
splash,
splash.binaries,
a.scripts,
a.binaries,
a.datas,