this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

blog 5 & 6, downpour, some other things

Tholp a6055ca0 19f45aff

+1504 -97
content/res/albumart/buttertones-jazzhound.jpg

This is a binary file and will not be displayed.

content/res/blog/5/QTC_Buildsteps.png

This is a binary file and will not be displayed.

content/res/blog/5/QTC_EditFiles.png

This is a binary file and will not be displayed.

content/res/blog/5/QTC_Import.png

This is a binary file and will not be displayed.

content/res/blog/5/QTC_Includes.png

This is a binary file and will not be displayed.

content/res/icons/pissdrop.png

This is a binary file and will not be displayed.

content/res/icons/pissdrop_close.png

This is a binary file and will not be displayed.

+1 -1
content/res/scripts/bsky-comments.js
··· 151 151 152 152 return `<ul class="bsky_comment" style="display: flex; list-style: none;"> 153 153 <li style="margin-right: 10px;"> 154 - <img src="${comment.post.author.avatar}" width="64px" height="64px" /> 154 + <img src="${comment.post.author.avatar}" width="64px" height="64px" class="downpour_collide"/> 155 155 </li> 156 156 <li> 157 157 <div>
+1
content/res/scripts/downpour/.gitignore
··· 1 + /target
+146
content/res/scripts/downpour/Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "bumpalo" 7 + version = "3.19.1" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" 10 + 11 + [[package]] 12 + name = "cfg-if" 13 + version = "1.0.4" 14 + source = "registry+https://github.com/rust-lang/crates.io-index" 15 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 16 + 17 + [[package]] 18 + name = "console_error_panic_hook" 19 + version = "0.1.7" 20 + source = "registry+https://github.com/rust-lang/crates.io-index" 21 + checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" 22 + dependencies = [ 23 + "cfg-if", 24 + "wasm-bindgen", 25 + ] 26 + 27 + [[package]] 28 + name = "downpour" 29 + version = "0.1.0" 30 + dependencies = [ 31 + "console_error_panic_hook", 32 + "wasm-bindgen", 33 + "web-sys", 34 + ] 35 + 36 + [[package]] 37 + name = "js-sys" 38 + version = "0.3.83" 39 + source = "registry+https://github.com/rust-lang/crates.io-index" 40 + checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" 41 + dependencies = [ 42 + "once_cell", 43 + "wasm-bindgen", 44 + ] 45 + 46 + [[package]] 47 + name = "once_cell" 48 + version = "1.21.3" 49 + source = "registry+https://github.com/rust-lang/crates.io-index" 50 + checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 51 + 52 + [[package]] 53 + name = "proc-macro2" 54 + version = "1.0.105" 55 + source = "registry+https://github.com/rust-lang/crates.io-index" 56 + checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" 57 + dependencies = [ 58 + "unicode-ident", 59 + ] 60 + 61 + [[package]] 62 + name = "quote" 63 + version = "1.0.43" 64 + source = "registry+https://github.com/rust-lang/crates.io-index" 65 + checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" 66 + dependencies = [ 67 + "proc-macro2", 68 + ] 69 + 70 + [[package]] 71 + name = "rustversion" 72 + version = "1.0.22" 73 + source = "registry+https://github.com/rust-lang/crates.io-index" 74 + checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 75 + 76 + [[package]] 77 + name = "syn" 78 + version = "2.0.114" 79 + source = "registry+https://github.com/rust-lang/crates.io-index" 80 + checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" 81 + dependencies = [ 82 + "proc-macro2", 83 + "quote", 84 + "unicode-ident", 85 + ] 86 + 87 + [[package]] 88 + name = "unicode-ident" 89 + version = "1.0.22" 90 + source = "registry+https://github.com/rust-lang/crates.io-index" 91 + checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 92 + 93 + [[package]] 94 + name = "wasm-bindgen" 95 + version = "0.2.106" 96 + source = "registry+https://github.com/rust-lang/crates.io-index" 97 + checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" 98 + dependencies = [ 99 + "cfg-if", 100 + "once_cell", 101 + "rustversion", 102 + "wasm-bindgen-macro", 103 + "wasm-bindgen-shared", 104 + ] 105 + 106 + [[package]] 107 + name = "wasm-bindgen-macro" 108 + version = "0.2.106" 109 + source = "registry+https://github.com/rust-lang/crates.io-index" 110 + checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" 111 + dependencies = [ 112 + "quote", 113 + "wasm-bindgen-macro-support", 114 + ] 115 + 116 + [[package]] 117 + name = "wasm-bindgen-macro-support" 118 + version = "0.2.106" 119 + source = "registry+https://github.com/rust-lang/crates.io-index" 120 + checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" 121 + dependencies = [ 122 + "bumpalo", 123 + "proc-macro2", 124 + "quote", 125 + "syn", 126 + "wasm-bindgen-shared", 127 + ] 128 + 129 + [[package]] 130 + name = "wasm-bindgen-shared" 131 + version = "0.2.106" 132 + source = "registry+https://github.com/rust-lang/crates.io-index" 133 + checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" 134 + dependencies = [ 135 + "unicode-ident", 136 + ] 137 + 138 + [[package]] 139 + name = "web-sys" 140 + version = "0.3.83" 141 + source = "registry+https://github.com/rust-lang/crates.io-index" 142 + checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" 143 + dependencies = [ 144 + "js-sys", 145 + "wasm-bindgen", 146 + ]
+21
content/res/scripts/downpour/Cargo.toml
··· 1 + [package] 2 + name = "downpour" 3 + version = "0.1.0" 4 + edition = "2024" 5 + publish = false 6 + authors = ["Tholp"] 7 + 8 + [profile.release] 9 + lto = true 10 + 11 + [lib] 12 + crate-type = ["cdylib"] 13 + 14 + 15 + [dependencies] 16 + console_error_panic_hook = "0.1.7" 17 + wasm-bindgen = "0.2.106" 18 + 19 + [dependencies.web-sys] 20 + version = "0.3.4" 21 + features = ["Document", "Element", "HtmlElement", "Node", "Window", "ImageData", "HtmlCanvasElement", "CanvasRenderingContext2d", "HtmlCollection", "NodeList", "DomRect", "DomRectList"]
+152
content/res/scripts/downpour/glue.js
··· 1 + // Downpour glue 2 + // Author: Tholp 15 Jan 2026 3 + import init, {Board} from "/res/scripts/downpour/pkg/downpour.js"; 4 + import {hasCookie, getCookie, setCookie} from "/res/scripts/monster.js"; 5 + 6 + var board = undefined; 7 + var canvas; 8 + var scale = 0.5; 9 + var dt = 1000 / 60; 10 + 11 + var paintTimer; 12 + var remakeTimer; 13 + 14 + function remakeBoard() 15 + { 16 + if (paintTimer) 17 + clearTimeout(paintTimer); 18 + 19 + if (!toggle.checked) 20 + return; 21 + 22 + let rect = canvas.parentNode.getBoundingClientRect(); 23 + canvas.style.width = rect.width; 24 + canvas.style.height = canvas.parentNode.scrollHeight; 25 + canvas.width = Math.ceil(rect.width * scale); 26 + canvas.height = Math.ceil(canvas.parentNode.scrollHeight * scale); 27 + 28 + board = new Board(canvas.width, canvas.height, scale, true, canvas, Math.floor(Date.now() / 1000)); 29 + board.setRainRate(rainrate.value); 30 + 31 + paint(); 32 + } 33 + 34 + function onResize() 35 + { 36 + // dont recreate 200 times when drag resizing 37 + clearTimeout(paintTimer); 38 + clearTimeout(remakeTimer); 39 + remakeTimer = setTimeout(remakeBoard, 250); 40 + } 41 + 42 + function paint() 43 + { 44 + if (!document.hasFocus() && !focus.checked) 45 + { 46 + paintTimer = setTimeout(paint, 250); 47 + return; 48 + } 49 + 50 + board.doTick(); 51 + board.paintBoardToCanvas(); 52 + paintTimer = setTimeout(paint, dt); 53 + } 54 + 55 + let toggle; 56 + let focus; 57 + let tps; 58 + let rainrate; 59 + let scalecontrol; 60 + let scaledisplay; 61 + 62 + function hookControlPanel() 63 + { 64 + toggle = document.getElementById("downpour_enable"); 65 + toggle.addEventListener("input", ()=>{ 66 + setCookie("downpour_enabled", toggle.checked, 30); 67 + 68 + if (toggle.checked) 69 + { 70 + remakeBoard(); 71 + } else 72 + { 73 + clearTimeout(paintTimer); 74 + clearTimeout(remakeTimer); 75 + canvas.getContext("2d").clearRect(0,0, canvas.width, canvas.height); 76 + } 77 + }); 78 + if (hasCookie("downpour_enabled")) 79 + { 80 + toggle.checked = getCookie("downpour_enabled"); 81 + } 82 + 83 + focus = document.getElementById("downpour_focus") 84 + if (hasCookie("downpour_focus")) 85 + { 86 + focus.checked = getCookie("downpour_focus"); 87 + } 88 + 89 + tps = document.getElementById("downpour_tps"); 90 + tps.addEventListener("input", ()=>{ 91 + setCookie("downpour_tps", tps.value, 30); 92 + dt = 1000 / tps.value; 93 + }); 94 + if(hasCookie("downpour_tps")) 95 + { 96 + tps.value = getCookie("downpour_tps"); 97 + dt = 1000 / tps.value; 98 + } 99 + 100 + rainrate = document.getElementById("downpour_rainrate"); 101 + rainrate.addEventListener("input", ()=>{ 102 + setCookie("downpour_rainrate", rainrate.value, 30); 103 + if (board) 104 + board.setRainRate(Math.max(rainrate.value, 1)); 105 + }); 106 + if(hasCookie("downpour_rainrate")) 107 + { 108 + rainrate.value = getCookie("downpour_rainrate"); 109 + if (board) 110 + board.setRainRate(Math.max(rainrate.value, 1)); 111 + } 112 + 113 + scalecontrol = document.getElementById("downpour_scale"); 114 + scaledisplay = document.getElementById("downpour_scale_display"); 115 + 116 + scalecontrol.addEventListener("input", ()=>{ 117 + setCookie("downpour_scale", scalecontrol.value, 30); 118 + scale = 1/scalecontrol.value; 119 + scaledisplay.innerHTML = scalecontrol.value; 120 + remakeBoard(); 121 + }); 122 + if (hasCookie("downpour_scale")) 123 + { 124 + scalecontrol.value = getCookie("downpour_scale"); 125 + scale = 1/scalecontrol.value; 126 + scaledisplay.innerHTML = scalecontrol.value; 127 + } 128 + } 129 + 130 + async function glue() 131 + { 132 + await init(); 133 + 134 + canvas = document.createElement("canvas"); 135 + canvas.id = "downpourcanvas"; 136 + 137 + window.addEventListener("resize", onResize); 138 + document.body.appendChild(canvas); 139 + hookControlPanel(); 140 + 141 + // Restart when/if bsky comments load 142 + const resizeObserver = new ResizeObserver((elements) => { 143 + // nondestructive size change of the board is very doable, might look into it later 144 + // Main issue would be what to do when writing webcollide particles onto existing rain, overwrite probably looks ok? 145 + remakeBoard(); 146 + }); 147 + resizeObserver.observe(document.body); 148 + 149 + remakeBoard(); 150 + } 151 + 152 + await glue();
+386
content/res/scripts/downpour/src/board.rs
··· 1 + use crate::particles::{Neighborhood, Particle}; 2 + use crate::randumb::Randumb; 3 + use wasm_bindgen::prelude::*; 4 + use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData, Window}; 5 + 6 + struct BoardCell { 7 + content: Particle, 8 + ticked: bool, // Did we already iterate over this? 9 + } 10 + 11 + #[wasm_bindgen] 12 + pub struct Board { 13 + cells: Vec<Vec<BoardCell>>, 14 + 15 + board_width: usize, 16 + board_height: usize, 17 + scale: f64, //gridcells per pixel 18 + 19 + canvas: HtmlCanvasElement, 20 + ctx: CanvasRenderingContext2d, 21 + 22 + rand: Randumb, 23 + ri: usize, 24 + 25 + rainrate: u16, 26 + } 27 + #[wasm_bindgen] 28 + impl Board { 29 + // returns new width and height, if one of them are 0 you should discard whatever you're doing 30 + fn clip_region(&self, x: usize, y: usize, w: usize, h: usize) -> (usize, usize) { 31 + if x > self.board_width || y > self.board_height { 32 + return (0, 0); 33 + } 34 + let mut neww = w; 35 + let mut newh = h; 36 + if x + w > self.board_width { 37 + neww = self.board_width - x; 38 + } 39 + if y + h > self.board_height { 40 + newh = self.board_height - y; 41 + } 42 + (neww, newh) 43 + } 44 + 45 + // Does no bounds checking, use clip_region beforehand if it's important 46 + // left to right, top to bottom 47 + fn for_region<F>(x: usize, y: usize, w: usize, h: usize, mut f: F) 48 + where 49 + F: FnMut(usize, usize), 50 + { 51 + let mut cur_x = x; 52 + let mut cur_y = y; 53 + while cur_y < y + h { 54 + while cur_x < x + w { 55 + f(cur_x, cur_y); 56 + cur_x += 1; 57 + } 58 + cur_x = x; 59 + cur_y += 1; 60 + } 61 + } 62 + 63 + // left to right, bottom to top 64 + fn for_region_bottom_up<F>(x: usize, y: usize, w: usize, h: usize, mut f: F) 65 + where 66 + F: FnMut(usize, usize), 67 + { 68 + let mut cur_x = x; 69 + let mut cur_y = y + h - 1; 70 + while cur_y >= y { 71 + while cur_x <= x + w - 1 { 72 + f(cur_x, cur_y); 73 + cur_x += 1; 74 + } 75 + cur_x = x; 76 + if cur_y != 0 { 77 + cur_y -= 1; 78 + } else { 79 + return; 80 + } 81 + } 82 + } 83 + 84 + fn set_cell_ticked(&mut self, x: usize, y: usize, ticked: bool) { 85 + self.cells[x][y].ticked = ticked; 86 + } 87 + 88 + fn is_cell_ticked(&self, x: usize, y: usize) -> bool { 89 + self.cells[x][y].ticked 90 + } 91 + 92 + fn get_particle(&self, x: usize, y: usize) -> Particle { 93 + self.cells[x][y].content 94 + } 95 + 96 + fn set_particle(&mut self, x: usize, y: usize, particle: Particle) { 97 + // if x > self.board_width || y > self.board_height { 98 + // return; 99 + // } 100 + self.cells[x][y].content = particle; 101 + } 102 + 103 + fn make_web_colliders(&mut self, window: Window) { 104 + let document = window.document().expect("should have a document on window"); 105 + let elements = document.get_elements_by_class_name("downpour_collide"); 106 + let mut i = 0; 107 + while i < elements.length() { 108 + let e = elements.item(i); 109 + if e.is_none() { 110 + return; 111 + } 112 + let e = e.unwrap(); 113 + let rects = e.get_client_rects(); 114 + let mut j = 0; 115 + while j < rects.length() { 116 + let rect = rects.item(j); 117 + if rect.is_none() { 118 + crate::warn("got bad rect :("); 119 + return; 120 + } 121 + let rect = rect.unwrap(); 122 + 123 + let to_board_coord = |f: f64| -> usize { 124 + let scaled = f * self.scale; 125 + scaled.round() as usize 126 + }; 127 + 128 + let scroll_x = window.scroll_x().unwrap_or(0.0f64); 129 + let scroll_y = window.scroll_y().unwrap_or(0.0f64); 130 + 131 + let x = to_board_coord(rect.x() + scroll_x); 132 + let y = to_board_coord(rect.y() + scroll_y); 133 + let w = to_board_coord(rect.width()); 134 + let h = to_board_coord(rect.height()); 135 + 136 + let (w, h) = self.clip_region(x, y, w, h); 137 + 138 + if w == 0 || h == 0 { 139 + return; 140 + } 141 + 142 + Board::for_region(x, y, w, h, |x, y| { 143 + self.set_particle(x, y, Particle::WebCollide); 144 + }); 145 + 146 + j += 1; 147 + } 148 + i += 1; 149 + } 150 + } 151 + 152 + // 3x3 with (x,y) as center 153 + fn get_neighborhood(&self, x: usize, y: usize) -> Neighborhood { 154 + let mut n: Neighborhood = [ 155 + [Particle::Air, Particle::Air, Particle::Air], 156 + [Particle::Air, Particle::Air, Particle::Air], 157 + [Particle::Air, Particle::Air, Particle::Air], 158 + ]; 159 + 160 + let nx = x as i32 - 1; 161 + let ny = y as i32 - 1; 162 + let mut index = 0; 163 + let mut cur_x = nx; 164 + let mut cur_y = ny; 165 + while cur_y < ny + 3 { 166 + while cur_x < nx + 3 { 167 + let p: Particle; 168 + if cur_x < 0 { 169 + p = Particle::EdgeLeft; 170 + } else if cur_x >= self.board_width as i32 { 171 + p = Particle::EdgeRight; 172 + } else if cur_y < 0 { 173 + p = Particle::EdgeTop; 174 + } else if cur_y >= self.board_height as i32 { 175 + p = Particle::EdgeBottom 176 + } else { 177 + p = self.get_particle(cur_x as usize, cur_y as usize); 178 + } 179 + 180 + n[index / 3][index % 3] = p; 181 + index += 1; 182 + cur_x += 1; 183 + } 184 + cur_x = x as i32 - 1; 185 + cur_y += 1; 186 + } 187 + 188 + n 189 + } 190 + 191 + // When the board is ticked is controlled from the JS side so it's easier to have a tickrate option 192 + #[wasm_bindgen(js_name = "doTick")] 193 + pub fn do_tick(&mut self) { 194 + //crate::log("Ticking.."); 195 + // Bottom up so stuff moves 196 + Board::for_region_bottom_up(0, 0, self.board_width, self.board_height, |x, y| { 197 + // crate::log(&format!("{} {}", x, y)); 198 + if self.is_cell_ticked(x, y) { 199 + return; 200 + } 201 + 202 + let p = self.get_particle(x, y); 203 + let swap_preferences = p.swap_preference(); 204 + // Static particle 205 + if swap_preferences == [[[0, 0]]] { 206 + //crate::log("static par"); 207 + return; 208 + } 209 + //crate::log("mover"); 210 + 211 + // Run through this partricle's potential moves 212 + for group in swap_preferences { 213 + let mut group_index = 0; 214 + self.ri += self.rand.next() as usize % group.len(); 215 + if self.ri >= usize::max_value() / 2 { 216 + self.ri = 0; 217 + } 218 + //let group_index_offset = self.rand.next() as usize;// % group.len(); 219 + while group_index < group.len() { 220 + let pref = group[(self.ri + group_index) % group.len()]; 221 + let mut rx = x as i32 + pref[0]; 222 + let ry = y as i32 + pref[1]; 223 + let replacement; 224 + 225 + if ry < 0 { 226 + self.set_particle(x, y, Particle::Air); 227 + return; 228 + } else if ry >= self.board_height as i32 { 229 + self.set_particle(x, y, Particle::Air); 230 + return; 231 + } else if rx < 0 { 232 + rx = self.board_width as i32; 233 + replacement = self.get_particle(rx as usize, ry as usize); 234 + } else if rx >= self.board_width as i32 { 235 + rx = 0; 236 + replacement = self.get_particle(rx as usize, ry as usize); 237 + } else { 238 + replacement = self.get_particle(rx as usize, ry as usize); 239 + } 240 + 241 + if replacement == Particle::Air { 242 + //crate::log(&format!("airreplace {} {}", x, y)); 243 + self.set_particle(x, y, Particle::Air); 244 + self.set_particle(rx as usize, ry as usize, p); 245 + self.set_cell_ticked(rx as usize, ry as usize, true); 246 + return; 247 + } 248 + 249 + group_index += 1; 250 + } 251 + } 252 + // crate::log("No suitable move"); 253 + }); 254 + 255 + //Once everything has moved have it react to it's new surroundings 256 + Board::for_region(0, 0, self.board_width, self.board_height, |x, y| { 257 + let p = self.get_particle(x, y); 258 + if p == Particle::Rain || p == Particle::FastRain 259 + // eek out a little perf? 260 + { 261 + self.set_particle(x, y, p.react_to_neighborhood(self.get_neighborhood(x, y))); 262 + } 263 + }); 264 + 265 + // Spawn new rain 266 + Board::for_region(0, 1, self.board_width, 2, |x, y| { 267 + if (x as u16 + self.rand.next()) % self.rainrate == 0 { 268 + self.set_particle(x, y, Particle::Rain); 269 + } 270 + }); 271 + 272 + // unmark everything as ticked 273 + Board::for_region(0, 0, self.board_width, self.board_height, |x, y| { 274 + self.set_cell_ticked(x, y, false); 275 + }); 276 + } 277 + 278 + #[wasm_bindgen(js_name = "paintBoardToCanvas")] 279 + pub fn paint_board_to_canvas(&mut self) -> Result<(), JsValue> { 280 + //crate::log("Painting.."); 281 + let mut data = Vec::new(); 282 + let w = self.board_width; 283 + let h = self.board_height; 284 + data.reserve(w * h * 4); 285 + Board::for_region(0, 0, w, h, |x, y| { 286 + // crate::log(&format!("{} {}", x, y).to_string()); 287 + for channel in self.get_particle(x, y).color() { 288 + data.push(channel); 289 + } 290 + }); 291 + self.ctx.put_image_data( 292 + &ImageData::new_with_u8_clamped_array( 293 + wasm_bindgen::Clamped(data.as_slice()), 294 + self.board_width as u32, 295 + ) 296 + .unwrap(), 297 + 0.0, 298 + 0.0, 299 + ) 300 + } 301 + 302 + #[wasm_bindgen(constructor)] 303 + pub fn new( 304 + width: usize, 305 + height: usize, 306 + scale: f64, 307 + do_web_collide: bool, 308 + canvas: HtmlCanvasElement, 309 + seed: u32, 310 + ) -> Board { 311 + let ctx = canvas 312 + .get_context("2d") 313 + .unwrap() 314 + .unwrap() 315 + .dyn_into::<web_sys::CanvasRenderingContext2d>() 316 + .unwrap(); 317 + 318 + // check the style for #downpourcanvas, this isn't the only thing that makes it not blurry 319 + ctx.set_image_smoothing_enabled(false); 320 + let _ = ctx.set_global_composite_operation("copy"); 321 + 322 + let mut board = Board { 323 + cells: Vec::new(), 324 + board_width: width, 325 + board_height: height - 1, 326 + scale, 327 + canvas, 328 + ctx, 329 + 330 + rand: Randumb::new(seed), 331 + // rand: Randumb::new_static(), 332 + ri: 0, 333 + 334 + rainrate: 175, 335 + }; 336 + 337 + board.cells.reserve(board.board_width); 338 + let mut i = 0; 339 + while i < board.board_width { 340 + board.cells.push(Vec::new()); 341 + i += 1; 342 + } 343 + 344 + for collum in &mut board.cells { 345 + collum.reserve(board.board_height); 346 + 347 + let mut j = 0; 348 + while j < board.board_height { 349 + // if j < 5 { 350 + // collum.push(Particle::Rain); 351 + // } else { 352 + collum.push(BoardCell { 353 + content: Particle::Air, 354 + ticked: false, 355 + }); 356 + // } 357 + 358 + j += 1; 359 + } 360 + } 361 + 362 + //crate::log(&format!("dims {} {}", board.board_width, board.board_height).to_string()); 363 + //crate::log(&format!("vecdims {} {}", board.cells.len(), board.cells[0].len()).to_string()); 364 + 365 + if do_web_collide { 366 + //crate::log("making web collide"); 367 + let window = web_sys::window().expect("no global `window` exists"); 368 + // let document = window.document().expect("should have a document on window"); 369 + board.make_web_colliders(window); 370 + } 371 + 372 + board 373 + } 374 + 375 + #[wasm_bindgen(js_name = "printRand")] 376 + pub fn print_rand(&mut self) { 377 + for _ in 0..256 { 378 + crate::log_u16(self.rand.next()); 379 + } 380 + } 381 + 382 + #[wasm_bindgen(js_name = "setRainRate")] 383 + pub fn set_rain_rate(&mut self, rate: u16) { 384 + self.rainrate = rate; 385 + } 386 + }
+34
content/res/scripts/downpour/src/lib.rs
··· 1 + use wasm_bindgen::prelude::*; 2 + mod board; 3 + mod particles; 4 + mod randumb; 5 + 6 + #[wasm_bindgen] 7 + extern "C" { 8 + // Use `js_namespace` here to bind `console.log(..)` instead of just 9 + // `log(..)` 10 + #[wasm_bindgen(js_namespace = console)] 11 + fn log(s: &str); 12 + 13 + // The `console.log` is quite polymorphic, so we can bind it with multiple 14 + // signatures. Note that we need to use `js_name` to ensure we always call 15 + // `log` in JS. 16 + #[wasm_bindgen(js_namespace = console, js_name = log)] 17 + fn log_u32(a: u32); 18 + 19 + #[wasm_bindgen(js_namespace = console, js_name = log)] 20 + fn log_u16(a: u16); 21 + 22 + #[wasm_bindgen(js_namespace = console, js_name = log)] 23 + fn log_str_u32(s: &str, a: u32); 24 + 25 + #[wasm_bindgen(js_namespace = console, js_name = warn)] 26 + fn warn(s: &str); 27 + } 28 + 29 + #[wasm_bindgen(start)] 30 + fn start() -> Result<(), JsValue> { 31 + console_error_panic_hook::set_once(); 32 + log("Downpour successfully loaded!"); 33 + Ok(()) 34 + }
+168
content/res/scripts/downpour/src/particles.rs
··· 1 + #[derive(Clone, Copy, PartialEq, Eq)] 2 + pub enum Particle { 3 + Air, 4 + WebCollide, 5 + Rain, 6 + FastRain, // Rain bunches up on the side more than i want, speed it up when theres alot 7 + 8 + // these won't actually exist but are returned when a cell's neighborhood goes past the edge 9 + // seperate entries encase i want to do screen wrap 10 + EdgeLeft, 11 + EdgeRight, 12 + EdgeTop, 13 + EdgeBottom, // Freaks 14 + } 15 + 16 + pub type Neighborhood = [[Particle; 3]; 3]; 17 + 18 + pub trait NeighborhoodChecks { 19 + // includes corners 20 + fn surroundings_have(&self, p: Particle) -> u8; 21 + 22 + // shares edges 23 + fn immediate_surrondings_have(&self, p: Particle) -> u8; 24 + 25 + fn is_ontop_of(&self, p: Particle) -> u8; 26 + } 27 + 28 + impl NeighborhoodChecks for Neighborhood { 29 + fn surroundings_have(&self, p: Particle) -> u8 { 30 + let mut count = 0; 31 + let mut index = 0; 32 + while index < 9 { 33 + if index != 5 && self[index / 3][index % 3] == p { 34 + // index 5 is center 35 + count += 1; 36 + } 37 + index += 1; 38 + } 39 + 40 + count 41 + } 42 + 43 + fn immediate_surrondings_have(&self, p: Particle) -> u8 { 44 + let mut count = 0; 45 + let mut index = 0; 46 + while index < 9 { 47 + if index % 2 == 1 && self[index / 3][index % 3] == p { 48 + count += 1; 49 + } 50 + index += 1; 51 + } 52 + count 53 + } 54 + 55 + fn is_ontop_of(&self, p: Particle) -> u8 { 56 + let mut count = 0; 57 + let mut index = 7; 58 + while index < 9 { 59 + if index % 2 == 1 && self[index / 3][index % 3] == p { 60 + count += 1; 61 + } 62 + index += 1; 63 + } 64 + count 65 + } 66 + } 67 + 68 + impl Particle { 69 + // RGBA 70 + pub fn color(&self) -> [u8; 4] { 71 + match self { 72 + // Self::Rain => [0, 0, 120, 255], 73 + // Self::Water => [0, 0, 120, 255], 74 + Self::Rain => [0, 90, 0, 255], 75 + Self::FastRain => [0, 90, 0, 255], 76 + // Self::FastRain => [90, 0, 0, 255], 77 + // Self::Rain => [202, 193, 35, 255], 78 + // Self::WebCollide => [255, 0, 255, 255], 79 + // Self::Air => [255, 0, 0, 255], 80 + // _ => [0, 0, 0, 0], 81 + _ => [0, 0, 0, 0], 82 + } 83 + } 84 + 85 + // Kind of a nasty return type, used as follows: 86 + // rval[x][y][z] where 87 + // x: a preference group, the board will try to execute these in order 88 + // y: a move in a group, the board will choose a random order to try these in and move to the next group if it can't do any 89 + // z: 0 for x offset, 1 for y, remember the coordinate system is from topleft so +y is down. 90 + pub fn swap_preference(&self) -> &[&[[i32; 2]]] { 91 + match self { 92 + // Yeah, it really is just this 93 + Self::Rain => &[ 94 + &[[1, 3], [1, 2], [1, 1]], 95 + &[[0, 4]], 96 + &[[0, 3]], 97 + &[[0, 2]], 98 + &[[0, 1]], 99 + &[[2, 0]], 100 + &[[-2, 0]], 101 + &[[1, 0], [-1, 0]], 102 + &[[0, 0]], 103 + ], 104 + Self::FastRain => &[ 105 + &[[2, 6], [2, 5], [2, 4], [2, 3], [2, 2], [2, 1]], 106 + &[[1, 6], [1, 5], [1, 4], [1, 3], [1, 2], [1, 1]], 107 + &[[0, 7]], 108 + &[[0, 6]], 109 + &[[0, 5]], 110 + &[[0, 4]], 111 + &[[0, 3]], 112 + &[[0, 2]], 113 + &[[0, 1]], 114 + &[[4, 0]], 115 + &[[-4, 0]], 116 + &[[3, 0]], 117 + &[[-3, 0]], 118 + &[[2, 0]], 119 + &[[-2, 0]], 120 + &[[1, 0]], 121 + &[[-1, 0]], 122 + ], 123 + // Self::Water => &[&[[1, 1], [-1, 1], [0, 1]], &[[1, 0], [-1, 0]], &[[0, 0]]], 124 + _ => &[&[[0, 0]]], 125 + } 126 + } 127 + 128 + pub fn is_edge(&self) -> bool { 129 + self.is_vertical_edge() || self.is_horizontal_edge() 130 + } 131 + 132 + pub fn is_vertical_edge(&self) -> bool { 133 + match self { 134 + Self::EdgeTop => true, 135 + Self::EdgeBottom => true, 136 + _ => false, 137 + } 138 + } 139 + 140 + pub fn is_horizontal_edge(&self) -> bool { 141 + match self { 142 + Self::EdgeLeft => true, 143 + Self::EdgeRight => true, 144 + _ => false, 145 + } 146 + } 147 + 148 + // Transform into different type 149 + pub fn react_to_neighborhood(&self, n: Neighborhood) -> Particle { 150 + match self { 151 + Self::Rain => { 152 + if n.surroundings_have(Self::Rain) + n.surroundings_have(Self::FastRain) > 5 { 153 + Self::FastRain 154 + } else { 155 + *self 156 + } 157 + } 158 + Self::FastRain => { 159 + if n.surroundings_have(Self::Air) > 5 { 160 + Self::Rain 161 + } else { 162 + *self 163 + } 164 + } 165 + _ => *self, 166 + } 167 + } 168 + }
+43
content/res/scripts/downpour/src/randumb.rs
··· 1 + // Not using the rand crate because I want to keep the wasm binary kind of small and we don't need that good of randomness 2 + 3 + const SEQUENCE_LENGTH: usize = 512; 4 + pub struct Randumb { 5 + sequence: [u16; SEQUENCE_LENGTH], 6 + rand_index: usize, 7 + } 8 + 9 + impl Randumb { 10 + pub fn new(seed: u32) -> Randumb { 11 + // this doesn't need to be good 12 + let mut sequence = [0; SEQUENCE_LENGTH]; 13 + let mut last_num: u16 = (seed % 65535) as u16; 14 + for i in 0..SEQUENCE_LENGTH { 15 + sequence[i] = (seed % std::cmp::min(last_num, last_num % 65534) as u32 + 3) as u16; 16 + sequence[i] += i as u16; 17 + last_num = sequence[i]; 18 + } 19 + 20 + Randumb { 21 + sequence, 22 + rand_index: 0, 23 + } 24 + } 25 + 26 + pub fn new_static() -> Randumb { 27 + let mut sequence = [0; SEQUENCE_LENGTH]; 28 + for i in 0..SEQUENCE_LENGTH { 29 + // sequence[i] = sequence[i - 1] + sequence[i - 2]; 30 + sequence[i] = i as u16; 31 + } 32 + 33 + Randumb { 34 + sequence, 35 + rand_index: 0, 36 + } 37 + } 38 + 39 + pub fn next(&mut self) -> u16 { 40 + self.rand_index += 1; 41 + self.sequence[self.rand_index % self.sequence.len()] 42 + } 43 + }
+2 -1
content/res/scripts/highlighter/styles/srcery-tholp.css
··· 1 1 pre code.hljs { 2 2 display: block; 3 3 overflow-x: auto; 4 - padding: 1em 4 + padding: 1em; 5 + height: inherit; 5 6 } 6 7 code.hljs { 7 8 padding: 3px 5px
+35
content/res/scripts/monster.js
··· 1 + // Basic cookie manager mostly pasted from w3schools 2 + 3 + export function getCookie(cname) // -> String 4 + { 5 + let name = cname + "="; 6 + let ca = document.cookie.split(';'); 7 + for(let i = 0; i < ca.length; i++) { 8 + let c = ca[i]; 9 + while (c.charAt(0) == ' ') { 10 + c = c.substring(1); 11 + } 12 + if (c.indexOf(name) == 0) { 13 + return c.substring(name.length, c.length); 14 + } 15 + } 16 + return ""; 17 + } 18 + 19 + export function setCookie(cname, cvalue, exdays) // -> void 20 + { 21 + const d = new Date(); 22 + d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); 23 + let expires = "expires="+d.toUTCString(); 24 + document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; 25 + } 26 + 27 + export function hasCookie(cname) // -> bool 28 + { 29 + return getCookie(cname) != ""; 30 + } 31 + 32 + export function deleteCookie(cname) // -> void 33 + { 34 + setCookie(cname, "", -1); 35 + }
+118 -51
content/style.css
··· 4 4 font-family: "Miracode"; 5 5 src: url("/res/fonts/Miracode.ttf"); 6 6 7 - size-adjust: 85%; 7 + size-adjust: 90%; 8 + /* ascent-override: 10%; */ 8 9 } 9 10 10 11 @font-face { ··· 153 154 /* padding: 1%; */ 154 155 } 155 156 157 + .blog_footer h4, .blog_footer h5 { 158 + color: inherit; 159 + } 160 + 156 161 .blog_songblurb { 157 162 margin-left: 5%; 158 163 margin-right: 5%; ··· 175 180 grid-template-columns: 1fr 2fr; 176 181 } 177 182 183 + .blog_songblurb_interior_bright p { 184 + color: black; 185 + } 186 + 178 187 .blog_songblurb_album { 179 188 /* display: inline-grid; */ 180 189 text-align: center; 181 190 /* width: 35%; */ 182 191 } 183 192 193 + .blog_songblurb_text{ 194 + align: center; 195 + display: block; 196 + } 197 + 184 198 .blog_section 185 199 { 186 200 text-decoration: none; 187 201 } 188 202 189 - .blog_section h2, .blog_section h3 { 203 + .blog_section h2, .blog_section h3, .blog_section h4 { 190 204 text-decoration: underline; 191 205 } 192 206 ··· 230 244 border: 2px solid black; 231 245 border-radius: 0px; 232 246 background: #222; 247 + } 248 + 249 + .tabs { 250 + display: grid; 251 + grid-template-areas: "content"; 233 252 } 234 253 235 254 .tabtitle { 236 255 border-radius: 0px; 237 256 background: #009900; 238 257 border-bottom: 2px solid black; 239 - } 240 - 241 - .tabscontainer { 242 - /* border-radius: 0px 0px 12px 12px; */ 258 + color: var(--attention); 243 259 } 244 260 245 261 .tabheader { ··· 263 279 264 280 .tabcontent { 265 281 display: block; 266 - /* border: 2px solid black;*/ 267 - /* margin: 0px; */ 268 - /* border-radius: 0px 0px 12px 12px; */ 269 282 overflow: hidden; 270 - } 271 - 272 - 273 - .blog_songblurb_text{ 274 - align: center; 275 - display: block; 276 - 283 + grid-area: content; 277 284 } 278 285 279 286 .missingpagegif { ··· 283 290 margin-right: 20%; 284 291 } 285 292 286 - .missingpagegiftext { 287 - /* shoutout to hellbie for this */ 288 - filter: url('data:image/svg+xml,\ 289 - <svg height="1000" preserveaspectratio="xMidYMid meet" style="width: 100%; height: 100%; transform: translate3d(0px, 0px, 0px);" viewbox="0 0 1000 1000" width="1000"\ 290 - xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">\ 291 - <defs>\ 292 - <filter id="pixelate" x="0%" y="0%" width="100%" height="100%">\ 293 - <feGaussianBlur stdDeviation="2" in="SourceGraphic" result="smoothed" />\ 294 - <feImage width="15" height="15" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAWSURBVAgdY1ywgOEDAwKxgJhIgFQ+AP/vCNK2s+8LAAAAAElFTkSuQmCC" result="displacement-map" />\ 295 - <feTile in="displacement-map" result="pixelate-map" />\ 296 - <feDisplacementMap in="smoothed" in2="pixelate-map" xChannelSelector="R" yChannelSelector="G" scale="15" result="pre-final"/>\ 297 - <feComposite operator="in" in2="SourceGraphic"/>\ 298 - </filter>\ 299 - </defs>\ 300 - </svg>#pixelate'); 301 - 302 - text-align: center; 303 - background: #FFFFFF; 304 - font-family: "Futura Extra Black Condesed", sans-serif; 305 - font-size: 90px; 306 - word-wrap: break-word; 307 - } 308 - 309 - 310 293 .landingbg { 311 294 width: 100%; 312 295 /* @media (width >= 1900px) { */ ··· 314 297 /* } */ 315 298 position: relative; 316 299 /* background: #001500; */ 317 - overflow: scroll; 300 + /* overflow: scroll; */ 318 301 319 302 } 320 303 ··· 324 307 /* width: 100vw; */ 325 308 left: 0; 326 309 right: 0; 327 - background: #001000; 310 + /* background: #001000; */ 328 311 padding-top: 15vh; 329 312 padding-bottom: 10vh; 330 313 } ··· 368 351 align-items: center; 369 352 padding: 1%; 370 353 371 - margin-left: auto; 372 - margin-right: auto; 354 + margin-left: .25%; 355 + margin-right: .25%; 373 356 374 357 @media (width >= 1280px) { 375 358 width: 25%; ··· 414 397 415 398 margin-left: auto; 416 399 margin-right: auto; 400 + margin-top: .5%; 417 401 418 402 @media (width >= 1235px) { 419 403 width: 1235px; ··· 532 516 /* Elements */ 533 517 534 518 body { 535 - background-color: #001500; 536 - /* font-family: "Noto Sans", sans-serif; */ 519 + /* background-color: #001500; */ 520 + background-color: #001000; 521 + /* font-family: "Noto Sans", sans-serif; */ 537 522 margin-left: 0; 538 523 margin-right: 0; 539 - /* background: #663300; */ 524 + /* background: #663300; */ 540 525 } 541 526 542 527 a { ··· 589 574 content:""; 590 575 margin-left: 0.25em; 591 576 } 592 - } 577 + 593 578 594 579 .blog_container audio { 595 580 margin-left: auto; ··· 610 595 611 596 h3 { 612 597 color: #0099BB; 598 + } 599 + 600 + h4, h5 { 601 + color: #0077FF; 613 602 } 614 603 615 604 footer { 616 605 color: var(--accent); 617 606 justify-content: center; 618 - text-algin:center; 607 + text-align: center; 619 608 } 620 609 621 610 code, strong { ··· 633 622 border: 2px solid black; 634 623 border-top: 0px; 635 624 overflow: hidden; 625 + } 626 + 627 + .tabs pre { 628 + height: -webkit-fill-available; 636 629 } 637 630 638 631 .tabcontent code { ··· 683 676 border: 2px solid var(--clickable); 684 677 } 685 678 686 - .blog_container h5:not([class="asterisk"]), .blog_container h4, figcaption { 679 + .blog_container h5:not([class="asterisk"]), figcaption { 687 680 color: var(--text-mild); 688 681 } 689 682 690 - h1, h2, h3 { 683 + h1, h2, h3, h4 { 691 684 font-family: "Gontserrat Semibold", sans-serif; 692 685 } 693 686 ··· 697 690 margin-left: 10px; 698 691 padding-left: 20px; 699 692 } 693 + 694 + fieldset { 695 + border-color: var(--accent); 696 + } 697 + 698 + legend { 699 + color: var(--accent); 700 + } 701 + 702 + /* Downpour things */ 703 + 704 + #downpourcanvas { 705 + pointer-events: none; 706 + position: absolute; 707 + top: 0; 708 + left: 0; 709 + z-index: -100; 710 + /* width: 100%; */ 711 + image-rendering: pixelated; 712 + image-rendering: crisp-edges; 713 + image-rendering: -moz-crisp-edges; 714 + image-rendering: -o-crisp-edges; 715 + -ms-interpolation-mode: nearest-neighbor; 716 + } 717 + 718 + #downpour_settings { 719 + display: none; 720 + background: var(--container-color); 721 + color: white; 722 + position: fixed; 723 + left: 2em; 724 + bottom: 2em; 725 + 726 + z-index: 100; 727 + } 728 + 729 + #downpour_open_settings { 730 + position: fixed; 731 + left: 2em; 732 + bottom: 2em; 733 + 734 + z-index: 100; 735 + } 736 + 737 + #downpour_close_settings, #downpour_open_settings { 738 + cursor: pointer; 739 + } 740 + 741 + #downpour_close_settings::before { 742 + background-image: url('/res/icons/pissdrop_close.png'); 743 + 744 + --size: 0.8em; 745 + 746 + background-size: var(--size) var(--size); 747 + display: inline-block; 748 + width: var(--size); 749 + height: var(--size); 750 + content:""; 751 + margin-left: 0.25em; 752 + } 753 + 754 + #downpour_open_settings::before { 755 + background-image: url('/res/icons/pissdrop.png'); 756 + 757 + --size: 0.8em; 758 + 759 + background-size: var(--size) var(--size); 760 + display: inline-block; 761 + width: var(--size); 762 + height: var(--size); 763 + content:""; 764 + margin-left: 0.25em; 765 + } 766 +
+27
skid/404.sk
··· 1 1 &insert("//style.sk") 2 2 &insert("//templates_generic.sk") 3 3 &page_title("404") 4 + 5 + <!-- This fucks with the highlighter for like everything --> 6 + <style> 7 + .missingpagegiftext { 8 + /* shoutout to hellbie for this */ 9 + filter: url('data:image/svg+xml,\ 10 + <svg height="1000" preserveaspectratio="xMidYMid meet" style="width: 100%; height: 100%; transform: translate3d(0px, 0px, 0px);" viewbox="0 0 1000 1000" width="1000"\ 11 + xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">\ 12 + <defs>\ 13 + <filter id="pixelate" x="0%" y="0%" width="100%" height="100%">\ 14 + <feGaussianBlur stdDeviation="2" in="SourceGraphic" result="smoothed" />\ 15 + <feImage width="15" height="15" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAWSURBVAgdY1ywgOEDAwKxgJhIgFQ+AP/vCNK2s+8LAAAAAElFTkSuQmCC" result="displacement-map" />\ 16 + <feTile in="displacement-map" result="pixelate-map" />\ 17 + <feDisplacementMap in="smoothed" in2="pixelate-map" xChannelSelector="R" yChannelSelector="G" scale="15" result="pre-final"/>\ 18 + <feComposite operator="in" in2="SourceGraphic"/>\ 19 + </filter>\ 20 + </defs>\ 21 + </svg>#pixelate'); 22 + 23 + text-align: center; 24 + background: #FFFFFF; 25 + font-family: "Futura Extra Black Condesed", sans-serif; 26 + font-size: 90px; 27 + word-wrap: break-word; 28 + } 29 + </style> 30 + 4 31 <div class="missingpagegif"> 5 32 <div class="missingpagegiftext unselectable" id="gifheader">Theres Nothing</div> 6 33 </div>
+1 -1
skid/blog/atom.sk
··· 23 23 <summary>!insert("[[file..1]]" "blurb")</summary> 24 24 <updated>!insert("[[file..1]]" "date")T01:00:00-05:00</updated> 25 25 </entry> 26 - }}} 26 + }}} 27 27 </feed>
+1 -1
skid/blog/blog1.sk
··· 38 38 39 39 }}} 40 40 &section(){{{ 41 - <div align="center" height="1.5em" class="blog_nav"> 41 + <div align="center" height="1.5em" class="blog_nav downpour_collide"> 42 42 43 43 ~~previous~~ 44 44
+1 -1
skid/blog/blog4.sk
··· 23 23 24 24 !note("psst, I've added an atom feed to the blog ↓ though if you're reading this when it came out you probably already know about it") 25 25 }}} 26 - &blog_nav_no_next("blog3.html" "blog5.html") 26 + &blog_nav("blog3.html" "blog5.html") 27 27 28 28 &bsky_comments("closed") 29 29
+223
skid/blog/blog5.sk
··· 1 + !insert("//style.sk") 2 + !insert("blog_templates.sk") 3 + &insert("//templates_generic.sk") 4 + 5 + !blog_post( 5 "Sourcemod Development on Linux" "2026-01-27" "Sharing some scripts and things I know") 6 + {{{ 7 + Over the last couple of months I have been asked by a couple of people how to or how I do a specific thing when developing Sourcemods on Linux, so it's probably a good idea to just aggregate parts of those conversations into a short little something I can just point people towards ~~instead of talking to them because I **HATE THEM** 😈~~. 8 + 9 + Going over how to use your system if you are a Linux newbie is outside of the scope of this post, but, if you are looking to install a Linux on your computer [Fedora KDE](https://fedoraproject.org/kde/) and [openSUSE Tumbleweed](https://get.opensuse.org/tumbleweed/?type=desktop)( What I'm using now ) are my go-to recommendations. They are very up-to-date and stable and KDE Plasma is the best Windows-like desktop environment in my opinion. Debian based distros have outdated packages( This becomes an issue with WINE ) and Mint's desktop, Cinnamon is still X11 only so I don't like to recomend those. 10 + 11 + I can only cover things I have done so if something isn't here, Sorry. I'll try to update this post if info becomes inaccurate or if I have something more to share. Comments will also be open if anyone wants to add or ask something. 12 + 13 + ## .desktop Files 14 + 15 + This is only tangentially related but you will probably want to make these for any apps you download off Github or ones that were made for Windows. They make it so the program defined in them is can be used as a default program when double clicking a file, shown in the application launcher and have the icon on the panel be correct. 16 + 17 + They are text files that look like this: 18 + 19 + !file_preview("📝 Jotter - 📄 vpkedit.desktop (Read-Only)" "toml") 20 + {{{ 21 + [Desktop Entry] 22 + Categories=Development 23 + Comment[en_US]=Source Engine VPK viewer. 24 + Comment=Source Engine VPK viewer. 25 + # Executable or script 26 + Exec=/home/tholp/bin/vpkedit 27 + Icon=/home/tholp/.local/share/applications/icons/vpkdeit.png 28 + Name[en_US]=VPKedit 29 + Name=VPKedit 30 + StartupNotify=false 31 + Terminal=false 32 + Type=Application 33 + }}} 34 + 35 + and are placed in `~/.local/share/applications` 36 + 37 + ## Programming 38 + 39 + Protip: link your compiled binaries to your game folder so you don't have top copy it everytime, ( with `ln -s <compiled>.so <gamebindir>.so` or drag and drop in Dolphin for KDE ) 40 + 41 + ### QT Creator 42 + 43 + IDE is really up to you but I have found myself to prefer Qt Creator (fast, free, not Chromium..) so thats what I'll be detailing. 44 + 45 + VPC of course, does not generate project files for QTC so we do have to do the importing ourselves. 46 + 47 + 1. Go to *File* `>` *New Project* `>` *Import Project* `>` *Import Existing Project*, choose the root of your code repo. Remember what you name the project. 48 + 49 + !blog_image_asis("/res/blog/5/QTC_Import.png" "Project import dialog, Project name is 'sdk13' and the location ends with '/source-sdk-2013'" "Example") 50 + 51 + 2. Click next, add `*.vpc;` to the file filter if you'd like and press apply filters. Click next. 52 + 3. Deselect adding the project files to version control. Finish. 53 + 4. Open your new project if it didn't do it for you already, Select *Projects* on the left sidebar 54 + 5. Remove the only build step, add a *Custom Process Step*, for its command browse to your build script 55 + 56 + !blog_image_asis("/res/blog/5/QTC_Buildsteps.png" "Project build steps panel, it has the 'buildallprojects' script in the sdk/src folder as its only step" "Something like this") 57 + 58 + 6. Check if it works with ctrl+B 59 + 60 + 7. Now we need to tell Qt Creator where to look for includes for this project, go to the root of the your repo in a terminal and run `find . -type d | sed 's/^.\{2\}//' > (projectname).includes` you need to replace *(projectname)* with what you named your project so in this example it'd be `find . -type d | sed 's/^.\{2\}//' > sdk13.includes`. 61 + 62 + If you open that file up it has every folder listed, if you care to, remove all the `.git` entries. 63 + 64 + Syntax highlighting should now work for the project. This method is easy but a bit overzealous, so paths that appear correct are not to VPC, you probably just need to prepend the folder the header is in to the #include 65 + 66 + if more files get added to the repo though means other than you doing it in QT Creator you have to let it know by right clicking the project folder on the right and *Edit Files* and check the new ones 67 + 68 + !blog_image_asis("/res/blog/5/QTC_EditFiles.png" "Project tree sidepanel in QT Creator, mousing over the Edit Files button" " " ) 69 + 70 + 71 + ### Shaders 72 + Funnily enough, the main road block to shader compilation on Linux is not compiling the shaders, but the fact that the script that tells the compiler what to compile is written in batch. I've written a small drop in replacement bash script specifically for this post as the CMake one used for Deathmatch Classic: Refragged is not mine to share yet and most people just use VPC anyway. 73 + 74 + Put these files in the src root of your project (Same place as the materialsystem folder) 75 + 76 + !tabs("blog5_shaders" "📝 Jotter - 📂 src" "buildshaders" "shaders.Dockerfile") {{{ 77 + !tab_content("blog5_shaders" "buildshaders") {{{ 78 + ```bash 79 + #\!/bin/bash 80 + cd $(dirname $0) 81 + 82 + function compileList() { 83 + pushd materialsystem/stdshaders > /dev/null 84 + 85 + local file=$1 86 + local version=$2 87 + local regex="^\w+\.fxc$" 88 + 89 + while read line; do # loop over line that looks right 90 + if [[ $line =~ $regex ]]; then 91 + wine ./shadercompile_bin/ShaderCompile.exe -ver $version -shaderpath . $line 2>/dev/null 92 + local return=$? 93 + if [[ $return -ne 0 ]]; then 94 + exit $return 95 + fi 96 + echo "Compiled $line" 97 + fi 98 + done < $file 99 + 100 + popd > /dev/null 101 + } 102 + 103 + # Check if we arent running in a container already 104 + # if you want to chain this into an existing thing as-is just export this beforehand 105 + # OR if you have d3dcompiler_43 and d3dcompiler_47 installed in winetricks already you can remove this if block 106 + if [[ $(printenv -- "buildingshaders") -ne 1 ]]; then 107 + 108 + 109 + podman run --rm -v $(pwd):/work:rw -w /work \ 110 + --env "buildingshaders=1" \ 111 + --env "TERM=$TERM" -t \ # colors in output 112 + "$(podman build -t tholpshadercontainer:latest -q --file shaders.Dockerfile .)" $0 # Run with the container defined by shaders.Dockerfile, make it if it doesn't exist already 113 + exit $? 114 + fi 115 + 116 + # Add text files( relative to materialsystem/stdshaders/ ) with their versions here 117 + compileList stdshader_dx9_20b.txt 20b 118 + compileList stdshader_dx9_30.txt 30 119 + ``` 120 + }}} 121 + 122 + !tab_content("blog5_shaders" "shaders.Dockerfile") {{{ 123 + ```dockerfile 124 + FROM registry.gitlab.steamos.cloud/steamrt/sniper/sdk 125 + 126 + ENV DEBIAN_FRONTEND=noninteractive 127 + ENV WINEPREFIX=/.wine 128 + 129 + RUN apt-get update 130 + RUN apt-get install -y wine winetricks 131 + RUN rm -rf /var/lib/apt/lists 132 + 133 + RUN winetricks -q d3dcompiler_43 134 + RUN winetricks -q d3dcompiler_47 135 + ``` 136 + }}} 137 + }}} 138 + 139 + ### Debugging 140 + 141 + Attaching a debugger the normal way when the attachee process is running in a container (like the Steam Runtime) is basically useless, the SRT comes with gdbserver inside to make this not painful. 142 + In your mod's launch options on steam set it to `GAME_DEBUGGER="gdbserver 127.0.0.1:12345" %command% <other args>`. 143 + 144 + You can skip all this script stuff and the launch argument and attach a debugger normally if you are not testing your game/mod through Steam. Though I recomend you do run it in Steam as that is how people will be playing. 145 + 146 + #### gdb 147 + Go to the folder of the executable (either the SDK's or your app's if you have one). Make a file named `debug` containing this: 148 + ```bash 149 + #\!/bin/bash 150 + cd $(dirname $0) 151 + # swap out hl2_linux64 if needed 152 + echo "set sysroot /proc/$(pidof hl2_linux64)/root" > .gbdinit 153 + echo "target remote 127.0.0.1:12345" >> .gbdinit 154 + gdb ./hl2_linux64 -x .gbdinit 155 + ``` 156 + Now when launching a window wont spawn, you'll need to start the `debug` script from earlier. Press enter and use the command `c` to start the program, once you get to the title screen press ctrl+c and type the command `file hl2_linux64`, should spew a bunch of symbols it read from. !note("sometimes this randomly won't work and I have no idea why. Sorry..") 157 + 158 + From here just look up how to use gdb, but the basics are ctrl+c to break and `c` to continue, `b whatever.cpp:line` or `b class::function` to set a break point.`info b` to list break points and `del #breakpointnum` to delete one. `bt` for the current stack trace and `f #` to jump to a stack frame in that trace. `info local` to print local variables, `p varname` to print a variable (you could use `p *pointer` to de-reference) `q` to quit. 159 + 160 + #### cgdb 161 + 162 + cgdb is the same as gdb but it has an always displayed source file pane on the top half of the terminal. If thats important to you just replace the `gdb` at the end of the above script with `cgdb` to use it instead. 163 + 164 + #### Debugging in your IDE 165 + 166 + This is really up to you to figure out the specifics for your IDE but searching "How to debug remote in (IDE)" should get you most of the way there. They all use gdb as a backend AFAIK so any options are likely analogous to whats written to `.gdbinit` inside the script above. 167 + 168 + ## Assets 169 + 170 + ### Downloading the Windows version of the SDK 171 + 172 + Unfortunately, there are still some tools that are Windows only with no (mature) alternatives so you will need to use Windows apps under wine to do somethings. 173 + 174 + 1. Go to the SDK's properties page in Steam by right clicking it in your library. 175 + 2. Compatibility > Toggle on "Force the use of a specific Steam Play compatibility tool". 176 + 3. Select one of the Proton entries. 177 + 4. Right click on the SDK in your library again. Manage > Browse local files. 178 + 5. Copy this folder out to somewhere Steam won't touch it, a new folder on your desktop or in your home folder or wherever. 179 + 6. Turn off the compatibility check box from before. 180 + 181 + ### Modeling & Animating 182 + 183 + Setting up Blender with all the Source plugins should be the same as it would be on Windows, however last I checked Crowbar does not work under wine, so we will be using `studiomdl.exe` from the [Windows version of the SDK](#Downloading_the_Windows_version_of_the_SDK) instead. 184 + 185 + Compiling models can be done in the command line with something like `wine studiomdl.exe -game "Z:/home/tholp/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/sourcemods/dmcr" "Z:/home/tholp/Documents/dmcr-content-src/mdls/modelsrc/Items/Ammo Pickups/Bullets/bullets_large.qc"` and it will write the compiled model to the game folder. 186 + 187 + ### VPK Packing & Viewing 188 + [VPKEdit](https://github.com/craftablescience/VPKEdit) is the best program for this with previews for textures, models, and text files and read/write capabilities for a weirdly high number of pack formats for things other than Source. 189 + 190 + ### Converting Textures 191 + Your options are [VTF Forge](https://github.com/Trico-Everfire/VTF-Forge) and [MareTF](https://github.com/craftablescience/MareTF). They are what you would expect though MareTF cannot create VTFs in its GUI as of writing, only through it's CLI (e.g. `maretf create texture.png --format DXT5`). 192 + 193 + ### Mapping 194 + While [RatHammer](https://github.com/nmalthouse/rathammer) and [Source Radiant](https://github.com/erysdren/SourceRadiant) do exist they are very new projects and are not up to par with Hammer as of yet, they also have different workflows to Hammer (RatHammer being ground-up and SourceRadiant being based on NetRadiant-custom), still check them out or keep an eye on them though. 195 + 196 + What will you will probably want to do is use Hammer++ in wine. You will need a relatively up-to-date version of wine for it to work well so distro choice matters unless you are willing to figure out containers. AFAIK, as of writing any Debian based distro won't work ( notably Mint ) unless they provide an updated wine package. 197 + 198 + Install Hammer++ to your [Windows copy of the SDK](#Downloading_the_Windows_version_of_the_SDK), remember when setting up a game configuration Linux paths in wine are prefixed with `Z:\\` (e.g. `/home/tholp/` in wine is `Z:\\home\\tholp\\`). 199 + 200 + Issues I've had first hand ( tested with wine-11.0-rc4 ): 201 + * The face edit window starts open, you just have to select a face with it to be able to close it. 202 + * Material browser is a bit broken but still usable ( The material name text does not render ) 203 + * Viewports will some times not display anything if they are not focused 204 + * The object properties window will not have a close button in the window decoration until resized 205 + * Performance and stability can vary a bit each wine update 206 + 207 + I only really map test rooms so there may be an obvious thing I've missed but this overall seems to work fine. 208 + 209 + #### Discord RPC 210 + If you like having Discord presence in hammer you can bridge the RPC pipe from inside wine to your host system using [this](https://github.com/0e4ef622/wine-discord-ipc-bridge) 211 + and having your !nobr("Hammer++.desktop") point instead to a script that looks like this: 212 + 213 + ```bash 214 + #\!/bin/bash 215 + wine /home/tholp/.wine/drive_c/windows/winediscordipcbridge.exe& #exits when attached proc exits 216 + wine /home/tholp/Desktop/tf2sdkwinbin/bin/x64/hammerplusplus.exe 217 + ``` 218 + 219 + }}} 220 + &blog_nav("blog4.html" "blog6.html") 221 + &bsky_comments("placeholder") 222 + 223 + &copy_footer()
+36
skid/blog/blog6.sk
··· 1 + &insert("//style.sk") 2 + !insert("blog_templates.sk") 3 + 4 + !blog_post( 6 "It's Raining?" "2026-01-27" "I talk about a website update &amp; the new year.") {{{ 5 + Ok, stop for a second. 6 + 7 + I don't want to alarm you in case you haven't noticed, but it has begun to rain and it's getting my website all wet :(. 8 + 9 + To be a bit more serious: I had a lot of fun writing the new background over about a week, was a nice change of pace just to write something to look pretty and not be concerned with anything else. It's pretty simplistic but shouldn't be too hard to extend into a more fleshed out thing more akin to something like my main inspiration, *The Power Toy*, if I wanted. It still isn't exactly how I want it but I've been tweaking it enough for now, I'll likely silently update the rain to behave different later. 10 + 11 + I decided to write it in Rust for wasm32 after getting frustrated with JavaScript while making a quick prototype. It's code ~~is available on my Tangled repo if you care~~ I feel like I have issues with my tangled knot every time I want to push to it, so the repos on it are unavailable for now but hopefully I can get that fixed soon. 12 + 13 + I've also been thinking about making a "toys" section on the main page of the site to put a more featureful interactive version of the rain along with another dumb idea I have, but that probably wont happen for a while. 14 + 15 + ## 2025 16 + For whatever reason I was very introspective in 2025, I've taken to tracking some of my less desired intrusive thoughts and trying to figure out why I do and think things the way I do. 17 + 18 + I had a whole thing written about a certain though process here but it was hard to follow and doesn't matter, but basically I realized my reason for continuing to live is no longer the "What else am I gonna do" that I had for years prior, so that's nice. 19 + 20 + I think my main goal for this year is to just get distracted less and grow some discipline, I am very rarely in a flow nowadays and I feel everything takes me long than it should just from the gaps of me not actually doing anything while I'm sat down to work on things. The previous post was started more than a month ago and I probably could've had it out before new year's if I didn't check Discord or scroll Bluesky or Reddit every 20 minutes. This all may sound a bit self loathing but I'm optimistic about rectifying it. 21 + 22 + Thats all for now, later. 23 + 24 + !blog_songblurb_bright( "Denial You Win Again" "The Buttertones" "Jazzhound" "/res/albumart/buttertones-jazzhound.jpg" "#DDDDDD") 25 + {{{ 26 + I've realized a motif in my life is that the vast majority of problems I have are not caused by an external force or me trying and failing, but instead by ignoring it and making no attempt at all. A habit I'd like to break. 27 + 28 + It's always been my move but I've looked away. 29 + }}} 30 + 31 + }}} 32 + &blog_nav_no_next("blog5.html" "blog7.html") 33 + 34 + &bsky_comments("closed") 35 + 36 + &copy_footer()
+65 -32
skid/blog/blog_templates.sk
··· 2 2 3 3 !template(blog_nav prev next) 4 4 {{{ 5 - <div align="center" height="1.5em" class="blog_nav"> 5 + <div align="center" height="1.5em" class="blog_nav downpour_collide"> 6 6 7 7 [previous]([[prev]]) 8 8 ··· 19 19 20 20 !template(blog_nav_no_next prev next) 21 21 {{{ 22 - <div align="center" height="1.5em" class="blog_nav"> 22 + <div align="center" height="1.5em" class="blog_nav downpour_collide"> 23 23 24 24 [previous]([[prev]]) 25 25 ··· 38 38 39 39 !template(blog_post number title date desc) 40 40 {{{ 41 + &downpour() 41 42 &page_title("Blog #[[number]]: [[title]]") 42 43 &basic_embed("Blog #[[number]]: [[title]]" "[[desc]]") 43 44 ··· 56 57 57 58 </header> 58 59 59 - <div align="center" class="blog_container"> 60 + <div align="center" class="blog_container downpour_collide"> 60 61 <!-- <sub>#[[number]]</sub>--> 61 62 !colored("#[[number]]" #0F0) 62 63 ··· 72 73 [[{}]] 73 74 </div> 74 75 </div> 76 + 75 77 <script> 76 78 function linkize_headers() 77 79 { 78 - // var contents = document.getElementById("blog_contents"); 79 - // var htwo = contents.getElementsByTagName("h2"); 80 - // var hthree = contents.getElementsByTagName("h3"); 81 - var htwo = document.querySelectorAll("#blog_contents > h2"); 82 - var hthree = document.querySelectorAll("#blog_contents > h3"); 83 - for (var i = 0; i < htwo.length; i++) 84 - { 85 - var a = document.createElement("a"); 86 - a.setAttribute("id", htwo[i].innerHTML); 87 - a.setAttribute("href", "#" + htwo[i].innerHTML); 88 - a.setAttribute("class", "blog_section"); 89 - a.innerHTML = "<h2>" + htwo[i].innerHTML + "</h2>"; 90 - htwo[i].parentNode.replaceChild(a, htwo[i]); 80 + const linkize = (elements, tag) => { 81 + for (var i = 0; i < elements.length; i++) 82 + { 83 + var e = elements[i]; 84 + 85 + var section = e.innerHTML.split(' ').join('_'); 86 + section = section.replace("&amp;", "and"); 87 + 88 + var a = document.createElement("a"); 89 + a.setAttribute("id", section); 90 + a.setAttribute("href", "#" + section); 91 + a.setAttribute("class", "blog_section"); 92 + a.innerHTML = "<" + tag + ">" + e.innerHTML + "</" + tag + ">"; 93 + 94 + e.parentNode.replaceChild(a, e); 95 + } 91 96 } 92 - for (var i = 0; i < hthree.length; i++) 93 - { 94 - var a = document.createElement("a"); 95 - a.setAttribute("id", hthree[i].innerHTML); 96 - a.setAttribute("href", "#" + hthree[i].innerHTML); 97 - a.setAttribute("class", "blog_section"); 98 - a.innerHTML = "<h3>" + hthree[i].innerHTML + "</h3>"; 99 - hthree[i].parentNode.replaceChild(a, hthree[i]); 100 - } 97 + 98 + linkize(document.querySelectorAll("#blog_contents > h2"), "h2"); 99 + linkize(document.querySelectorAll("#blog_contents > h3"), "h3"); 100 + linkize(document.querySelectorAll("#blog_contents > h4"), "h4"); 101 101 } 102 102 linkize_headers(); 103 103 </script> ··· 107 107 {{{ 108 108 <figure class="divcenter"> 109 109 <img width="95%" alt="[[alt]]" src="[[imgsrc]]"/> 110 + <figcaption>[[caption]]</figcaption> 111 + </figure> 112 + }}} 113 + 114 + !template(blog_image_asis imgsrc alt caption) 115 + {{{ 116 + <figure class="divcenter"> 117 + <img alt="[[alt]]" src="[[imgsrc]]" style="max-width: 95%"/> 110 118 <figcaption>[[caption]]</figcaption> 111 119 </figure> 112 120 }}} ··· 167 175 168 176 }}} 169 177 178 + !template(blog_songblurb_bright song artist album imgsrc color) 179 + {{{ 180 + 181 + <div class="blog_songblurb" style="background: [[color]];"> 182 + <h2>Song of the Post</h2> 183 + <div class="blog_songblurb_interior blog_songblurb_interior_bright"> 184 + <figure class="blog_songblurb_album"> 185 + <img width="100%" alt="Album cover of [[album]] by [[artist]]" src="[[imgsrc]]"/> 186 + <figcaption>"[[album]]" by [[artist]]</figcaption> 187 + </figure> 188 + <div class="blog_songblurb_text"> 189 + 190 + ### [[artist]] - [[song]] 191 + [[{}]] 192 + 193 + </div> 194 + </div> 195 + </div> 196 + 197 + }}} 198 + 170 199 171 200 <!-- give a list of tab names and put a \!tabcontent in the block for each one --> 172 201 <!-- id needs to be unique for now--> ··· 181 210 <span class="tabheader tabheader_[[id]]" id="tabheader_[[id]]_[[tabname..1]]" data-active="false" onclick="tab_select_e_[[id]](event, '[[tabname..1]]')"> 📄 [[tabname..1]] </span> 182 211 }}} 183 212 </div> 213 + 184 214 <!--contents--> 215 + <div class="tabs"> 185 216 [[{}]] 217 + </div> 186 218 </div> 187 219 188 220 <script> 189 221 190 222 function tab_select_[[id]]() 191 223 { 192 - var tabcontents = document.getElementsByClassName('tabcontent_[[id]]') 193 - for (var i = 0; i < tabcontents.length; i++) 224 + let tabcontents = document.getElementsByClassName('tabcontent_[[id]]') 225 + for (let i = 0; i < tabcontents.length; i++) 194 226 { 195 - tabcontents[i].style.display = 'none'; 227 + tabcontents[i].style.visibility = 'hidden'; 196 228 } 197 - var tabheaders = document.getElementsByClassName('tabheader_[[id]]') 229 + let tabheaders = document.getElementsByClassName('tabheader_[[id]]') 198 230 for (var i = 0; i < tabheaders.length; i++) 199 231 { 200 232 tabheaders[i].setAttribute('data-active', false); ··· 204 236 function tab_select_e_[[id]](event, tabname) 205 237 { 206 238 tab_select_[[id]](tabname); 207 - document.getElementById('tabcontent_[[id]]_' + tabname).style.display = 'block'; 239 + document.getElementById('tabcontent_[[id]]_' + tabname).style.visibility = 'visible'; 208 240 event.currentTarget.setAttribute('data-active', true); 209 241 } 242 + 210 243 211 244 tab_select_[[id]](); 212 - document.getElementById('tabcontent_[[id]]_[[default]]').style.display = 'block'; 245 + document.getElementById('tabcontent_[[id]]_[[default]]').style.visibility = 'visible'; 213 246 document.getElementById('tabheader_[[id]]_[[default]]').setAttribute('data-active', true); 214 247 </script> 215 248 ··· 249 282 {{{ 250 283 <figure class="divcenter"> 251 284 <audio controls> 252 - <source src="[[src]]"> Heres a nickel kid, go download yourself a better browser that can play modern audio formats. 285 + <source src="[[src]]"> Heres a nickel kid, go download yourself a better browser. 253 286 </audio> 254 287 <figcaption>[[caption]]</figcaption> 255 288 </figure>
+2 -1
skid/blog/index.sk
··· 1 1 &insert("//style.sk") 2 2 &insert("blog_templates.sk") 3 3 &page_title("Blog Index") 4 + &downpour() 4 5 5 6 !construction() 6 7 ··· 13 14 </header> 14 15 15 16 !for_each_file_in_group_reverse( post "blog" ) {{{ 16 - <div align="center" style="color: #cac123;" class="blog_container"> 17 + <div align="center" style="color: #cac123;" class="blog_container downpour_collide"> 17 18 <a href="blog!insert("[[post..1]]" "number").html"> 18 19 <h1>!colored("#!insert("[[post..1]]" "number"):" #00FF00) !insert("[[post..1]]" "title")</h1> 19 20 <h4>!insert("[[post..1]]" "date") - !insert("[[post..1]]" "blurb")</h4>
+8 -7
skid/index.sk
··· 1 1 &insert("//style.sk") 2 2 &insert("//templates_generic.sk") 3 3 &page_title("Tholps Site") 4 + &downpour() 4 5 5 6 !template(friend_link link img) 6 7 {{{ ··· 65 66 !construction() 66 67 67 68 <div class="landingblocks"> 68 - <div class="landingblock"> 69 + <div class="landingblock downpour_collide"> 69 70 70 71 ## Links 71 72 ··· 79 80 80 81 Anything not here is either not me, inactive or uninteresting. 81 82 </div> 82 - <div class="landingblock aboutmeblock"> 83 + <div class="landingblock aboutmeblock downpour_collide"> 83 84 84 85 ## About Me 85 86 ··· 99 100 100 101 I have been a part of more projects than listed but I either don't think what I contributed was substantial or I don't like what the project has become. 101 102 </div> 102 - <div class="landingblock"> 103 + <div class="landingblock downpour_collide"> 103 104 104 105 ## Pages 105 106 ··· 114 115 More to come... Maybe. 115 116 </div> 116 117 </div> 117 - <div class="landingblock coolblock"> 118 + <div class="landingblock coolblock downpour_collide"> 118 119 119 120 ## Shoutouts 120 121 ··· 129 130 !friend_link_display( "sites.google.com/view/deathknight/dethknigt" "/res/images/deathknightfavi.png" "dethknigt") 130 131 !friend_link("Flare145.com" "https://flare145.com/favicon.ico") 131 132 !friend_link("Kleadron.net" "https://kleadron.net/favicon.ico") 132 - !friend_link("Hellbie.github.io" "/res/images/missing.webp") 133 + !friend_link("Hellbie.github.io" "https://hellbie.github.io/favico.png") 133 134 !friend_link("KMatter.net" "/res/images/missing.webp") 134 135 !friend_link("Sourdani.dev" "https://sourdani.dev/images/avatar.png") 135 136 !friend_link_display("Bsky.app/profile/mtd.coaxion.games" "https://web-cdn.bsky.app/static/favicon.png" "MTD.Coaxion.games") ··· 139 140 </div> 140 141 </div> 141 142 142 - <img src="/res/images/wave.png" class="landingimg"/> 143 + <!-- <img src="/res/images/wave.png" class="landingimg"/> --> 143 144 144 145 </div> 145 146 <!-- <image src="/res/blog/1/writing.png" class="fullscreen"></img> --> 146 147 147 148 <div class="lowerlanding"> 148 149 149 - <div class="contactblock"> 150 + <div class="contactblock downpour_collide"> 150 151 151 152 # Contact 152 153
+32
skid/templates_generic.sk
··· 65 65 <image class="hardhat" src="/res/images/construction-sign.png"></image> 66 66 </div> 67 67 }}} 68 + 69 + !template(downpour) 70 + {{{ 71 + <fieldset id="downpour_settings"> 72 + <legend>Settings</legend> 73 + <div> 74 + <input type="checkbox" id="downpour_enable" name="downpour_enable" checked /> 75 + <label for="downpour_enable">Enable rain background</label> 76 + </div> 77 + <div> 78 + <input type="checkbox" id="downpour_focus" name="downpour_focus" /> 79 + <label for="downpour_focus">Continue raining without browser focus</label> 80 + </div> 81 + <div> 82 + <input type="number" id="downpour_tps" name="downpour_tps" min="1" max="120" value="60"/> 83 + <label for="downpour_tps">Rain background ticks per second</label> 84 + </div> 85 + <div> 86 + <input type="number" id="downpour_rainrate" name="downpour_rainrate" min="1" max="65535" value="175"/> 87 + <label for="downpour_rainrate">Inverse chance for new rain per tick</label> 88 + </div> 89 + <div> 90 + <input type="range" id="downpour_scale" name="downpour_scale" min="1" max="12" value="3" step="1"/> 91 + <label for="downpour_scale">[ <span id="downpour_scale_display"></span> ] Screen pixels per cell</label> 92 + </div> 93 + <div> 94 + <a id="downpour_close_settings" onclick="document.getElementById('downpour_settings').style.display = 'none';document.getElementById('downpour_open_settings').style.display = 'block';">Close</a> 95 + </div> 96 + </fieldset> 97 + <a id="downpour_open_settings" onclick="document.getElementById('downpour_settings').style.display = 'block';document.getElementById('downpour_open_settings').style.display = 'none';">Open Settings</a> 98 + <script type="module" src="/res/scripts/downpour/glue.js" defer></script> 99 + }}}
+1 -1
skidmark.toml
··· 5 5 [fileGroups] 6 6 7 7 [fileGroups.blog] 8 - files = ["blog/blog1.sk", "blog/blog2.sk", "blog/blog3.sk", "blog/blog4.sk"] 8 + files = ["blog/blog1.sk", "blog/blog2.sk", "blog/blog3.sk", "blog/blog4.sk", "blog/blog5.sk", "blog/blog6.sk"] 9 9 10 10 [fileGroups.misc] 11 11 files = ["index.sk", "blog/index.sk", "404.sk", "sinister.sk", "junk-drawer.sk", "changelog.sk"]