Move from Sourcehut

This commit is contained in:
stefan burke 2024-12-03 01:41:35 +00:00
commit a9c3d5b314
32 changed files with 2140 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use nix

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
_site
.bundle
.direnv
.jekyll-cache
.jekyll-metadata
.nix-gems
.sass-cache
vendor

1
.prettierrc.json Normal file
View file

@ -0,0 +1 @@
{}

1
.ruby-version Normal file
View file

@ -0,0 +1 @@
3.1.2

11
Gemfile Normal file
View file

@ -0,0 +1,11 @@
source "https://rubygems.org"
gem "jekyll"
group :jekyll_plugins do
gem "jekyll-minifier"
end
gem "webrick", "~> 1.7"
gem 'sass-embedded', '1.80.3'

92
Gemfile.lock Normal file
View file

@ -0,0 +1,92 @@
GEM
remote: https://rubygems.org/
specs:
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
bigdecimal (3.1.8)
colorator (1.1.0)
concurrent-ruby (1.3.4)
cssminify2 (2.0.1)
em-websocket (0.5.3)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0)
eventmachine (1.2.7)
execjs (2.9.1)
ffi (1.17.0-x86_64-linux-gnu)
forwardable-extended (2.6.0)
google-protobuf (4.28.2-x86_64-linux)
bigdecimal
rake (>= 13)
htmlcompressor (0.4.0)
http_parser.rb (0.8.0)
i18n (1.14.6)
concurrent-ruby (~> 1.0)
jekyll (4.3.4)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
i18n (~> 1.0)
jekyll-sass-converter (>= 2.0, < 4.0)
jekyll-watch (~> 2.0)
kramdown (~> 2.3, >= 2.3.1)
kramdown-parser-gfm (~> 1.0)
liquid (~> 4.0)
mercenary (>= 0.3.6, < 0.5)
pathutil (~> 0.9)
rouge (>= 3.0, < 5.0)
safe_yaml (~> 1.0)
terminal-table (>= 1.8, < 4.0)
webrick (~> 1.7)
jekyll-minifier (0.1.10)
cssminify2 (~> 2.0)
htmlcompressor (~> 0.4)
jekyll (>= 3.5)
json-minify (~> 0.0.3)
uglifier (~> 4.1)
jekyll-sass-converter (3.0.0)
sass-embedded (~> 1.54)
jekyll-watch (2.2.1)
listen (~> 3.0)
json (2.7.2)
json-minify (0.0.3)
json (> 0)
kramdown (2.4.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (4.0.4)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
mercenary (0.4.0)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (6.0.1)
rake (13.2.1)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
rexml (3.3.8)
rouge (4.4.0)
safe_yaml (1.0.5)
sass-embedded (1.80.3)
google-protobuf (~> 4.28)
rake (>= 13)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
uglifier (4.2.1)
execjs (>= 0.3.0, < 3)
unicode-display_width (2.6.0)
webrick (1.8.2)
PLATFORMS
x86_64-linux
DEPENDENCIES
jekyll
jekyll-minifier
sass-embedded (= 1.80.3)
webrick (~> 1.7)
BUNDLED WITH
2.5.9

5
README.md Normal file
View file

@ -0,0 +1,5 @@
# stefn
stefan burke - software engineer
[stefn.co.uk](https://stefn.co.uk)

30
_config.yml Normal file
View file

@ -0,0 +1,30 @@
plugins:
- jekyll-minifier
collections:
sites:
output: false
jobs:
output: false
pages:
output: true
permalink: /:name/
jekyll-minifier:
exclude: 'game/*'
exclude:
- deploy-neocities
- build-jekyll
- .sass-cache/
- .jekyll-cache/
- README.md
- LICENSE
- gemfiles/
- Gemfile
- Gemfile.lock
- node_modules/
- vendor/bundle/
- vendor/cache/
- vendor/gems/
- vendor/ruby/

28
_data/jobs.yml Normal file
View file

@ -0,0 +1,28 @@
- name: Bandcamp
description: artist-first online music store
role: senior engineer
start: 2019
end: 2024
- name: Bouncy Castle Network
description: booking systems & websites for bouncy castle hirers
role: lead developer
start: 2009
end: 2019
- name: Metro Salvage
description: vehicle recycling and lead generation system
role: technical lead
start: 2011
end: 2012
- name: Sizzle Media
description: web development agency
role: copywriter, web developer
start: 2009
end: 2012
- name: Freelancing
role: copywriter, web developer
start: 2008
end: 2019

26
_data/sites.yml Normal file
View file

@ -0,0 +1,26 @@
- name: bluepitshousingaction.co.uk
href: https://bluepitshousingaction.co.uk
source: https://git.chobble.com/hosted-by-chobble/blue-pits
- name: chobble.com
href: https://chobble.com
source: https://git.chobble.com/chobble/chobble
- name: freeholdcottage.com
href: https://freeholdcottage.com
source: https://git.sr.ht/~stfn/freehold-cottage
- name: newbarnltd.co.uk
href: https://newbarnltd.co.uk
source: https://git.chobble.com/hosted-by-chobble/newbarn
- name: stefn.co.uk
href: https://stefn.co.uk
- name: thisandthatcafe.co.uk
href: https://thisandthatcafe.co.uk
source: https://git.sr.ht/~stfn/this-and-that
- name: veganprestwich.co.uk
href: https://veganprestwich.co.uk
source: https://git.chobble.com/hosted-by-chobble/vegan-prestwich

2
_includes/colour Normal file
View file

@ -0,0 +1,2 @@
{% assign colour = colour | plus:1 %}
colour{{ colour | modulo: 12 }}

133
_includes/style.scss Normal file
View file

@ -0,0 +1,133 @@
@mixin glow($colour) {
color: $colour;
text-shadow: 0 0 3px $colour;
}
* {
box-sizing: border-box;
}
body {
color: #99c2f9;
font-size: 1.6em;
font-family: monospace;
background-color: black;
background-image: radial-gradient(rgba(0, 62, 0, 0.75), black 120%);
background-attachment: fixed;
height: 100vh;
&::after {
content: "";
position: fixed;
pointer-events: none;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: repeating-linear-gradient(
0deg,
rgba(black, 0.15),
rgba(black, 0.15) 1px,
transparent 1px,
transparent 2px
);
}
}
.wrapper {
max-width: 450px;
margin: 30px auto;
padding: 10px 10px 100px;
}
pre {
font-size: 0.5vw;
color: #8fdd8f;
margin: 0 auto;
}
a {
text-decoration: none;
@include glow(#fff);
&:hover {
text-decoration: underline;
}
}
.smaller {
font-size: 0.6em;
}
li {
margin: 0.5rem 0;
}
strong {
text-shadow: 0 0 1px;
}
::selection {
background: #0080ff;
text-shadow: none;
}
// colours taken from https://iamkate.com/data/12-bit-rainbow/
.colour {
&1 {
@include glow(#c1b);
}
&2 {
@include glow(#ed0);
}
&3 {
@include glow(#0bc);
}
&4 {
@include glow(#a35);
}
&5 {
@include glow(#9d5);
}
&6 {
@include glow(#09c);
}
&7 {
@include glow(#c66);
}
&8 {
@include glow(#4d8);
}
&9 {
@include glow(rgb(94, 140, 220));
}
&10 {
@include glow(#e94);
}
&11 {
@include glow(#2cb);
}
&12 {
@include glow(#639);
}
}
.setup {
padding-left: 0;
list-style-type: none;
}
.setup ul {
font-size: 0.8em;
}

23
_layouts/default.html Normal file
View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>{{ page.title }}</title>
<meta
name="description"
content="stefan burke - programmer - manchester, uk"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🤘</text></svg>"
/>
{% capture styles %} {% include style.scss %} {% endcapture %}
<style>
{{ styles | scssify }}
</style>
</head>
<body>
<div class="wrapper">{{ content }}</div>
</body>
</html>

222
cv.html Normal file
View file

@ -0,0 +1,222 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Stefan Burke, Senior Software Engineer, Manchester UK</title>
<style>
body {
padding: 0.5em;
background: #f0ebd8;
color: #0d1321;
font-family: Iowan Old Style, Apple Garamond, Baskerville,
Times New Roman, Droid Serif, Times, Source Serif Pro, serif,
Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
}
article {
max-width: 800px;
margin: 0 auto;
}
h1,
h2,
h3 {
color: #1d2d44;
border-bottom: 1px dotted #748cab;
padding: 1rem 0;
margin: 0;
}
h1 {
font-size: 1.5rem;
}
h2 {
font-size: 1.4rem;
}
h3 {
font-size: 1.2rem;
}
ul {
list-style-type: none;
padding: 0em;
font-weight: bold;
color: #1d2d44;
}
section {
padding: 0.5rem;
border: 1px dotted #748cab;
border-top: 0;
}
section h3 {
padding: 0.5rem;
margin: 0 -0.5rem;
}
a {
color: #1d2d44;
}
a:hover,
a:active {
text-decoration: underline;
}
</style>
</head>
<body>
<article>
<h1>Stefan Burke: Senior Software Engineer</h1>
<p>
Full stack engineer with 17 years' experience building and optimising
large scale web applications in Ruby, C#, PHP and more.
</p>
<p>
he/him //
<a href="https://stefn.co.uk">stefn.co.uk</a> //
<a href="mailto:me@stefn.co.uk">me@stefn.co.uk</a> //
<a
href="https://www.linkedin.com/in/stef-burke/">linkedin.com/in/stef-burke</a>
// Manchester, UK // remote
</p>
<h2>Professional Experience</h2>
<section>
<h3>
2019-24: <a href="https://bandcamp.com">Bandcamp</a> - Online Music
Platform
</h3>
<ul>
<li>Ruby / MySQL / Linux / Cypress / Web</li>
</ul>
<p>
Bandcamp is an online 'fair trade' music store that processed $200+
million in sales in the last year.
</p>
<p>
I was a senior developer on the Growth team and also built tools for the
Support team. My team recently built the slick new mobile web
interface, but my favourite technical accomplishments are probably the
numerous significant performance improvements I found across many
highly-trafficked pages and in gnarly legacy code.
</p>
<p>
I wrote new tools to measure, test, and verify the pages my team was
responsible for. I implemented SEO fixes, measured their effect, and
advised other teams about search engines and performance, using my
know-how from previous jobs.
</p>
<p>
On the Support side, I migrated the team to HelpScout and sped up
their workflows with automations, shortcuts, and new admin pages.
</p>
<p>
I also added an easter egg to the purchase dialog which goes viral
every now and again.
</p>
<h3>
2009-19:
<a href="https://www.bouncycastlenetwork.com">Bouncy Castle Network</a>
- Online Booking System
</h3>
<ul>
<li>ASP.Net / C#, MVC, MSSQL / Entity Framework</li>
</ul>
<p>
I was the technical lead and senior developer for an online booking
system and website provider for the party hire industry, growing from
1 to 1,200 customers across the UK and internationally and processing
6,700+ bookings per day at the peak of the season.
</p>
<p>
I was the public face of the software side of the business from its
inception, working to support the existing client base through new
features while adopting a sales role to bring in new registrations.
</p>
<p>
Along the way I trained new staff members on support, mentored junior
developers, wrote tutorials, hosted marketing seminars, cultivated an
online community of hirers, and built an advanced internal CRM to
allow the business to double in turnover each year to £1m in 2018.
</p>
<h3>2012+: Freelancing</h3>
<ul>
<li>Jekyll, Eleventy, Git, Linux</li>
</ul>
<p>
I build and host my favourite curry cafe's
<a href="https://thisandthatcafe.co.uk/">website</a> and manage
<a href="https://www.facebook.com/ThisAndThatManchester">their
social media.</a>
Nothing too massive, but I get free curry from it.
</p>
<p>
I'm 'the web guy' for my friends and family and host a bunch of sites
for them, and I manage
<a href="https://veganprestwich.co.uk">a local vegan food
recommendations website</a>
with my wife.
</p>
<h3>2010-12: Metro Salvage - Vehicle Recycling Firm</h3>
<ul>
<li>
ASP.Net / C#, MSSQL, PHP, Wordpress, Joomla, Magento, IIS / Windows
Server, Photoshop
</li>
</ul>
<p>
I was the sole developer at one of the North West's largest vehicle
recycling yards, creating an intranet and online system to sell leads
and parts to other recyclers around the UK through a network of
websites and widgets. The system was generating £200k+ of new income
after year one.
</p>
<h3>2007-10: Reach BCS - Web Agency</h3>
<ul>
<li>Classic ASP, VB, ASP.Net / C#, SQL, Access, PHP, HTML, CSS</li>
</ul>
<p>
I joined as a copywriter but soon adopted an internal security role
(read: breaking everyone else's code), finally ending as a full-stack
developer. I built custom e-commerce systems, scrapers, image
generators, CMS systems, blogs, directories, and internal business
automation tools.
</p>
<h3>2004-7: Lancaster University, Linguistics</h3>
<p>
I gained infamy within the IT department for routing around the
firewall and facilitating mass file-sharing, resulting in meetings
with the dean. I've since forgotten everything I learned about
Linguistics.
</p>
<h3>2000+: Recreational Programming</h3>
<ul>
<li>Linux, Docker, Git, Nix, DNS</li>
</ul>
<p>
I host my own '<a href="https://stefn.co.uk/setup">private cloud</a>'
in NixOS and Debian, which takes a lot of tinkering. Programming is a
hobby to me as well as a career - I find it therapeutic to get my
headphones on, turn up some good tunes, and get stuck into building
something awesome. Having said that, I once had a nightmare about
Scala.
</p>
</section>
<h2>Me</h2>
<p>
I live with my wife and dog in the suburbs of Manchester. I spend my free
time listening to very heavy metal music, headbanging at concerts, eating
out, keeping healthy, noodling on instruments, and playing video games.
</p>
<p>
Thanks for considering me!
</p>
</article>
</body>
</html>

52
default.nix Normal file
View file

@ -0,0 +1,52 @@
{ pkgs ? import <nixpkgs> {} }:
let
src = ./.;
env = pkgs.bundlerEnv {
name = "stefn-co-uk";
inherit (pkgs) ruby;
gemfile = ./Gemfile;
lockfile = ./Gemfile.lock;
gemset = ./gemset.nix;
};
in
pkgs.stdenv.mkDerivation {
name = "stefn-co-uk";
src = builtins.filterSource
(path: type: !(builtins.elem (baseNameOf path) [
"_site"
".jekyll-cache"
".git"
"node_modules"
"result"
"vendor"
]))
src;
nativeBuildInputs = with pkgs; [
ruby_3_3
html-minifier
];
configurePhase = ''
export HOME=$TMPDIR
mkdir -p _site
'';
buildPhase = ''
echo "Building site with Jekyll..."
JEKYLL_ENV=production ${env}/bin/jekyll build --source . --destination _site --trace
echo "Minifying HTML..."
html-minifier --input-dir _site --output-dir _site --collapse-whitespace --file-ext html
'';
installPhase = ''
echo "Creating output directory..."
mkdir -p $out
echo "Copying site files..."
cp -r _site/* $out/
'';
}

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

90
game/baddy.js Normal file
View file

@ -0,0 +1,90 @@
class Baddy extends Character {
baseSpeed = 1;
ticksLeft = 0;
stuckLeft = 5;
previousDirection = null;
render() {
splatter.beginPath();
splatter.arc(
this.x + this.width / 2,
this.y + this.height / 2,
this.width,
0,
Math.PI * 2
);
splatter.fillStyle = "black";
splatter.fill();
super.render();
}
speed() {
let distanceFromPlayer = calculateDistance(this, player);
if (distanceFromPlayer < 3) return this.baseSpeed * 3;
if (distanceFromPlayer < 5) return this.baseSpeed * 2;
return this.baseSpeed;
}
kill() {
this.dead = Date.now();
this.x = null;
this.y = null;
}
move() {
if (this.dead && Date.now() - this.dead > 2000) {
this.dead = null;
this.resetPosition(5);
}
if (this.x == null || this.y == null || this.dead) return;
let speed = this.speed();
if (calculateDistance(this, player) <= 5 && this.stuckLeft <= 0) {
this.direction.x = this.x == player.x ? 0 : this.x > player.x ? -1 : 1;
this.direction.y = this.y == player.y ? 0 : this.y > player.y ? -1 : 1;
this.ticksLeft = 0;
} else {
if (this.ticksLeft <= 0) {
this.direction = pickRandom(DIRECTIONS);
this.ticksLeft = Math.ceil(10 * BLOCK_SIZE * Math.random());
}
this.ticksLeft -= 1;
this.stuckLeft -= 1;
}
const oldX = this.x;
this.x = oldX + this.direction.x * speed;
const oldY = this.y;
this.y = oldY + this.direction.y * speed;
let stuck = false;
if (
isWall(this.left, this.top) ||
isWall(this.right, this.top) ||
isWall(this.left, this.bottom) ||
isWall(this.right, this.bottom)
) {
this.x = oldX;
this.y = oldY;
this.direction = pickRandom(DIRECTIONS);
this.stuckLeft = 30;
stuck = true;
}
if (stuck && Math.random() - 0.5 > 0.2) {
explode(this);
}
// this.debugMessage = {
// x: this.direction.x,
// y: this.direction.y,
// s: speed,
// t: this.stuckLeft,
// r: this.ticksLeft,
// u: stuck,
// };
}
}
function pickRandom(array) {
return array.sort(function (a, b) {
return 0.5 - Math.random();
})[0];
}

81
game/bomb.js Normal file
View file

@ -0,0 +1,81 @@
let bombs = [];
let lastDropped = null;
class Bomb {
radius = 0;
constructor(x, y) {
let now = Date.now();
this.x = x;
this.y = y;
this.dropped = now;
if (!lastDropped || now - lastDropped > 1000) {
bombs.push(this);
score -= 3;
lastDropped = Date.now();
}
}
tick() {
this.radius += 6;
if (this.radius >= 200) {
bombs = bombs.filter((b) => b != this);
return;
}
baddies.forEach((baddy) => {
if (this.intersects(baddy)) {
explode(baddy);
baddy.kill();
score += 1;
}
});
}
intersects(baddy) {
let x = Math.abs(this.x - (baddy.x - baddy.width / 2));
let y = Math.abs(this.y - (baddy.y - baddy.height / 2));
if (x > baddy.width / 2 + this.radius) {
return false;
}
if (y > baddy.height / 2 + this.radius) {
return false;
}
if (x <= baddy.width / 2) {
return true;
}
if (y <= baddy.height / 2) {
return true;
}
let cornerDistance_sq =
(x - baddy.width / 2) ^ (2 + (y - baddy.height / 2)) ^ 2;
return cornerDistance_sq <= (this.radius ^ 2);
}
lightenDarkenColor(col, amt) {
let num = parseInt(col, 16);
let r = (num >> 16) + amt;
let b = ((num >> 8) & 0x00ff) + amt;
let g = (num & 0x0000ff) + amt;
let newColor = g | (b << 8) | (r << 16);
return newColor.toString(16);
}
drawCircle(radius, color) {
fx.fillStyle = color;
fx.beginPath();
fx.arc(this.x, this.y, radius, 0, 2 * Math.PI);
fx.closePath();
fx.fill();
}
render() {
for (let i = this.radius; i >= 5; i -= 5) {
this.drawCircle(i, i % 2 == 0 ? "#2de2e6" : "#035ee8");
}
}
}

131
game/constants.js Normal file
View file

@ -0,0 +1,131 @@
const BLOCK_SIZE = 32;
const DIRECTIONS = [
{ x: 1, y: 1 },
{ x: 1, y: 0 },
{ x: 1, y: -1 },
{ x: 0, y: 1 },
{ x: 0, y: -1 },
{ x: -1, y: 1 },
{ x: -1, y: 0 },
{ x: -1, y: -1 },
{ x: 1, y: 1 },
{ x: 0, y: 1 },
{ x: -1, y: 1 },
{ x: 1, y: 0 },
{ x: -1, y: 0 },
{ x: 1, y: -1 },
{ x: 0, y: -1 },
{ x: -1, y: -1 },
{ x: 0.5, y: 1 },
{ x: 0.5, y: 0 },
{ x: 0.5, y: -1 },
{ x: 0, y: 1 },
{ x: 0, y: -1 },
{ x: -0.5, y: 1 },
{ x: -0.5, y: 0 },
{ x: -0.5, y: -1 },
{ x: 0.5, y: 1 },
{ x: 0, y: 1 },
{ x: -0.5, y: 1 },
{ x: 0.5, y: 0 },
{ x: -0.5, y: 0 },
{ x: 0.5, y: -1 },
{ x: 0, y: -1 },
{ x: -0.5, y: -1 },
];
let Character = class {
mass = 80;
baseSpeed = 6;
yke = 0;
gpe = 0;
debugMessage = "";
dead = false;
startedMovement = {
left: null,
right: null,
};
constructor(x, y, width, height, color) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.color = color;
}
render() {
if (this.dead) return;
fg.fillStyle = this.color;
fg.fillRect(this.left, this.top, this.width, this.height);
fg.strokeStyle = "black";
fg.strokeRect(this.left, this.y, this.width, this.height);
}
get left() {
return this.x;
}
get right() {
return this.x + this.width;
}
get top() {
return this.y;
}
get middle() {
return this.y + this.height / 2;
}
get bottom() {
return this.y + this.height;
}
get speed() {
return this.baseSpeed;
}
speed(direction, onGround, increase = false) {
let multiplier = (this.startedMovement[direction] || 0) + 1;
if (increase && onGround) this.startedMovement[direction] = multiplier;
let speed = Math.floor(
this.baseSpeed +
(multiplier != 1
? this.baseSpeed * (multiplier / 20) * (!onGround ? 0.3 : 1)
: 0)
);
return speed;
}
movement() {
let newBottom = this.bottom - this.yke;
let down = !isWall(this.left, newBottom) && !isWall(this.right, newBottom);
let newLeft = this.left - this.speed("left", !down);
let newRight = this.right + this.speed("right", !down);
return {
down: down,
up: !isWall(this.left, this.top) && !isWall(this.right, this.top),
left: !isWall(newLeft, this.top) && !isWall(newLeft, this.middle),
right: !isWall(newRight, this.top) && !isWall(newRight, this.middle),
};
}
resetPosition(minDistanceFromPlayer) {
let minDistance = minDistanceFromPlayer * BLOCK_SIZE;
let emptySpaces = [];
for (let row = 0; row < currentLevel.length; row++) {
for (let col = 0; col < currentLevel[0].length; col++) {
let x = player.x - col * BLOCK_SIZE;
let y = player.y - row * BLOCK_SIZE;
if (
(x > minDistance || x < -minDistance) &&
(y > minDistance || y < -minDistance) &&
currentLevel[row][col] === "0"
) {
emptySpaces.push([col, row]);
}
}
}
let randIndex = Math.floor(Math.random() * emptySpaces.length);
let position = emptySpaces[randIndex];
this.x =
position[0] * BLOCK_SIZE +
Math.floor(Math.random() * (BLOCK_SIZE - this.width));
this.y =
position[1] * BLOCK_SIZE +
Math.floor(Math.random() * (BLOCK_SIZE - this.height));
}
};

13
game/debug.js Normal file
View file

@ -0,0 +1,13 @@
debugText.font = "8 Arial black";
function debug() {
clearCanvas(debugText);
baddies.forEach((baddy) => {
if (baddy.debugMessage)
debugText.fillText(
JSON.stringify(baddy.debugMessage),
baddy.x + baddy.width + 2,
baddy.y + baddy.height + 2
);
});
}

62
game/explosion.js Normal file
View file

@ -0,0 +1,62 @@
let explosions = [];
let explosionPoints = [];
class Explosion {
animationDuration = 1000;
constructor(x, y, color) {
this.startX = this.x = x;
this.startY = this.y = y;
this.color = color;
this.speed = {
x: -5 + Math.random() * 10,
y: -5 + Math.random() * 10,
};
this.radius = 5 + Math.random() * 5;
this.life = 30 + Math.random() * 10;
this.remainingLife = this.life;
this.startTime = Date.now();
explosions.push(this);
}
render() {
if (this.remainingLife > 0 && this.radius > 0) {
fx.beginPath();
fx.arc(this.startX, this.startY, this.radius, 0, Math.PI * 2);
fx.fillStyle = this.color;
fx.fill();
if (Math.random() - 0.5 > 0.2) {
splatter.beginPath();
splatter.arc(this.startX, this.startY, this.radius, 0, Math.PI * 2);
splatter.fillStyle = this.color;
splatter.fill();
}
// Update the particle's location and life
this.remainingLife--;
this.radius -= 0.25;
this.startX += this.speed.x;
this.startY += this.speed.y;
}
}
}
function explode(character) {
for (let i = 0; i < (character.width * character.height) / 5; i++) {
new Explosion(character.x, character.y, character.color);
}
}
function renderExplosions() {
for (let i = 0; i < explosions.length; i++) {
explosions[i].render();
// Simple way to clean up if the last particle is done animating
if (i === explosions.length - 1) {
let percent =
(Date.now() - explosions[i].startTime) /
explosions[i].animationDuration;
if (percent > 1) {
explosions = [];
}
}
}
}

64
game/game.html Normal file
View file

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>splatter bounce</title>
</head>
<body>
<div
id="viewport"
style="position: relative; width: 512px; height: 512px; background: black"
>
<canvas
id="splatter"
style="position: absolute; width: 512px; height: 512px"
width="512"
height="512"
></canvas>
<canvas
id="bombs"
style="position: absolute; width: 512px; height: 512px"
width="512"
height="512"
></canvas>
<canvas
id="bg"
style="position: absolute; width: 512px; height: 512px"
width="512"
height="512"
></canvas>
<canvas
id="fg"
style="position: absolute; width: 512px; height: 512px"
width="512"
height="512"
></canvas>
<canvas
id="overlay"
style="position: absolute; width: 512px; height: 512px"
width="512"
height="512"
></canvas>
<canvas
id="debug_text"
style="position: absolute; width: 512px; height: 512px"
width="512"
height="512"
></canvas>
</div>
<ul>
<li>
<strong>fps:</strong>
<input type="text" value="60" type="number" id="debug_fps" />
</li>
</ul>
<script src="constants.js"></script>
<script src="level.js"></script>
<script src="player.js"></script>
<script src="baddy.js"></script>
<script src="bomb.js"></script>
<script src="explosion.js"></script>
<script src="game.js"></script>
<script src="debug.js"></script>
</body>
</html>

184
game/game.js Normal file
View file

@ -0,0 +1,184 @@
const bg = document.getElementById("bg").getContext("2d");
const splatter = document.getElementById("splatter").getContext("2d");
const fg = document.getElementById("fg").getContext("2d");
const fx = document.getElementById("bombs").getContext("2d");
const overlay = document.getElementById("overlay").getContext("2d");
const debugText = document.getElementById("debug_text").getContext("2d");
[bg, splatter, fg, fx, overlay, debugText].forEach((l) => {
l.height = 512;
l.width = 512;
});
overlay.fillStyle = "white";
function isWall(x, y) {
let yAvg = Math.floor(y / BLOCK_SIZE);
let xAvg = Math.floor(x / BLOCK_SIZE);
let row = currentLevel[yAvg];
return ((row && row[xAvg]) || "1") !== "0";
}
function overlapping(obj1, obj2) {
let horizontal = obj1.left <= obj2.right && obj2.left <= obj1.right;
let vertical = obj1.top <= obj2.bottom && obj2.top <= obj1.bottom;
return horizontal && vertical;
}
function calculateDistance(obj1, obj2) {
let a = obj1.x - obj2.x;
let b = obj1.y - obj2.y;
return Math.sqrt(a * a + b * b) / BLOCK_SIZE;
}
const GAME_DURATION = 30;
let previousRemaining = null,
previousScore = null,
score = 0,
highscore = null;
function renderScore() {
let seconds = (Date.now() - lastGameStarted) / 1000;
let remaining = GAME_DURATION - Math.floor(seconds);
if (previousRemaining == remaining) return;
previousRemaining = remaining;
clearCanvas(overlay);
overlay.font = "30px Sans-Serif";
if (highscore) {
overlay.fillText(`${score}`, 20, bg.height - 40);
overlay.fillText(`top: ${highscore}`, 20, bg.height - 10);
} else {
overlay.fillText(`${score}`, 20, bg.height - 30);
}
if (remaining > GAME_DURATION - 5 && previousScore) {
let minus = GAME_DURATION - remaining;
let fontSize = 72 - minus * 4;
let offset = 60 + minus * 5;
overlay.font = `${fontSize}px Sans-Serif`;
overlay.fillText(`you got ${previousScore}!`, offset, offset);
} else if (remaining <= 0) {
previousScore = score;
if (score > highscore) highscore = score;
score = 0;
lastGameStarted = Date.now();
restart();
} else if (remaining < 10) {
overlay.font = "60px Sans-Serif";
overlay.fillText(remaining, overlay.width / 2, overlay.height / 2);
}
}
let lastFrameRender = Date.now(),
lastGameStarted = null,
frameCount = 0;
const token = new Character(null, null, 20, 20, "yellow");
function restart() {
let level = pickRandom(levels);
lastGameStarted = Date.now();
currentLevel = level.map((l) => l.split(""));
token.resetPosition(5);
explode(token);
explode(player);
baddies.forEach((b) => explode(b));
baddies = [];
debug;
let colours = [
"#2de2e6",
"#035ee8",
"#f6019d",
"#d40078",
"#9700cc",
"#2de2e6",
"#035ee8",
"#f6019d",
"#d40078",
"#9700cc",
];
for (let i = 0; i < colours.length; i++) {
let baddy = new Baddy(null, null, 10, 10, colours[i], i);
baddies.push(baddy);
baddy.resetPosition(5);
}
drawLevel();
main();
}
function main() {
frameCount++;
requestAnimationFrame(main);
let fps = parseInt(document.getElementById("debug_fps").value) || 60;
let fpsInterval = 1000 / fps;
let now = Date.now();
let elapsed = now - lastFrameRender;
if (elapsed > fpsInterval) {
lastFrameRender = now - (elapsed % fpsInterval);
bombs.forEach((b) => b.tick());
input();
player.gravity();
baddies.forEach((baddy) => baddy.move());
if (overlapping(player, token)) {
score += 20;
explode(token);
token.resetPosition(5);
}
baddies.forEach((baddy) => {
if (overlapping(player, baddy)) {
score--;
explode(baddy);
baddy.resetPosition(5);
}
});
if (score < 0) score = 0;
clearCanvas(fg);
clearCanvas(fx);
shiftCanvas(splatter);
renderExplosions();
bombs.forEach((b) => b.render());
player.render();
token.render();
baddies.forEach((baddy) => baddy.render());
renderScore();
debug();
}
}
function shiftCanvas(canvas) {
canvas.putImageData(
canvas.getImageData(0, -1, canvas.width, canvas.height - 1),
0,
0
);
canvas.clearRect(canvas.width, 0, -1, canvas.height - 1);
}
let currentLevel = null;
let baddies = [];
window.onload = function () {
overlay.font = "30px Sans-Serif";
overlay.fillText(`you are the white square!`, 20, 40);
overlay.fillText(`yellow square: +20`, 20, 80);
overlay.fillText(`bad guy: -1`, 20, 120);
overlay.fillText(`move: wasd / arrows`, 20, 160);
overlay.fillText(`bomb: space (-3)`, 20, 200);
overlay.fillText(`rounds are 30 secs`, 20, 240);
overlay.fillText(`starting in 10 secs!`, 20, 280);
setTimeout(restart, 10000);
};

BIN
game/game.tar.gz Normal file

Binary file not shown.

72
game/level.js Normal file
View file

@ -0,0 +1,72 @@
const levels = [
[
"0111111111111110",
"0000000000000000",
"1110000000000000",
"0000000000001111",
"0000111000000000",
"0000000000011110",
"1000000000000000",
"1000000000111111",
"1000000000011000",
"0110000000000000",
"0000000010000110",
"0001111111100000",
"0000000000000000",
"0000000000010000",
"0000001111111000",
"0000000000000000",
],
[
"0000000000000000",
"0000000000000000",
"0000000011111000",
"0000000000000001",
"0000111000000000",
"0000000000011110",
"0011111000000000",
"0000000000000111",
"0000001111001000",
"0011000000000010",
"0000000000000000",
"0001111001111000",
"0000000000000000",
"0001110000000000",
"0000001111000000",
"0111000000000000",
],
[
"0000000000000000",
"0111111110000000",
"0000000000000000",
"0000000001111001",
"1111100110000000",
"0000000001110010",
"0000011100000000",
"1000000000000000",
"0000111000000000",
"0011100000111010",
"0000001100000000",
"0000000001001000",
"0000111000000000",
"0010000001111000",
"0000000000110000",
"0110000000001100",
],
];
function clearCanvas(layer) {
layer.clearRect(0, 0, layer.width, layer.height);
}
function drawLevel() {
clearCanvas(bg);
bg.fillStyle = "black";
for (let row = 0; row < currentLevel.length; row++) {
for (let col = 0; col < currentLevel[0].length; col++) {
if (currentLevel[row][col] === "1") {
bg.fillRect(col * BLOCK_SIZE, row * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}
}
}
}

127
game/player.js Normal file
View file

@ -0,0 +1,127 @@
const player = new Character(0, 0, 20, 20, "white");
player.isDoubleJumping = false;
player.lastTouchedGround = null;
player.pausedMidAirJump = null;
player.gravity = function () {
this.y -= Math.floor(this.yke);
let mass = keyDown(MOVEMENT_KEYS.down) ? this.mass * 4 : this.mass;
let gravity = 9.8 / 1000000;
let height = bg.height - this.height - this.y / BLOCK_SIZE;
this.gpe = mass * gravity * height;
this.yke -= this.gpe;
let movement = this.movement();
if (!movement.up) {
if (this.yke >= 0) {
this.yke = -0.5;
this.y += 1;
}
} else {
if (!movement.down) {
this.lastTouchedGround = Date.now();
this.isDoubleJumping = false;
this.pausedMidAirJump = null;
if (this.yke <= 0) {
this.yke = 0;
let newY = this.y + this.height;
let bl = isWall(this.left, newY);
let br = isWall(this.right, newY);
if (bl || br) {
this.y -= (this.y % BLOCK_SIZE) - (BLOCK_SIZE - this.height);
}
}
}
}
};
const MOVEMENT_KEYS = {
left: ["ArrowLeft", "a"],
right: ["ArrowRight", "d"],
up: ["UpArrow", "w"],
down: ["ArrowDown", "s"],
fire: [" "],
};
const ALL_KEYS = [];
Object.keys(MOVEMENT_KEYS).forEach((key) => {
MOVEMENT_KEYS[key].forEach((button) => {
ALL_KEYS.push(button);
});
});
let keysDown = {};
addEventListener("keydown", function (event) {
if (!ALL_KEYS.includes(event.key)) return;
event.preventDefault();
keysDown[event.key] = true;
});
addEventListener("keyup", function (event) {
if (!ALL_KEYS.includes(event.key)) return;
if (
!player.pausedMidAirJump &&
player.yke != 0 &&
MOVEMENT_KEYS.up.includes(event.key)
) {
player.pausedMidAirJump = Date.now();
}
event.preventDefault();
delete keysDown[event.key];
});
function keyDown(search) {
return Object.keys(keysDown).filter((k) => search.includes(k)).length > 0;
}
function input() {
let movingLeft = false,
movingRight = false;
const movement = player.movement();
if (keyDown(MOVEMENT_KEYS.left)) {
if (movement.left) {
player.x -= player.speed("left", !movement.down, true);
movingLeft = true;
}
} else if (keyDown(MOVEMENT_KEYS.right)) {
if (movement.right) {
player.x += player.speed("right", !movement.down, true);
movingRight = true;
}
}
if (keyDown(MOVEMENT_KEYS.fire)) {
new Bomb(player.x, player.y);
}
if (!movingLeft) delete player.startedMovement.left;
if (!movingRight) delete player.startedMovement.right;