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

29
pak/rant/flavor.rant Normal file
View File

@@ -0,0 +1,29 @@
@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.
}

55
pak/rant/food/humans.rant Normal file
View File

@@ -0,0 +1,55 @@
@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>]
}
}

View File

@@ -0,0 +1,55 @@
@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>]
}
}

View File

@@ -0,0 +1,4 @@
{
a watch |
a bracelet
}

View File

@@ -0,0 +1,4 @@
{
a watch |
a bracelet
}

View File

@@ -0,0 +1,23 @@
<?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>

View File

@@ -0,0 +1,23 @@
<?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>

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>

232
pak/static/css/seagull.css Normal file
View File

@@ -0,0 +1,232 @@
html, body { height: 100% }
/** MAIN GAME **/
div#root {
display: flex;
width: 100%;
height: 100%;
z-index: 0;
font-family: sans-serif;
}
div#main-sidebar {
display: flex;
flex-direction: column;
max-width: 265px;
/*padding-left: 5px;*/
padding-right: 5px;
border-right: 0.125em solid rgb(192, 192, 192);
}
div#side-seagull-name {
text-align: center;
}
div#side-seagull-name-editor {
display: none;
}
div#main-content {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding-left: 5px;
}
div#main-header {
display: flex;
flex-direction: row;
min-height: 100px;
vertical-align: middle;
border-bottom: 0.125em solid rgb(192,192,192);
}
div#main-day-stats {
width: 100%;
margin-top: auto;
margin-bottom: auto;
vertical-align: middle;
font-size: large;
}
div#main-button-bar {
display: flex;
flex-direction: row;
min-height: 125px;
vertical-align: middle;
}
div#main-log {
display: flex;
flex-direction: column-reverse;
}
div.log-line {
display: flex;
flex-direction: row;
vertical-align: top;
width: 100%;
min-height: 1.5em;
padding-top: 5px;
}
div.log-line-alt {
display: flex;
flex-direction: row;
vertical-align: top;
width: 100%;
min-height: 1.5em;
padding-top: 5px;
background-color: rgb(192, 192, 192);
}
div.log-tick {
font-size: 0.75em;
margin-right: 0.2em;
}
div.log-msg {
margin-left: 0.2em;
}
div#charsheet {
display: flex;
flex-direction: row;
width: 100%;
background-color: rgb(240, 240, 240);
}
/** MODAL **/
div#modal-background {
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: 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;
}

Binary file not shown.

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

@@ -0,0 +1,38 @@
globalThis.desktop_mode = true;
async function prepare_gamestate() {
var gamestate_loaded = null;
try {
gamestate_loaded = await window.pywebview.api.load_data("gamestate");
} catch (exc) {
console.error("no gamestate");
gamestate_loaded = null;
}
if (gamestate_loaded == null) {
record_log("Welcome to Seagull Game! We haven't found a save in your app data, so we're starting a new game!");
gamestate = structuredClone(gamestate_default);
}
else {
console.log(gamestate_loaded);
gamestate = JSON.parse(gamestate_loaded);
record_log("Welcome back! Game loaded.")
}
tick_meter_running = true;
if (window.pywebview.api.debug_mode) {
dev_toolbox(true);
}
}
function save_game() {
window.pywebview.api.save_data("gamestate", JSON.stringify(gamestate));
record_log("Game saved.");
}
function reset_game() {
tick_meter_running = false;
window.pywebview.api.delete_data("gamestate");
window.location.reload();
}

View File

@@ -0,0 +1,27 @@
globalThis.desktop_mode = false;
function load_gamestate() {
var gamestate_loaded = window.localStorage.getItem("gamestate");
if (gamestate_loaded == null) {
record_log("Welcome to Seagull Game! We haven't found a save in your browser storage, so we're starting a new game!");
gamestate = structuredClone(gamestate_default);
}
else {
gamestate = JSON.parse(gamestate_loaded);
record_log("Welcome back! Game loaded.")
}
}
function save_game() {
window.localStorage.setItem("gamestate", JSON.stringify(gamestate));
record_log("Game saved.");
}
var tick_meter_running = true;
function reset_game() {
tick_meter_running = false;
window.localStorage.removeItem("gamestate");
window.location.reload();
}

