Move from Sourcehut
This commit is contained in:
commit
a9c3d5b314
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
_site
|
||||
.bundle
|
||||
.direnv
|
||||
.jekyll-cache
|
||||
.jekyll-metadata
|
||||
.nix-gems
|
||||
.sass-cache
|
||||
vendor
|
1
.prettierrc.json
Normal file
1
.prettierrc.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
.ruby-version
Normal file
1
.ruby-version
Normal file
|
@ -0,0 +1 @@
|
|||
3.1.2
|
11
Gemfile
Normal file
11
Gemfile
Normal 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
92
Gemfile.lock
Normal 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
5
README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# stefn
|
||||
|
||||
stefan burke - software engineer
|
||||
|
||||
[stefn.co.uk](https://stefn.co.uk)
|
30
_config.yml
Normal file
30
_config.yml
Normal 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
28
_data/jobs.yml
Normal 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
26
_data/sites.yml
Normal 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
2
_includes/colour
Normal file
|
@ -0,0 +1,2 @@
|
|||
{% assign colour = colour | plus:1 %}
|
||||
colour{{ colour | modulo: 12 }}
|
133
_includes/style.scss
Normal file
133
_includes/style.scss
Normal 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
23
_layouts/default.html
Normal 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
222
cv.html
Normal 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
52
default.nix
Normal 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
BIN
favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
90
game/baddy.js
Normal file
90
game/baddy.js
Normal 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
81
game/bomb.js
Normal 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
131
game/constants.js
Normal 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
13
game/debug.js
Normal 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
62
game/explosion.js
Normal 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
64
game/game.html
Normal 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
184
game/game.js
Normal 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
BIN
game/game.tar.gz
Normal file
Binary file not shown.
72
game/level.js
Normal file
72
game/level.js
Normal 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
127
game/player.js
Normal 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;
|
||||
|
||||