Merge branch 'pet'

This commit is contained in:
zyl 2024-06-25 00:58:24 -07:00
commit 849e6e8f97
No known key found for this signature in database
8 changed files with 708 additions and 3 deletions

3
.cargo/config.toml Normal file
View file

@ -0,0 +1,3 @@
[target.x86_64-pc-windows-msvc]
linker = "ld.lld.exe"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]

View file

@ -1,7 +1,7 @@
base_url: "https://zyl.gay"
title: zyl is gay
description: "zyl's website."
sass_styles: [index.scss]
sass_styles: [index.scss, "pet.scss"]
images_per_page: 10
blog_posts_per_page: 20
cdn_url: "https://i.zyl.gay"

71
site/pages/pet.md Normal file
View file

@ -0,0 +1,71 @@
---
title: pet
scripts: ["js/pet.js"]
styles: ["pet.css"]
---
<div id="pet">
<noscript><p>javascript is required for the pet game!!</p></noscript>
<div id="pet-display">
<h2 class="pet-name"></h2>
<div class="the-pet"></div>
<div class="status">
<p name="hungry" class="hidden"><span class="pet-name"></span> looks hungry..</p>
<p name="starving" class="hidden"><span class="pet-name"></span> is starving!! you need to feed them!!</p>
<p name="unhappy" class="hidden"><span class="pet-name"></span> looks at you with wide eyes..</p>
<p name="messy-1" class="hidden"><span class="pet-name"></span> has left a bit of a mess! time to clean!</p>
<p name="messy-2" class="hidden">there's even more mess in here.. shouldn't you clean it for <span class="pet-name"></span>?</p>
<p name="messy-3" class="hidden">what a mess!! <span class="pet-name"></span> can't be happy.. you've gotta clean in here</p>
</div>
</div>
<div id="egg">
<p>whoa! you just found a weird egg! maybe you should watch it and see what happens..</p>
</div>
<div id="adult-info" class="hidden">
<p><span class="pet-name"></span> has grown up to be an adult!! what will they do with their life now....</p>
<button class="advance">okay!</button>
</div>
<div id="elder-info" class="hidden">
<p>oh? <span class="pet-name"></span> has aged and is now an elder creature! they may not have much left in their life.... hopefully it's been a good life!</p>
<button class="advance">hopefully!!</button>
</div>
<div id="passed-away-info" class="hidden">
<p>oh... <span class="pet-name"></span> has finally gone and kicked the bucket. its story comes to an end....</p>
<button>but what's this egg lying here about..?</button>
</div>
<form id="pet-setup" class="hidden">
<p>whoa! your egg just hatched into a new creature! what should you name it?</p>
<input type="text" name="pet-name" min-length="3" max-length="50">
<button type="submit">name it!</button>
</form>
<div id="pet-actions">
<div name="hatched-actions" class="hidden">
<button name="feed">feed</button>
<button name="pet">pet</button>
<button name="clean">clean</button>
</div>
<button name="pause">pause</button>
</div>
<div id="debug-section" class="hidden">
<button id="force-update">force update</button> <button id="reset">reset</button>
<p>LS: <span name="ls"></span> A: <span name="a"></span> F: <span name="f"></span> B: <span name="b"></span> P: <span name="p"></span> MC: <span name="mc"></span> H: <span name="h"></span></p>
</div>
</div>
<details>
<summary>tips!!</summary>
<ul>
<li>pets need to be fed about once every eight hours!</li>
<li>the game (currently) doesn't simulate while the page is unloaded, so make sure to keep the page loaded for your pet to exist!</li>
<li>make sure to keep your pet clean!!</li>
<li>if your pet is turning grey, make sure you're giving them the attention they need!! pet's deserve happiness too :(</li>
<li>if you take good enough care of your pet they'll stop going potty on the floor!</li>
</ul>
</details>

547
site/root/js/pet.js Normal file
View file

@ -0,0 +1,547 @@
(function () {
"use strict";
const UPDATES_PER_MINUTE = 2;
const UPDATES_PER_HOUR = UPDATES_PER_MINUTE * 60;
const UPDATES_PER_DAY = UPDATES_PER_HOUR * 24;
/** the current pet version.. */
const CURRENT_PET_VERSION = 1;
/** the max food a pet will eat */
const MAX_FOOD = 100;
/** the amount of time it takes for a pet to have to GO */
const POTTY_TIME = 100;
/** how fast a pet's food value decays */
const FOOD_DECAY = MAX_FOOD / (UPDATES_PER_HOUR * 8); // to stay on top should be fed roughly once every 8 hours?
/** the rate at which a pet ages */
const AGING_RATE = 1;
/** how fast a pet's potty need decays */
const POTTY_DECAY = FOOD_DECAY / 2; // roughly every 4 hours?
/** how much mess can be in a pet's space at once */
const MAX_MESS = 5;
/** how fast a pet's happiness decays */
const HAPPINESS_DECAY = FOOD_DECAY;
/** a pet's maximum happiness */
const MAX_HAPPINESS = 100;
/** how quickly a pet's happiness will be reduced by when hungry */
const HAPPINESS_EMPTY_STOMACH_MODIFIER = -10 / UPDATES_PER_HOUR;
/** how quickly a pet's happiness will be reduced by when their space is messy, per piece of mess */
const HAPPINESS_MESS_MODIFIER = -5 / UPDATES_PER_HOUR;
/** the amount of happiness gained when the pet is fed (excluding when the pet doesn't yet need food) */
const FEED_HAPPINESS = 5;
/** the amount of happiness gained when the pet is pet */
const PET_HAPPINESS = 20;
/** the amount of happiness gained when the pet's space is cleaned */
const CLEAN_HAPPINESS = 1;
/** the minimum amount of time between feedings */
const FEED_TIMER = 5000;
/** the minimum amount of time between pets */
const PET_TIMER = 2000;
/** the minimum amount of time between cleans */
const CLEAN_TIMER = 5000;
const PET_SAVE_KEY = "pet-game";
/** life stage for an egg */
const LIFE_STAGE_EGG = 1;
/** life stage for a pup */
const LIFE_STAGE_PUP = 2;
/** life stage for an adult */
const LIFE_STAGE_ADULT = 3;
/** life stage for an elder pet */
const LIFE_STAGE_ELDER = 4;
/** the time it takes for a pet to grow past the egg phase */
const EGG_TIME = UPDATES_PER_MINUTE;
/** the time it takes for a pet to grow past the pup phase */
const PUP_TIME = UPDATES_PER_DAY * 7;
/** the time it takes for a pet to grow past the adult phase */
const ADULT_TIME = UPDATES_PER_DAY * 15;
/** the time it takes for a pet to grow past the elder phase */
const ELDER_TIME = UPDATES_PER_DAY * 7;
const WIDTH_PUP = 150;
const HEIGHT_PUP = 150;
const WIDTH_ADULT = 250;
const HEIGHT_ADULT = 250;
const WIDTH_ELDER = 210;
const HEIGHT_ELDER = 210;
/** the different types of pets available */
const PET_TYPES = ["circle", "square", "triangle"];
const petDisplay = document.querySelector("#pet-display");
const thePet = petDisplay.querySelector(".the-pet");
const status = petDisplay.querySelector(".status");
const statusHungry = status.querySelector("[name=hungry]");
const statusStarving = status.querySelector("[name=starving]");
const statusUnhappy = status.querySelector("[name=unhappy]");
const statusMessy1 = status.querySelector("[name=messy-1]");
const statusMessy2 = status.querySelector("[name=messy-2]");
const statusMessy3 = status.querySelector("[name=messy-3]");
const petName = document.querySelectorAll(".pet-name");
const eggDiv = document.querySelector("div#egg");
const petSetup = document.querySelector("#pet-setup");
const adultInfo = document.querySelector("div#adult-info");
const elderInfo = document.querySelector("div#elder-info");
const passedAwayInfo = document.querySelector("div#passed-away-info");
const name = petSetup.querySelector("input[name=pet-name]");
const petActions = document.querySelector("div#pet-actions");
const pauseButton = petActions.querySelector("button[name=pause]");
const hatchedActions = petActions.querySelector("div[name=hatched-actions]");
const feedButton = hatchedActions.querySelector("button[name=feed]");
const petButton = hatchedActions.querySelector("button[name=pet]");
const cleanButton = hatchedActions.querySelector("button[name=clean]");
const debug = document.querySelector("div#debug-section");
const debugLifeStage = debug.querySelector("span[name=ls]");
const debugAge = debug.querySelector("span[name=a]");
const debugFood = debug.querySelector("span[name=f]");
const debugBehavior = debug.querySelector("span[name=b]");
const debugPotty = debug.querySelector("span[name=p]");
const debugMessCounter = debug.querySelector("span[name=mc]");
const debugHappiness = debug.querySelector("span[name=h]");
const forceUpdateButton = debug.querySelector("button#force-update");
const resetButton = debug.querySelector("button#reset");
let canFeed = true;
let canPet = true;
let canClean = true;
/**
* generates a random number within the given range
* @param {number} min the minimum number for the random generation
* @param {number} max the maximum number for the random generation
* @returns the generated number
*/
function rand(min, max) {
return Math.random() * (max - min) + min;
}
/**
* class containing information about a pet
*/
class Pet {
/** current pet version */
version = CURRENT_PET_VERSION;
/** whether the pet can die or not */
canDie = false;
/** whether the pet is alive or dead */
alive = true;
/** whether the pet simulation is paused */
paused = false;
/** whether the pet simulation needs an interactive advancement */
needsAdvancement = false;
/** the pet's current life stage */
lifeStage = LIFE_STAGE_EGG;
/** the pet's name */
name = "";
/** how much food the pet has stored */
food = MAX_FOOD;
/** the pet's age */
age = 0;
/** the pet's behavior score */
behavior = 0;
/** how long until the pet needs to go potty */
pottyTimer = POTTY_TIME;
/** how much mess the pet has made */
messCounter = 0;
/** the pet's current happiness */
_happiness = MAX_HAPPINESS;
/** the time the pet was last updated */
lastUpdate = Date.now();
/** the time the egg was found */
eggFound = Date.now();
/** the time the egg hatched */
hatched = Date.now();
/** the pet's type */
type = PET_TYPES[Math.floor(rand(0, PET_TYPES.length))];
/** the pet's color */
color = `rgb(${rand(0, 255)}, ${rand(0, 255)}, ${rand(0, 255)})`;
/** the pet's scaled width */
scaleWidth = rand(0.6, 1.4);
/** the pet's scaled height */
scaleHeight = rand(0.6, 1.4);
/**
* updates a pet
*/
update() {
if (!this.alive || this.paused || this.needsAdvancement) {
return;
}
console.log("update");
this.lastUpdate = Date.now();
this.age += AGING_RATE;
if (this.lifeStage !== LIFE_STAGE_EGG) {
this.food -= FOOD_DECAY;
this.pottyTimer -= POTTY_DECAY;
this.happiness -= HAPPINESS_DECAY;
if (this.food < 0) {
this.happiness += HAPPINESS_EMPTY_STOMACH_MODIFIER;
this.food = 0;
if (this.canDie) {
// TODO: pet dies
}
}
if (this.pottyTimer < 0) {
this.goPotty();
}
for (let i = 0; i < this.messCounter; i++) {
this.happiness += HAPPINESS_MESS_MODIFIER;
}
}
if (this.lifeStage === LIFE_STAGE_EGG && this.age >= EGG_TIME) {
this.needsAdvancement = true;
this.lifeStage = LIFE_STAGE_PUP;
this.age = 0;
} else if (this.lifeStage === LIFE_STAGE_PUP && this.age >= PUP_TIME) {
this.needsAdvancement = true;
this.lifeStage = LIFE_STAGE_ADULT;
this.age = 0;
} else if (
this.lifeStage === LIFE_STAGE_ADULT &&
this.age >= ADULT_TIME
) {
this.needsAdvancement = true;
this.lifeStage = LIFE_STAGE_ELDER;
this.age = 0;
} else if (
this.lifeStage === LIFE_STAGE_ELDER &&
this.age >= ELDER_TIME
) {
this.needsAdvancement = true;
this.alive = false;
// TODO: DEATH
}
this.updateDom();
}
/**
* updates the html dom
*/
updateDom() {
eggDiv.classList.add("hidden");
petSetup.classList.add("hidden");
hatchedActions.classList.remove("hidden");
thePet.classList.remove("egg");
thePet.classList.remove("pup");
thePet.classList.remove("adult");
thePet.classList.remove("elder");
thePet.classList.remove("dead");
statusHungry.classList.add("hidden");
statusStarving.classList.add("hidden");
statusUnhappy.classList.add("hidden");
statusMessy1.classList.add("hidden");
statusMessy2.classList.add("hidden");
statusMessy3.classList.add("hidden");
let width = 0;
let height = 0;
if (this.lifeStage === LIFE_STAGE_EGG) {
eggDiv.classList.remove("hidden");
hatchedActions.classList.add("hidden");
thePet.classList.add("egg");
} else if (this.lifeStage === LIFE_STAGE_PUP) {
if (this.needsAdvancement) {
petSetup.classList.remove("hidden");
}
thePet.classList.add("pup");
width = WIDTH_PUP;
height = HEIGHT_PUP;
} else if (this.lifeStage === LIFE_STAGE_ADULT) {
if (this.needsAdvancement) {
adultInfo.classList.remove("hidden");
}
thePet.classList.add("adult");
width = WIDTH_ADULT;
height = HEIGHT_ADULT;
} else if (this.lifeStage === LIFE_STAGE_ELDER) {
if (this.needsAdvancement) {
if (this.alive) {
elderInfo.classList.remove("hidden");
} else {
passedAwayInfo.classList.remove("hidden");
thePet.classList.add("elder");
width = WIDTH_ELDER;
height = HEIGHT_ELDER;
}
}
}
width *= this.scaleWidth;
height *= this.scaleHeight;
thePet.style.setProperty("--width", `${width}px`);
thePet.style.setProperty("--height", `${height}px`);
thePet.style.setProperty("--color", this.color);
let happinessFilter = 1 - this.happiness / MAX_HAPPINESS;
if (happinessFilter < 0.6) {
happinessFilter = 0;
}
happinessFilter = (happinessFilter - 0.6) * 2.5;
if (happinessFilter < 0) {
happinessFilter = 0;
}
thePet.style.setProperty(
"filter",
`grayscale(${happinessFilter * 100}%)`
);
if (!this.alive) {
thePet.classList.add("dead");
} else if (this.lifeStage !== LIFE_STAGE_EGG) {
thePet.classList.add(this.type);
if (this.food <= MAX_FOOD / 10) {
statusStarving.classList.remove("hidden");
} else if (this.food <= MAX_FOOD / 2) {
statusHungry.classList.remove("hidden");
}
if (this.happiness <= MAX_HAPPINESS / 3) {
statusUnhappy.classList.remove("hidden");
}
if (this.messCounter >= MAX_MESS) {
statusMessy3.classList.remove("hidden");
} else if (this.messCounter >= MAX_MESS / 2) {
statusMessy2.classList.remove("hidden");
} else if (this.messCounter > 0) {
statusMessy1.classList.remove("hidden");
}
}
if (this.paused) {
pauseButton.innerText = "unpause";
} else {
pauseButton.innerText = "pause";
}
debugLifeStage.innerText = this.lifeStage;
debugAge.innerText = this.age;
debugFood.innerText = this.food;
debugBehavior.innerText = this.behavior;
debugPotty.innerText = this.pottyTimer;
debugMessCounter.innerText = this.messCounter;
debugHappiness.innerText = this.happiness;
this.save();
}
/**
* feeds the pet
* @param {number} amount the amount to feed the pet by
*/
feed(amount) {
if (this.food > MAX_FOOD) {
return;
}
this.food += amount;
if (this.food <= MAX_FOOD) {
this.happiness += FEED_HAPPINESS;
}
this.updateDom();
}
/**
* makes the pet go potty
*/
goPotty() {
if (this.behavior > 45) {
// go potty properly
} else {
this.messCounter += 1;
if (this.messCounter > MAX_MESS) {
this.messCounter = MAX_MESS;
}
}
this.pottyTimer = POTTY_TIME;
pet.updateDom();
}
/**
* pets the pet
*/
pet() {
this.behavior += 0.5;
this.happiness += PET_HAPPINESS;
pet.updateDom();
}
/**
* cleans the pet's space
*/
clean() {
if (this.messCounter > 0) {
this.messCounter -= 1;
this.happiness += CLEAN_HAPPINESS;
} else {
this.behavior += 1;
this.happiness -= CLEAN_HAPPINESS;
}
pet.updateDom();
}
/**
* saves the pet
*/
save() {
localStorage.setItem(PET_SAVE_KEY, JSON.stringify(this));
}
/**
* loads the pet
*/
load() {
const item = localStorage.getItem(PET_SAVE_KEY);
if (item != undefined) {
const loaded = JSON.parse(localStorage.getItem(PET_SAVE_KEY));
for (let k of Object.keys(loaded)) {
this[k] = loaded[k];
}
this.version = CURRENT_PET_VERSION;
this.updateDom();
}
}
/** whether the pet can be updated */
get canUpdate() {
return !this.paused && !this.needsAdvancement;
}
/** the pet's happiness */
get happiness() {
return this._happiness;
}
set happiness(amount) {
if (amount < 0) {
amount = 0;
} else if (amount > MAX_HAPPINESS) {
amount = MAX_HAPPINESS;
}
this._happiness = amount;
}
}
let pet = new Pet();
petSetup.addEventListener("submit", (e) => {
e.preventDefault();
const newName = name.value;
if (newName.trim().length === 0) {
return;
}
pet.name = newName;
for (let name of petName) {
name.innerText = pet.name;
}
pet.hatched = Date.now();
pet.needsAdvancement = false;
pet.updateDom();
});
feedButton.addEventListener("click", () => {
if (!canFeed || !pet.canUpdate) {
return;
}
canFeed = false;
feedButton.disabled = true;
setTimeout(() => {
canFeed = true;
feedButton.disabled = false;
}, FEED_TIMER);
pet.feed(38);
});
petButton.addEventListener("click", () => {
if (!canPet || !pet.canUpdate) {
return;
}
canPet = false;
petButton.disabled = true;
setTimeout(() => {
canPet = true;
petButton.disabled = false;
}, PET_TIMER);
pet.pet();
});
cleanButton.addEventListener("click", () => {
if (!canClean || !pet.canUpdate) {
return;
}
canClean = false;
cleanButton.disabled = true;
setTimeout(() => {
canClean = true;
cleanButton.disabled = false;
}, CLEAN_TIMER);
pet.clean();
});
pauseButton.addEventListener("click", () => {
pet.paused = !pet.paused;
pet.updateDom();
});
const advance = () => {
pet.needsAdvancement = false;
pet.updateDom();
};
for (let btn of document.querySelectorAll("button.advancement")) {
btn.addEventListener("click", advance);
}
passedAwayInfo.querySelector("button").addEventListener("click", () => {
pet = new Pet();
pet.updateDom();
});
const update = () => {
pet.update();
};
setInterval(update, 60000 / UPDATES_PER_MINUTE);
forceUpdateButton.addEventListener("click", update);
resetButton.addEventListener("click", () => {
thePet.classList.remove(pet.type);
pet = new Pet();
pet.updateDom();
});
pet.load();
for (let name of petName) {
name.innerText = pet.name;
}
if (document.body.classList.contains("debug")) {
debug.classList.remove("hidden");
document.pet = pet;
}
pet.updateDom();
console.log(pet);
})();

50
site/sass/pet.scss Normal file
View file

@ -0,0 +1,50 @@
#pet {
#pet-display {
.the-pet {
--color: red;
--width: 250px;
--height: 250px;
background-color: var(--color);
width: var(--width);
height: var(--height);
margin-bottom: 8px;
&.egg {
background-color: white;
// egg shape from https://css-tricks.com/the-shapes-of-css/
width: 126px;
height: 180px;
border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
}
&.square {
border-radius: 2px;
}
&.circle {
border-radius: 50%;
}
&.triangle {
clip-path: polygon(50% 0, 100% 100%, 0 100%);
}
}
.status {
font-style: italic;
}
}
#debug-section [name] {
font-weight: bold;
}
button {
margin-bottom: 4px;
}
.hidden {
display: none;
}
}

