diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..13b29fe --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[target.x86_64-pc-windows-msvc] +linker = "ld.lld.exe" +rustflags = ["-C", "link-arg=-fuse-ld=lld"] diff --git a/site/config.yaml b/site/config.yaml index df17015..4427dce 100644 --- a/site/config.yaml +++ b/site/config.yaml @@ -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" diff --git a/site/pages/pet.md b/site/pages/pet.md new file mode 100644 index 0000000..761b8fc --- /dev/null +++ b/site/pages/pet.md @@ -0,0 +1,71 @@ +--- +title: pet +scripts: ["js/pet.js"] +styles: ["pet.css"] +--- + +
+ +
+

+
+
+ + + + + + +
+
+ +
+

whoa! you just found a weird egg! maybe you should watch it and see what happens..

+
+ + + + + + + + + +
+ + +
+ + +
+ +
+ tips!! + +
diff --git a/site/root/js/pet.js b/site/root/js/pet.js new file mode 100644 index 0000000..429004a --- /dev/null +++ b/site/root/js/pet.js @@ -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); +})(); diff --git a/site/sass/pet.scss b/site/sass/pet.scss new file mode 100644 index 0000000..983f3d7 --- /dev/null +++ b/site/sass/pet.scss @@ -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; + } +} diff --git a/site/templates/base.hbs b/site/templates/base.hbs index c1efc4a..c3e85df 100644 --- a/site/templates/base.hbs +++ b/site/templates/base.hbs @@ -7,6 +7,12 @@ {{title}} {{{head}}} + {{#each scripts}} + + {{/each}} + {{#each styles}} + + {{/each}} @@ -16,6 +22,7 @@ it/puppy(/she) + creature | blog | images | pay me! | diff --git a/src/builder.rs b/src/builder.rs index 57eb790..1b9d40a 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -23,6 +23,10 @@ struct TemplateData<'a, T> { pub title: &'a str, /// Custom head data for the page. pub head: Option, + /// 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#""#, 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, }, )?; diff --git a/src/lib.rs b/src/lib.rs index b36889f..2b7703f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,6 +71,12 @@ pub struct PageMetadata { /// custom embed info for a template #[serde(default)] pub embed: Option, + /// The page's custom scripts, if any. + #[serde(default)] + pub scripts: Vec, + /// the page's custom styles, if any. + #[serde(default)] + pub styles: Vec, /// The extra stuff to run for the page, if any. pub extra: Option, }