564
pak/static/js/seagull.js Normal file
View File

@@ -0,0 +1,564 @@
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 = {};
globalThis.gamestate = {};
globalThis.tick_meter_running = false;
var ticks_since_last_save = 0;
globalThis.gamestate_default = {
"statever": "1",
"tick": 1,
"name": "Nameless",
"class": "Seaglet",
"level": 1,
"shinies": 0,
"colony": 1,
"food": 0,
"autosave": 35,
"story_beat": 0,
"xp": 0,
"xp_next": 50,
"enc_human": "pause",
"enc_seagull": "pause",
"agility": 0,
"instinct": 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
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"; }
bool_log_alt = !bool_log_alt;
const div_logtick = document.createElement("div");
div_logtick.className = "log-tick"
div_logtick.innerHTML = "Day " + gamestate["tick"];
div_logrow.append(div_logtick);
const div_logmsg = document.createElement("div");
div_logmsg.innerHTML = text;
div_logmsg.className = "log-msg";
div_logrow.append(div_logmsg);
page_elements["div_log"].append(div_logrow);
}
function record_log_with_choices() {
const div_logrow = document.createElement("div");
if (bool_log_alt) { div_logrow.className = "log-line"; }
else { div_logrow.className = "log-line-alt"; }
bool_log_alt = !bool_log_alt;
const div_logtick = document.createElement("div");
div_logtick.className = "log-tick"
div_logtick.innerHTML = "Day " + gamestate["tick"];
div_logrow.append(div_logtick);
const div_logdata = document.createElement("div");
const div_logmsg = document.createElement("div");
div_logmsg.innerHTML = arguments[0];
div_logmsg.className = "log-msg";
div_logdata.append(div_logmsg);
const div_logactions = document.createElement("div");
div_logactions.className = "log-button-row";
for (var i = 1; i < arguments.length; i += 2) {
console.log(i)
var label = arguments[i];
var callback = arguments[i+1];
var btn_action = document.createElement("button");
btn_action.innerHTML = label;
btn_action.className = "log-action-button";
btn_action.setAttribute("onclick", callback + "; tick_meter_running = true;");
div_logactions.append(btn_action);
}
div_logdata.append(div_logactions);
div_logrow.append(div_logdata);
page_elements["div_log"].append(div_logrow);
tick_meter_running = 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) {
if (!modal_dialog_open) {
tick_meter_running = false;
modal_dialog_open = true;
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);
}
if (dialog_queue.length == 0) {
modal_background.style.zIndex = "-10";
modal_background.style.visibility = "hidden";
document.body.removeChild(modal_background);
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() {
page_elements["lbl_name"].innerHTML = gamestate["name"];
page_elements["lbl_tick"].innerHTML = gamestate["tick"];
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"];
page_elements["lbl_level"].innerHTML = gamestate["level"];
page_elements["menu_enc_human"].value = gamestate["enc_human"];
page_elements["menu_enc_seagull"].value = gamestate["enc_seagull"];
}
var dev_toolbox_open = false;
function dev_toolbox(open) {
if (open != dev_toolbox_open) {
if (open) {
var div_toolbox = document.createElement("div");
page_elements["div_toolbox"] = div_toolbox;
div_toolbox.setAttribute("id", "dev_toolbox");
fetch("/dev/get-toolbox")
.then((response) => response.text())
.then((resp) => {div_toolbox.innerHTML = resp})
page_elements["div_sidebar"].appendChild(div_toolbox);
}
else {
var div_toolbox = page_elements["div_toolbox"];
page_elements["div_sidebar"].removeChild(div_toolbox);
div_toolbox.remove();
delete page_elements["div_toolbox"];
}
}
dev_toolbox_open = open;
}
function reward_xp(amount) {
gamestate["xp"] += amount;
if (gamestate["xp"] >= 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;
record_log("The humans have fired off some sort of large rocket from a nearby platform. You watch it as it pierces the sky above you and fades into the heavens.");
} else if (gamestate["level"] == 3) {
gamestate["story_beat"] = 2;
gamestate["class"] = "Seagull";
record_log("You have grown up from a young, eager seaglet to a full blown Seagull. As your colony participates in the ritual honoring your coming of age, you begin to detect a shift in the winds, though you're not certain exactly how.");
}
}
}
globalThis.steal_resource = async function (resource, target, amount, itemstr) {
var items = itemstr.split(",")
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}`); }
}
globalThis.recruit = async function (amount) {
if (gamestate["shinies"] < amount) {
record_log("You do not have enough shinies to recruit this seagull.");
return;
}
var stealdata = await fetch("/act/recruit", {method: "POST", body: JSON.stringify({gamestate: gamestate})})
.then(res => { return res.json(); })
.catch(e => { throw e; });
if (stealdata["success"] && amount > 0) {
gamestate["shinies"] -= amount;
reward_xp(5);
gamestate["colony"] += 1;
record_log("Successfully recruited a seagull into the colony");
}
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 => {
var json = res.json()
console.log(json)
return json
})
.catch(e => { throw e; });
console.log(JSON.stringify(tickdata));
if (tickdata["code"] != 200) {
console.error("Non-200 tick code: " + tickdata["code"]);
return;
}
if (tickdata["event_type"] == 0) {
// pass
} else if (tickdata["event_type"] == 1) {
// Flavor event - no gameplay effect, but occasionally says something fun.
record_log(tickdata["log"]);
} else if (tickdata["event_type"] == 10) { // ENCHUMAN
var total_food = 0;
var food_descs = [];
var total_shinies = 0;
var shinies_descs = [];
switch (page_elements["menu_enc_human"].value) {
case "pause":
tickdata.items.food.forEach((item) => {
total_food += item["amount"];
food_descs.push(item["desc"]);
});
tickdata.items.shinies.forEach((item) => {
total_shinies += item["amount"];
shinies_descs.push(item["desc"]);
});
var logstring = "You have encountered a human. It is carrying these resources:\n\n"
logstring += "<ol>\n"
if (total_food > 0) {
logstring += `<li><b>${total_food.toFixed(2)} food:</b> ${food_descs.join(", ")}</li>\n`;
}
if (total_shinies > 0) {
logstring += `<li><b>${total_shinies.toFixed(2)} shinies:</b> ${shinies_descs.join(", ")}</li>\n`;
}
logstring += "</ol>\nWhat would you like to do?";
record_log_with_choices(logstring,
"Steal food", `steal_resource('food', 'humans', ${total_food}, "${food_descs.toString()}")`,
"Steal shinies", `steal_resource('shinies', 'humans', ${total_shinies}, "${shinies_descs.toString()}")`
)
break;
case "steal-food":
record_log("You have encountered a human. Attempting to steal food.");
tickdata.items.food.forEach((item) => {
total_food += item["amount"];
food_descs.push(item["desc"]);
})
steal_resource("food", "humans", total_food, food_descs.toString());
break;
case "steal-shinies":
record_log("You have encountered a human. Attempting to steal shinies.");
tickdata.items.shinies.forEach((item) => {
total_shinies += item["amount"];
shinies_descs.push(item["desc"]);
})
steal_resource("shinies", "humans", total_shinies, shinies_descs.toString());
break;
default:
console.error("undefined action " + page_elements["menu_enc_human"]);
break;
}
} else if (tickdata["event_type"] == 11) { // ENCGULL
var total_food = 0;
var food_descs = [];
var total_shinies = 0;
var shinies_descs = [];
switch (page_elements["menu_enc_seagull"].value) {
case "pause":
tickdata.items.food.forEach((item) => {
total_food += item["amount"];
food_descs.push(item["desc"]);
});
tickdata.items.shinies.forEach((item) => {
total_shinies += item["amount"];
shinies_descs.push(item["desc"]);
});
var logstring = "You have encountered a seagull. It is carrying these resources:\n\n"
logstring += "<ol>\n"
if (total_food > 0) {
logstring += `<li><b>${total_food.toFixed(2)} food:</b> ${food_descs.join(", ")}</li>\n`;
}
if (total_shinies > 0) {
logstring += `<li><b>${total_shinies.toFixed(2)} shinies:</b> ${shinies_descs.join(", ")}</li>\n`;
}
logstring += "</ol>\nWhat would you like to do?";
record_log_with_choices(logstring,
"Recruit", `recruit(${tickdata.recruit_cost})`,
"Steal food", `steal_resource('food', 'seagulls', ${total_food}, "${food_descs.toString()}")`,
"Steal shinies", `steal_resource('shinies', 'seagulls', ${total_shinies}, "${shinies_descs.toString()}")`
)
break;
case "recruit":
recruit(tickdata.recruit_cost);
break
case "steal-food":
record_log("You have encountered a seagull. Attempting to steal food.");
tickdata.items.food.forEach((item) => {
total_food += item["amount"];
food_descs.push(item["desc"]);
})
steal_resource("food", "seagulls", total_food, food_descs.toString());
break;
case "steal-shinies":
record_log("You have encountered a seagull. Attempting to steal shinies.");
tickdata.items.shinies.forEach((item) => {
total_shinies += item["amount"];
shinies_descs.push(item["desc"]);
})
steal_resource("shinies", "seagulls", total_shinies, shinies_descs.toString());
break;
default:
console.error("undefined action " + page_elements["menu_enc_human"]);
break;
}
}
// sanity check
if (!("autosave" in gamestate)) {
gamestate["autosave"] = 35;
}
if (ticks_since_last_save % gamestate["autosave"] == 0 && ticks_since_last_save != 0) {
save_game();
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();
}
var start_event = "";
var target = null;
if (desktop_mode) {
// pywebview's native JS is nerfed in a few places and needs the additional python API
// which gets loaded after initial DOM via injections
start_event = "pywebviewready";
target = window;
}
else {
// in web mode, browsers are expected to have working local storage by this point
start_event = "DOMContentLoaded";
target = document;
}
function update_action(enc, value) {
gamestate[`enc_${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");
page_elements["div_name_editor"] = document.querySelector("#side-seagull-name-editor");
page_elements["lbl_name"] = document.querySelector("#lbl-seagull-name");
page_elements["lbl_class"] = document.querySelector("#lbl-seagull-class");
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");
page_elements["lbl_xp_next"] = document.querySelector("#lbl-seagull-xp-next");
page_elements["lbl_level"] = document.querySelector("#lbl-seagull-lvl");
page_elements["menu_enc_human"] = document.querySelector("#menu-enc-human");
page_elements["menu_enc_seagull"] = document.querySelector("#menu-enc-seagull");
page_elements["btn_charsheet"] = document.querySelector("#button-charsheet");
page_elements["btn_settings"] = document.querySelector("#button-settings");
page_elements["btn_about"] = document.querySelector("#button-about");
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();
record_log("seagull game ver. " + ver_string);
const interval = setInterval(() => {
if (tick_meter_running) { game_tick(); }
}, 1200);
update_ui();
});
function change_seagull_name() {
page_elements["div_name"].style.display = "none";
page_elements["div_name_editor"].style.display = "block";
}
function confirm_seagull_name() {
const new_name = page_elements["edt_name"].value;
page_elements["lbl_name"].innerHTML = new_name;
gamestate["name"] = new_name;
save_game();
page_elements["div_name"].style.display = "block";
page_elements["div_name_editor"].style.display = "none";
}
function cancel_seagull_name() {
page_elements["edt_name"].value = "";
page_elements["div_name"].style.display = "block";
page_elements["div_name_editor"].style.display = "none";
}

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

@@ -0,0 +1,2 @@
{% if not desktop %}IP: {{ipaddr}}<br />{% endif %}
<button id="dev-reset" onClick="reset_game()">Reset Game</button>

View File

@@ -0,0 +1,69 @@
<!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 -%}
{%- if script[1] -%}
<script type="module" src="{{ script[0] }}"></script>
{%- else -%}
<script src="{{ script[0] }}"></script>
{%- endif -%}
{%- 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> <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 />
<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>

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