View file

@ -7,6 +7,12 @@
<link rel="stylesheet" href="/styles/index.css">
<title>{{title}}</title>
{{{head}}}
{{#each scripts}}
<script type="text/javascript" src="{{this}}" defer></script>
{{/each}}
{{#each styles}}
<link rel="stylesheet" href="/styles/{{this}}">
{{/each}}
</head>
<body>
@ -16,6 +22,7 @@
<span class="pronouns">it/puppy(/she)</span>
</span>
<span class="spacer"></span>
<a href="/pet">creature</a> |
<a href="/blog/">blog</a> |
<a href="/images/">images</a> |
<a href="/pay-me">pay me!</a> |

View file

@ -23,6 +23,10 @@ struct TemplateData<'a, T> {
pub title: &'a str,
/// Custom head data for the page.
pub head: Option<String>,
/// The page's custom scripts.
pub scripts: &'a [String],
/// the page's custom styles.
pub styles: &'a [String],
/// Custom template data.
#[serde(flatten)]
pub extra_data: T,
@ -97,10 +101,16 @@ impl<'a> SiteBuilder<'a> {
let root_path = self.site.site_path.join(ROOT_PATH);
if root_path.exists() {
for entry in root_path.read_dir()? {
for entry in walkdir::WalkDir::new(&root_path) {
let entry = entry?;
let path = entry.path();
std::fs::copy(&path, self.build_path.join(path.strip_prefix(&root_path)?))?;
if path.is_dir() {
continue;
}
let output_path = self.build_path.join(path.strip_prefix(&root_path)?);
let parent_path = output_path.parent().expect("should never fail");
std::fs::create_dir_all(parent_path)?;
std::fs::copy(path, output_path)?;
}
}
@ -125,6 +135,10 @@ impl<'a> SiteBuilder<'a> {
let mut rewriter = HtmlRewriter::new(
Settings {
element_content_handlers: vec![
element!("body", |el| {
el.set_attribute("class", "debug")?;
Ok(())
}),
element!("head", |el| {
el.prepend(r#"<meta charset="utf-8">"#, ContentType::Html);
if self.serving {
@ -211,12 +225,19 @@ impl<'a> SiteBuilder<'a> {
embed.build()
});
let head = page_metadata.embed.map(|mut embed| {
embed.site_name.clone_from(&self.site.config.title);
embed.build()
});
let out = self.reg.render(
&page_metadata.template.unwrap_or_else(|| "base".to_string()),
&TemplateData {
page: page_html,
title: &title,
head,
scripts: &page_metadata.scripts,
styles: &page_metadata.styles,
extra_data,
},
)?;

View file

@ -71,6 +71,12 @@ pub struct PageMetadata {
/// custom embed info for a template
#[serde(default)]
pub embed: Option<EmbedMetadata>,
/// The page's custom scripts, if any.
#[serde(default)]
pub scripts: Vec<String>,
/// the page's custom styles, if any.
#[serde(default)]
pub styles: Vec<String>,
/// The extra stuff to run for the page, if any.
pub extra: Option<String>,
}