Browse Source

basic framework added from roguelike

master
Inderjit Gill 1 year ago
parent
commit
5349a90981
26 changed files with 7591 additions and 1 deletions
  1. +9
    -0
      .gitignore
  2. +127
    -0
      Cargo.lock
  3. +23
    -0
      Cargo.toml
  4. +49
    -1
      README.md
  5. +13
    -0
      benches/bench.rs
  6. +26
    -0
      index.html
  7. +5802
    -0
      package-lock.json
  8. +15
    -0
      package.json
  9. BIN
      research/tileset.pdn
  10. BIN
      research/tileset.png
  11. +38
    -0
      serve.go
  12. +71
    -0
      src/controller.rs
  13. +36
    -0
      src/entity.rs
  14. +87
    -0
      src/game.rs
  15. +297
    -0
      src/geometry.rs
  16. +127
    -0
      src/lib.rs
  17. +26
    -0
      src/sprite.rs
  18. +46
    -0
      src/state.rs
  19. +103
    -0
      src/text.rs
  20. +71
    -0
      src/units.rs
  21. BIN
      web/img/tileset.png
  22. +246
    -0
      web/js/GLRenderer.js
  23. +259
    -0
      web/js/Matrix.js
  24. +4
    -0
      web/js/bootstrap.js
  25. +103
    -0
      web/js/index.js
  26. +13
    -0
      webpack.config.js

+ 9
- 0
.gitignore View File

@@ -0,0 +1,9 @@
/dist
/target
**/*.rs.bk

/node_modules/
/web/wasm_tetris*

serve.exe
serve

+ 127
- 0
Cargo.lock View File

@@ -0,0 +1,127 @@
[[package]]
name = "dtoa"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "itoa"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "proc-macro2"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "quote"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "serde"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "serde_derive"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.14.4 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "serde_json"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.69 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "syn"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "unicode-xid"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "wasm-bindgen"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"wasm-bindgen-macro 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "wasm-bindgen-backend"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.22 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.14.4 (registry+https://github.com/rust-lang/crates.io-index)",
"wasm-bindgen-shared 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "wasm-bindgen-macro"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.14.4 (registry+https://github.com/rust-lang/crates.io-index)",
"wasm-bindgen-backend 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "wasm-bindgen-shared"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"serde 1.0.69 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.69 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "wasm_tetris"
version = "0.1.0"
dependencies = [
"wasm-bindgen 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
]

[metadata]
"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
"checksum itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c069bbec61e1ca5a596166e55dfe4773ff745c3d16b700013bcaff9a6df2c682"
"checksum proc-macro2 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "effdb53b25cdad54f8f48843d67398f7ef2e14f12c1b4cb4effc549a6462a4d6"
"checksum quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e44651a0dc4cdd99f71c83b561e221f714912d11af1a4dff0631f923d53af035"
"checksum serde 1.0.69 (registry+https://github.com/rust-lang/crates.io-index)" = "210e5a3b159c566d7527e9b22e44be73f2e0fcc330bb78fef4dbccb56d2e74c8"
"checksum serde_derive 1.0.69 (registry+https://github.com/rust-lang/crates.io-index)" = "dd724d68017ae3a7e63600ee4b2fdb3cad2158ffd1821d44aff4580f63e2b593"
"checksum serde_json 1.0.22 (registry+https://github.com/rust-lang/crates.io-index)" = "84b8035cabe9b35878adec8ac5fe03d5f6bc97ff6edd7ccb96b44c1276ba390e"
"checksum syn 0.14.4 (registry+https://github.com/rust-lang/crates.io-index)" = "2beff8ebc3658f07512a413866875adddd20f4fd47b2a4e6c9da65cd281baaea"
"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
"checksum wasm-bindgen 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "e38789b21dd56c9b172efb263d635ba5203d4f6c65a7010a26fba1aaa8c55bda"
"checksum wasm-bindgen-backend 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "3360d814e6dfc2d1de99d20e224310cb2a640e5749133d7e7e6be2f8d401378a"
"checksum wasm-bindgen-macro 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "00cae1d10619d295b817c40cb5bb2fa7969d5abf39c74ceac4dd7e8b285376f0"
"checksum wasm-bindgen-shared 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "12fc3992a9356fb2eec8133e6336996cea864ebfe21660793ecd4a9d522b2985"

+ 23
- 0
Cargo.toml View File

@@ -0,0 +1,23 @@
[package]
name = "wasm_tetris"
version = "0.1.0"
authors = ["Inderjit Gill <inderjit.gill@gmail.com>"]

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2.8"

[profile.release]
# Include function names in the `.wasm` for better debugging and
# profiling. Comment this out if you're trying to create the smallest `.wasm`
# binaries you can.
debug = false

# # Uncomment these lines to create smaller `.wasm` binaries, at the cost of
# # longer compile times.
codegen-units = 1
incremental = false
lto = true
opt-level = "z"

+ 49
- 1
README.md View File

@@ -1,2 +1,50 @@
# tetris
Tetris
======

## Overview

A Game

## ensure that the correct toolchain is installed
```sh
$ rustup update
$ rustup install nightly
$ rustup target add wasm32-unknown-unknown --toolchain nightly
$ cargo +nightly install wasm-bindgen-cli

$ npm install
$ npm run build:server
```

## during dev
```sh
$ npm run build:dev:wasm
$ npm run build:dev
$ npm run serve:dev
```

can now visit http://localhost:8080


## for 'release'
```sh
$ npm run build:release
$ npm run serve:release
```

can now visit http://localhost:3000


# misc. notes

## graphics
- origin is in the bottom left
- rendering order for a square is: bl, br, tl, tr

## file structure
/dist -> destination directory for webpack output (npm run build)
index.html
/src -> rust source for top-level program + wasm declarations
/target -> destination directory for rust
/web -> destination for wasm-bindgen generated files (*.js, *.ts, *.wasm)
/web/js -> js files

+ 13
- 0
benches/bench.rs View File

@@ -0,0 +1,13 @@
#![feature(test)]

extern crate test;
extern crate wasm_tetris;

#[bench]
fn universe_ticks(b: &mut test::Bencher) {
let mut universe = wasm_game_of_life::Universe::new();

b.iter(|| {
universe.tick();
});
}

+ 26
- 0
index.html View File

@@ -0,0 +1,26 @@
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
<style>
body {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: monospace;
white-space: pre;
background: #333333;
}
#render-canvas {
background: #101010;
padding: 1em;
}
</style>
</head>
<body>
<canvas id="render-canvas"></canvas>
<script src='bootstrap.js'></script>
</body>
</html>

+ 5802
- 0
package-lock.json
File diff suppressed because it is too large
View File


+ 15
- 0
package.json View File

@@ -0,0 +1,15 @@
{
"scripts": {
"serve:dev": "webpack-dev-server",
"serve:release": "serve",
"build:dev": "webpack --mode=development --progress",
"build:dev:wasm": "cargo +nightly build --target wasm32-unknown-unknown && wasm-bindgen target/wasm32-unknown-unknown/debug/wasm_tetris.wasm --out-dir web",
"build:release": "cargo +nightly build --release --target wasm32-unknown-unknown && wasm-bindgen target/wasm32-unknown-unknown/release/wasm_tetris.wasm --out-dir web && webpack --mode=production --progress && cp index.html dist/index.html",
"build:server": "go build serve.go"
},
"devDependencies": {
"webpack": "4.9.2",
"webpack-cli": "3.0.8",
"webpack-dev-server": "3.1.4"
}
}

BIN
research/tileset.pdn View File


BIN
research/tileset.png View File

Before After
Width: 512  |  Height: 512  |  Size: 13KB

+ 38
- 0
serve.go View File

@@ -0,0 +1,38 @@
package main

import (
"fmt"
"net/http"
"regexp"
)

/*
serve web file from: /web (for images used as textures)
serve everything else from: /dist (generated with npm run build:prod)
*/

var wasmFile = regexp.MustCompile("\\.wasm$")

func maxAgeHandler(seconds int, h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Cache-Control", fmt.Sprintf("max-age=%d, public, must-revalidate, proxy-revalidate", seconds))

ruri := r.RequestURI
if wasmFile.MatchString(ruri) {
w.Header().Set("Content-Type", "application/wasm")
}

h.ServeHTTP(w, r)
})
}

func main() {
fs := http.FileServer(http.Dir("web"))
http.Handle("/web/", http.StripPrefix("/web/", fs))

fs = http.FileServer(http.Dir("dist"))
http.Handle("/", maxAgeHandler(0, fs))

fmt.Printf("Serving localhost:3000\n")
http.ListenAndServe(":3000", nil)
}

+ 71
- 0
src/controller.rs View File

@@ -0,0 +1,71 @@

#[derive(Clone, Copy)]
pub enum ControllerAction {
Down,
Up,
}

#[derive(Clone, Copy)]
pub enum ControllerButton {
Left,
Right,
Up,
Down,
A,
B,
Start,
Select,
}

// a virtual controller, used to normalise player input into a standard set of controls
#[derive(Default)]
pub struct Controller {
pub up: bool,
pub down: bool,
pub left: bool,
pub right: bool,

pub a: bool,
pub b: bool,

pub start: bool,
pub select: bool,
}

impl Controller {
pub fn new() -> Controller {
Controller {
up: false,
down: false,
left: false,
right: false,

a: false,
b: false,

start: false,
select: false,
}
}

pub fn input(&mut self, button: ControllerButton, action: ControllerAction) {
match button {
ControllerButton::Left => self.left = self.bool_from_action(action),
ControllerButton::Right => self.right = self.bool_from_action(action),
ControllerButton::Up => self.up = self.bool_from_action(action),
ControllerButton::Down => self.down = self.bool_from_action(action),
ControllerButton::A => self.a = self.bool_from_action(action),
ControllerButton::B => self.b = self.bool_from_action(action),
ControllerButton::Start => self.start = self.bool_from_action(action),
ControllerButton::Select => self.select = self.bool_from_action(action),
};
}

fn bool_from_action(&self, action: ControllerAction) -> bool {
match action {
ControllerAction::Up => false,
ControllerAction::Down => true,
}
}

}

+ 36
- 0
src/entity.rs View File

@@ -0,0 +1,36 @@
use units::*;
use sprite::{Sprite};

#[derive(Debug, Clone, Copy)]
pub enum Colour {
Red,
Green,
Blue,
Yellow,
Orange,
Purple,
Brown,
Grey,
}

// player, creatures, walls?, treasure, objects etc
#[derive(Debug, Clone, Copy)]
pub struct Entity {
pub pos: Tile2D,

pub colour: Colour,
pub sprite: Sprite,

pub is_barrier: bool,
}

impl Entity {
pub fn new(x: i32, y: i32, colour: Colour, sprite: Sprite, is_barrier: bool) -> Entity {
Entity {
pos: Tile2D { x, y },
colour,
sprite,
is_barrier,
}
}
}

+ 87
- 0
src/game.rs View File

@@ -0,0 +1,87 @@
use log;
use controller::{Controller, ControllerButton, ControllerAction};
use state::State;
use geometry::Geometry;

pub enum GameMode {
Playing,
Editing,
}

pub struct Game {
delta: f32,

mode: GameMode,

controller: Controller,
state: State,
geometry: Geometry,
}

impl Game {
pub fn new(
canvas_width: f32,
canvas_height: f32,
view_frame_width: i32, // view-frame width
view_frame_height: i32, // view-frame height
tileset_texture_width: i32, // tileset texture width
tileset_texture_height: i32, // tileset texture height
) -> Game {
Game {
delta: 0.0,

mode: GameMode::Playing,

controller: Controller::new(),

state: State::new(view_frame_width, view_frame_height),

geometry: Geometry::new(canvas_width,
canvas_height,
view_frame_width,
view_frame_height,
tileset_texture_width,
tileset_texture_height),
}
}

pub fn init(&mut self) {
self.state.init();
}

pub fn input(&mut self, button: ControllerButton, action: ControllerAction) {
self.controller.input(button, action);
}

pub fn tick(&mut self, delta: f32) -> bool {
// always followed by a render
// returns true if tick should be called again
//
self.delta = delta;

match self.mode {
GameMode::Playing => self.state.tick_playing(&self.controller),
GameMode::Editing => tick_editing(),
}
}

// use the current gamestate to update geometry
pub fn update_geometry(&mut self) {
self.state.update_geometry(&mut self.geometry);
self.state.update_geometry_debug(&mut self.geometry);
}

pub fn geo_len(&self) -> usize {
self.geometry.geo.len()
}

pub fn geo_ptr(&self) -> *const f32 {
self.geometry.geo.as_ptr() as *const f32
}
}

fn tick_editing() -> bool {
log("IMPLEMENT: tick_editing");

false
}

+ 297
- 0
src/geometry.rs View File

@@ -0,0 +1,297 @@
use units::*;
use entity::*;
use sprite::{Sprite, get_sprite_location};
use text::{sprite_location_from_char};

struct UV {
u: f32,
v: f32,
}

pub struct Geometry {
pub tile_width: f32, // pixels
pub tile_height: f32,

pub sprite_u_unit: f32, // size of each sprite in u,v normalized co-ordinates
pub sprite_v_unit: f32,

pub geo: Vec<f32>,
}

impl Geometry {

pub fn new(
canvas_width: f32,
canvas_height: f32,
view_frame_width: i32, // view-frame width
view_frame_height: i32, // view-frame height
tileset_texture_width: i32, // tileset texture width
tileset_texture_height: i32, // tileset texture height
) -> Geometry {

// the size of each sprite in the tileset (in pixels)
let sprite_width: i32 = 16;
let sprite_height: i32 = 16;

Geometry {
tile_width: canvas_width / (view_frame_width as f32),
tile_height: canvas_height / (view_frame_height as f32),

sprite_u_unit: 1.0 / ((tileset_texture_width / sprite_width) as f32),
sprite_v_unit: 1.0 / ((tileset_texture_height / sprite_height) as f32),

geo: Vec::with_capacity(4096),
}
}

pub fn clear(&mut self) {
self.geo.clear();
}

pub fn push(&mut self, x: f32, y: f32, r: f32, g: f32, b: f32, a: f32, u: f32, v: f32) {
self.geo.append(&mut vec![x, y, r, g, b, a, u, v]);
}

// called once at the start of drawing a line
pub fn push_line(&mut self, p1: Vec2D, p2: Vec2D, thickness: f32, c: Col) {
// keep thickness relative to tile_width just like the other thickness parameters
//
let hw = (thickness * self.tile_width) / 2.0; // half-width

let (t1, t2, t3, t4) = self.get_sprite_uv(Sprite::DebugFilled);

let n = normal(p1, p2);
let op = opposite_normal(n);

self.dup();
self.push(p1.x + (hw * n.x), p1.y + (hw * n.y), 0.0, 0.0, 0.0, 1.0, 0.0, 0.0);

self.push(p1.x + (hw * n.x), p1.y + (hw * n.y), c.r, c.g, c.b, c.a, t1.u, t1.v);
self.push(p1.x + (hw * op.x), p1.y + (hw * op.y), c.r, c.g, c.b, c.a, t2.u, t2.v);
self.push(p2.x + (hw * n.x), p2.y + (hw * n.y), c.r, c.g, c.b, c.a, t3.u, t3.v);
self.push(p2.x + (hw * op.x), p2.y + (hw * op.y), c.r, c.g, c.b, c.a, t4.u, t4.v);
}

// todo: push_path/push_lines


pub fn centre_point_from_tile(&self, tile: Tile2D) -> Vec2D {
let w = self.tile_width as f32;
let h = self.tile_height as f32;

let x = ((tile.x as f32) * w) + (w / 2.0);
let y = ((tile.y as f32) * h) + (h / 2.0);

Vec2D { x, y }
}

pub fn point_from_tile(&self, tile: Tile2D) -> Vec2D {
let w = self.tile_width as f32;
let h = self.tile_height as f32;

let x = (tile.x as f32) * w;
let y = (tile.y as f32) * h;

Vec2D { x, y }
}

// renders a rectangle over the TileFrame
// frame should be in screen co-ordinates
pub fn push_filled_tile(&mut self, tile: Tile2D, c: Col) {
let w = self.tile_width;
let h = self.tile_height;

let x1 = (tile.x as f32) * w;
let y1 = (tile.y as f32) * h;
let x2 = ((tile.x + 1) as f32) * w;
let y2 = ((tile.y + 1) as f32) * h;

self.push_filled(x1, y1, x2, y2, c.r, c.g, c.b, c.a);
}

pub fn push_outline_tile(&mut self, tile: Tile2D, thickness: f32, c: Col) {
// thickness of 1.0 == tile_width, tile_height
let w = self.tile_width;
let h = self.tile_height;

let x1 = (tile.x as f32) * w;
let y1 = (tile.y as f32) * h;
let x2 = ((tile.x + 1) as f32) * w;
let y2 = ((tile.y + 1) as f32) * h;

self.push_outline(x1, y1, x2, y2, thickness, c.r, c.g, c.b, c.a);
}

pub fn push_sprite(
&mut self,
sprite: Sprite,
pos_x: i32, // tilespace
pos_y: i32,
colour: Colour,
) {
// need local variable because they're used in calls to add_vert
let w = self.tile_width;
let h = self.tile_height;

let x = (pos_x as f32) * w;
let y = (pos_y as f32) * h;

let (r, g, b, a) = match colour {
Colour::Red => (1.0, 0.0, 0.0, 1.0),
Colour::Green => (0.0, 1.0, 0.0, 1.0),
Colour::Blue => (0.0, 0.0, 1.0, 1.0),
Colour::Yellow => (1.0, 1.0, 0.0, 1.0),
Colour::Orange => (1.0, 0.25, 0.0, 1.0),
Colour::Purple => (1.0, 0.0, 1.0, 1.0),
Colour::Brown => (0.5, 0.25, 0.0, 1.0),
Colour::Grey => (0.5, 0.5, 0.5, 1.0),
};

let (t1, t2, t3, t4) = self.get_sprite_uv(sprite);

// building a degenerate triangle:
// push last vertex then the 1st vertex followed by the 1st vertex again
self.dup();
self.push(x, y, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0);

self.push(x, y, r, g, b, a, t1.u, t1.v);
self.push(x + w, y, r, g, b, a, t2.u, t2.v);
self.push(x, y + h, r, g, b, a, t3.u, t3.v);
self.push(x + w, y + h, r, g, b, a, t4.u, t4.v);
}

pub fn push_text(
&mut self,
text: &str,
pos: Tile2D, // in tilespace screen-coordinates
col: Col,
) {
let mut v2 = self.point_from_tile(pos);

for c in text.chars() {
v2 = self.push_char(c, v2, col);
}
}

fn push_char(&mut self, c: char, pos: Vec2D, col: Col) -> Vec2D {
// need local variable because they're used in calls to add_vert
let w = self.tile_width / 2.0;
let h = self.tile_height;

let (t1, t2, t3, t4) = self.get_char_uv(c);

// building a degenerate triangle:
// push last vertex then the 1st vertex followed by the 1st vertex again
self.dup();
self.push(pos.x, pos.y, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0);

self.push(pos.x, pos.y, col.r, col.g, col.b, col.a, t1.u, t1.v);
self.push(pos.x + w, pos.y, col.r, col.g, col.b, col.a, t2.u, t2.v);
self.push(pos.x, pos.y + h, col.r, col.g, col.b, col.a, t3.u, t3.v);
self.push(pos.x + w, pos.y + h, col.r, col.g, col.b, col.a, t4.u, t4.v);

Vec2D {
x: pos.x + w,
y: pos.y,
}
}

fn push_filled(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, r: f32, g: f32, b: f32, a: f32) {
let (t1, t2, t3, t4) = self.get_sprite_uv(Sprite::DebugFilled);

self.dup();
self.push(x1, y1, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0);

self.push(x1, y1, r, g, b, a, t1.u, t1.v);
self.push(x2, y1, r, g, b, a, t2.u, t2.v);
self.push(x1, y2, r, g, b, a, t3.u, t3.v);
self.push(x2, y2, r, g, b, a, t4.u, t4.v);
}

fn push_outline(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, thickness: f32, r: f32, g: f32, b: f32, a: f32) {
let w = self.tile_width;
let h = self.tile_height;

let tw = w * thickness;
let th = h * thickness;

let (t1, t2, t3, t4) = self.get_sprite_uv(Sprite::DebugFilled);

self.dup();
self.push(x1, y1, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0);

// left bar
self.push(x1, y1, r, g, b, a, t1.u, t1.v);
self.push(x1 + tw, y1 + th, r, g, b, a, t2.u, t2.v);
self.push(x1, y2, r, g, b, a, t3.u, t3.v);
self.push(x1 + tw, y2 - th, r, g, b, a, t4.u, t4.v);

// top bar (tr corner)
self.push(x2, y2, r, g, b, a, t1.u, t1.v);
self.push(x2 - tw, y2 - th, r, g, b, a, t2.u, t2.v);

// right bar (br corner)
self.push(x2, y1, r, g, b, a, t3.u, t3.v);
self.push(x2 - tw, y1 + th, r, g, b, a, t4.u, t4.v);

// bottom bar
self.push(x1, y1, r, g, b, a, t1.u, t1.v);
self.push(x1 + tw, y1 + th, r, g, b, a, t2.u, t2.v);
}

// duplicate the last geometry point
fn dup(&mut self) {
let len = self.geo.len();

let x: f32;
let y: f32;
let r: f32;
let g: f32;
let b: f32;
let a: f32;
let u: f32;
let v: f32;
{
x = self.geo[len - 8];
y = self.geo[len - 7];
r = self.geo[len - 6];
g = self.geo[len - 5];
b = self.geo[len - 4];
a = self.geo[len - 3];
u = self.geo[len - 2];
v = self.geo[len - 1];
}

self.push(x, y, r, g, b, a, u, v);
}

fn get_sprite_uv(&self, sprite: Sprite) -> (UV, UV, UV, UV) {
let sprite_location = get_sprite_location(sprite);

let u_unit = self.sprite_u_unit;
let v_unit = self.sprite_v_unit;

let u = (sprite_location.col as f32) * u_unit;
let v = (sprite_location.row as f32) * v_unit;

(UV{u, v: 1.0 - (v + v_unit)},
UV{u: u + u_unit, v: 1.0 - (v + v_unit)},
UV{u, v: 1.0 - v},
UV{u: u + u_unit, v: 1.0 - v})
}

fn get_char_uv(&self, c: char) -> (UV, UV, UV, UV) {
let sprite_location = sprite_location_from_char(c);

let u_unit = self.sprite_u_unit / 2.0;
let v_unit = self.sprite_v_unit;

let u = (sprite_location.col as f32) * u_unit;
let v = (sprite_location.row as f32) * v_unit;

(UV{u, v: 1.0 - (v + v_unit)},
UV{u: u + u_unit, v: 1.0 - (v + v_unit)},
UV{u, v: 1.0 - v},
UV{u: u + u_unit, v: 1.0 - v})
}
}

+ 127
- 0
src/lib.rs View File

@@ -0,0 +1,127 @@
#![feature(proc_macro, wasm_custom_section, wasm_import_module)]
#![cfg_attr(feature = "cargo-clippy", allow(many_single_char_names, too_many_arguments))]

extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

pub mod units;
pub mod sprite;
pub mod text;
pub mod entity;
pub mod game;
pub mod state;
pub mod geometry;
pub mod controller;

use controller::{ControllerButton, ControllerAction};
use game::Game;

#[wasm_bindgen(js_namespace = console)]
extern "C" {
fn log(s: &str);
}

#[wasm_bindgen]
pub struct KeyEventReturn {
call_tick: bool,
prevent_default: bool,
}

#[wasm_bindgen]
impl KeyEventReturn {
pub fn call_tick(&self) -> bool {
self.call_tick
}

pub fn prevent_default(&self) -> bool {
self.prevent_default
}
}

#[wasm_bindgen]
pub struct GameBridge {
game: Game,
}

#[wasm_bindgen]
impl GameBridge {
#[wasm_bindgen(constructor)]
pub fn new(
canvas_width: f32,
canvas_height: f32,
view_frame_width: i32, // view-frame width
view_frame_height: i32, // view-frame height
tileset_texture_width: i32, // tileset texture width
tileset_texture_height: i32, // tileset texture height
) -> GameBridge {
GameBridge {
game: Game::new(canvas_width, canvas_height,
view_frame_width, view_frame_height,
tileset_texture_width, tileset_texture_height)
}
}

pub fn init(&mut self) {
self.game.init();
}

pub fn event_key_down(&mut self, key: String) -> KeyEventReturn {
let mut prevent_default = true;
match key.as_ref() {
"ArrowLeft" => self.game.input(ControllerButton::Left, ControllerAction::Down),
"ArrowRight" => self.game.input(ControllerButton::Right, ControllerAction::Down),
"ArrowUp" => self.game.input(ControllerButton::Up, ControllerAction::Down),
"ArrowDown" => self.game.input(ControllerButton::Down, ControllerAction::Down),
"a" => self.game.input(ControllerButton::A, ControllerAction::Down),
"z" => self.game.input(ControllerButton::B, ControllerAction::Down),
"Enter" => self.game.input(ControllerButton::Start, ControllerAction::Down),
"Shift" => self.game.input(ControllerButton::Select, ControllerAction::Down),
_ => prevent_default = false,
}

// return true if we should call tick
KeyEventReturn {
call_tick: true,
prevent_default,
}
}

pub fn event_key_up(&mut self, key: String) -> KeyEventReturn {
let mut prevent_default = true;
match key.as_ref() {
"ArrowLeft" => self.game.input(ControllerButton::Left, ControllerAction::Up),
"ArrowRight" => self.game.input(ControllerButton::Right, ControllerAction::Up),
"ArrowUp" => self.game.input(ControllerButton::Up, ControllerAction::Up),
"ArrowDown" => self.game.input(ControllerButton::Down, ControllerAction::Up),
"a" => self.game.input(ControllerButton::A, ControllerAction::Up),
"z" => self.game.input(ControllerButton::B, ControllerAction::Up),
"Enter" => self.game.input(ControllerButton::Start, ControllerAction::Up),
"Shift" => self.game.input(ControllerButton::Select, ControllerAction::Up),
_ => prevent_default = false,
}

// return true if we should call tick
KeyEventReturn {
call_tick: false,
prevent_default,
}
}

pub fn tick(&mut self, delta: f32) -> bool {
self.game.tick(delta)
}

// use the current gamestate to update geometry
pub fn update_geometry(&mut self) {
self.game.update_geometry();
}

pub fn geo_len(&self) -> usize {
self.game.geo_len()
}

pub fn geo_ptr(&self) -> *const f32 {
self.game.geo_ptr()
}
}

+ 26
- 0
src/sprite.rs View File

@@ -0,0 +1,26 @@
#[derive(Debug, Clone, Copy)]
pub struct SpriteLocation {
// location of sprite on tileset bitmap
// units are given in tiles (16x16 pixels)
pub row: i32,
pub col: i32,
}

#[derive(Debug, Clone, Copy)]
pub enum Sprite {
Block,
DebugChecker,
DebugBlank,
DebugFilled,
Debug4Corners,
}

pub fn get_sprite_location(sprite: Sprite) -> SpriteLocation {
match sprite {
Sprite::Block => SpriteLocation { row: 6, col: 9 },
Sprite::DebugChecker => SpriteLocation { row: 0, col: 13 },
Sprite::DebugBlank => SpriteLocation { row: 0, col: 14 },
Sprite::DebugFilled => SpriteLocation { row: 0, col: 15 },
Sprite::Debug4Corners => SpriteLocation { row: 0, col: 16 },
}
}

+ 46
- 0
src/state.rs View File

@@ -0,0 +1,46 @@
use entity::*;
use sprite::Sprite;
use controller::Controller;
use geometry::Geometry;

pub struct State {

player: Entity,
}

impl State {
pub fn new(view_frame_width: i32, view_frame_height: i32) -> State {
State {
player: Entity::new(0, 0, Colour::Blue, Sprite::Block, false),
}
}

pub fn init(&mut self) {
}

pub fn update_geometry(&self, geometry: &mut Geometry) {
geometry.clear();

// zero vert
geometry.push(0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0);

render_entity(geometry, &self.player);
}

pub fn update_geometry_debug(&self, geometry: &mut Geometry) {

}

pub fn tick_playing(&mut self, _controller: &Controller) -> bool {
false
}
}

fn render_entity(geometry: &mut Geometry, e: &Entity) {
geometry.push_sprite(
e.sprite,
e.pos.x,
e.pos.y,
e.colour,
)
}

+ 103
- 0
src/text.rs View File

@@ -0,0 +1,103 @@
use sprite::SpriteLocation;

// note: these spritelocation values are for letters which have half the width of regular sprites

pub fn sprite_location_from_char(c: char) -> SpriteLocation {
match c {
' ' => SpriteLocation { row: 23, col: 10 },

'0' => SpriteLocation { row: 23, col: 0 },
'1' => SpriteLocation { row: 23, col: 1 },
'2' => SpriteLocation { row: 23, col: 2 },
'3' => SpriteLocation { row: 23, col: 3 },
'4' => SpriteLocation { row: 23, col: 4 },
'5' => SpriteLocation { row: 23, col: 5 },
'6' => SpriteLocation { row: 23, col: 6 },
'7' => SpriteLocation { row: 23, col: 7 },
'8' => SpriteLocation { row: 23, col: 8 },
'9' => SpriteLocation { row: 23, col: 9 },

'A' => SpriteLocation { row: 24, col: 0 },
'B' => SpriteLocation { row: 24, col: 1 },
'C' => SpriteLocation { row: 24, col: 2 },
'D' => SpriteLocation { row: 24, col: 3 },
'E' => SpriteLocation { row: 24, col: 4 },
'F' => SpriteLocation { row: 24, col: 5 },
'G' => SpriteLocation { row: 24, col: 6 },
'H' => SpriteLocation { row: 24, col: 7 },
'I' => SpriteLocation { row: 24, col: 8 },
'J' => SpriteLocation { row: 24, col: 9 },
'K' => SpriteLocation { row: 24, col: 10 },
'L' => SpriteLocation { row: 24, col: 11 },
'M' => SpriteLocation { row: 24, col: 12 },
'N' => SpriteLocation { row: 24, col: 13 },
'O' => SpriteLocation { row: 24, col: 14 },
'P' => SpriteLocation { row: 24, col: 15 },
'Q' => SpriteLocation { row: 24, col: 16 },
'R' => SpriteLocation { row: 24, col: 17 },
'S' => SpriteLocation { row: 24, col: 18 },
'T' => SpriteLocation { row: 24, col: 19 },
'U' => SpriteLocation { row: 24, col: 20 },
'V' => SpriteLocation { row: 24, col: 21 },
'W' => SpriteLocation { row: 24, col: 22 },
'X' => SpriteLocation { row: 24, col: 23 },
'Y' => SpriteLocation { row: 24, col: 24 },
'Z' => SpriteLocation { row: 24, col: 25 },

'a' => SpriteLocation { row: 26, col: 0 },
'b' => SpriteLocation { row: 26, col: 1 },
'c' => SpriteLocation { row: 26, col: 2 },
'd' => SpriteLocation { row: 26, col: 3 },
'e' => SpriteLocation { row: 26, col: 4 },
'f' => SpriteLocation { row: 26, col: 5 },
'g' => SpriteLocation { row: 26, col: 6 },
'h' => SpriteLocation { row: 26, col: 7 },
'i' => SpriteLocation { row: 26, col: 8 },
'j' => SpriteLocation { row: 26, col: 9 },
'k' => SpriteLocation { row: 26, col: 10 },
'l' => SpriteLocation { row: 26, col: 11 },
'm' => SpriteLocation { row: 26, col: 12 },
'n' => SpriteLocation { row: 26, col: 13 },
'o' => SpriteLocation { row: 26, col: 14 },
'p' => SpriteLocation { row: 26, col: 15 },
'q' => SpriteLocation { row: 26, col: 16 },
'r' => SpriteLocation { row: 26, col: 17 },
's' => SpriteLocation { row: 26, col: 18 },
't' => SpriteLocation { row: 26, col: 19 },
'u' => SpriteLocation { row: 26, col: 20 },
'v' => SpriteLocation { row: 26, col: 21 },
'w' => SpriteLocation { row: 26, col: 22 },
'x' => SpriteLocation { row: 26, col: 23 },
'y' => SpriteLocation { row: 26, col: 24 },
'z' => SpriteLocation { row: 26, col: 25 },

'#' => SpriteLocation { row: 28, col: 0 },
'%' => SpriteLocation { row: 28, col: 1 },
'&' => SpriteLocation { row: 28, col: 2 },
'@' => SpriteLocation { row: 28, col: 3 },
'$' => SpriteLocation { row: 28, col: 4 },
'.' => SpriteLocation { row: 28, col: 5 },
',' => SpriteLocation { row: 28, col: 6 },
'!' => SpriteLocation { row: 28, col: 7 },
'?' => SpriteLocation { row: 28, col: 8 },
':' => SpriteLocation { row: 28, col: 9 },
';' => SpriteLocation { row: 28, col: 10 },
'\'' => SpriteLocation { row: 28, col: 11 },
'"' => SpriteLocation { row: 28, col: 12 },
'(' => SpriteLocation { row: 28, col: 13 },
')' => SpriteLocation { row: 28, col: 14 },
'[' => SpriteLocation { row: 28, col: 15 },
']' => SpriteLocation { row: 28, col: 16 },
'*' => SpriteLocation { row: 28, col: 17 },
'/' => SpriteLocation { row: 28, col: 18 },
'\\' => SpriteLocation { row: 28, col: 19 },
'+' => SpriteLocation { row: 28, col: 20 },
'-' => SpriteLocation { row: 28, col: 21 },
'<' => SpriteLocation { row: 28, col: 22 },
'=' => SpriteLocation { row: 28, col: 23 },
'>' => SpriteLocation { row: 28, col: 24 },

_ => SpriteLocation { row: 23, col: 10 },

}
}

+ 71
- 0
src/units.rs View File

@@ -0,0 +1,71 @@
use log;

#[derive(Debug, Clone, Copy)]
pub struct Col {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}

impl Col {
pub fn new(r: f32, g: f32, b: f32, a: f32) -> Col {
Col {
r, g, b, a
}
}
}

#[derive(Debug, Clone, Copy)]
pub struct Vec2D {
pub x: f32,
pub y: f32,
}

impl Vec2D {
pub fn log(self, msg: &str) {
log(&format!("{}: ({}, {})", msg, self.x, self.y))
}
}

pub fn length_vec2d(v: Vec2D) -> f32 {
((v.x * v.x) + (v.y * v.y)).sqrt()
}

pub fn normalize(v: Vec2D) -> Vec2D {
let len = length_vec2d(v);

Vec2D {
x: v.x / len,
y: v.y / len,
}
}

pub fn normal(p1: Vec2D, p2: Vec2D) -> Vec2D {
let v = Vec2D {
x: p1.y - p2.y,
y: p2.x - p1.x,
};

normalize(v)
}

pub fn opposite_normal(v: Vec2D) -> Vec2D {
Vec2D {
x: -v.x,
y: -v.y,
}
}

// 2D Vector in tilespace
#[derive(Debug, Clone, Copy)]
pub struct Tile2D {
pub x: i32,
pub y: i32,
}

impl Tile2D {
pub fn log(self, msg: &str) {
log(&format!("{}: ({}, {})", msg, self.x, self.y))
}
}

BIN
web/img/tileset.png View File

Before After
Width: 512  |  Height: 512  |  Size: 13KB

+ 246
- 0
web/js/GLRenderer.js View File

@@ -0,0 +1,246 @@
import Matrix from './Matrix';

const logToConsole = false;

function memorySubArray(mem, ptr, length) {
const nByte = 4;
const pos = ptr / nByte;
return mem.subarray(pos, pos + length);
}

function initGL(canvas) {
try {
const gl = canvas.getContext('experimental-webgl', {
alpha: false,
preserveDrawingBuffer: true
});
// commented out because of jshint
// if (!gl) {
//alert('Could not initialise WebGL, sorry :-(');
// }

return gl;
} catch (e) {
return undefined;
}
}

function compileShader(gl, type, src) {
const shader = gl.createShader(type);
gl.shaderSource(shader, src);
gl.compileShader(shader);

if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
//alert(gl.getShaderInfoLog(shader));
return null;
}
return shader;
}

function setupShaders(gl) {
const shaderProgram = gl.createProgram();

const fragmentSrc = `
precision mediump float;
varying vec4 vColor;
varying highp vec2 vTextureCoord;

uniform sampler2D uSampler;

void main(void) {
vec4 tex = texture2D(uSampler, vTextureCoord);

gl_FragColor.r = tex.r * vColor.a * vColor.r;
gl_FragColor.g = tex.r * vColor.a * vColor.g;
gl_FragColor.b = tex.r * vColor.a * vColor.b;
gl_FragColor.a = tex.r * vColor.a;

}
`;

const vertexSrc = `
attribute vec2 aVertexPosition;
attribute vec4 aVertexColor;
attribute vec2 aVertexTexture;

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

varying vec4 vColor;
varying highp vec2 vTextureCoord;

void main(void) {
gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 0.0, 1.0);
vColor = aVertexColor;
vTextureCoord = aVertexTexture;
}
`;

const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexSrc);
const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSrc);

gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);

gl.linkProgram(shaderProgram);

// commented out because of jshint
// if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
//alert('Could not initialise shaders');
// }

gl.useProgram(shaderProgram);

shaderProgram.positionAttribute =
gl.getAttribLocation(shaderProgram, 'aVertexPosition');
gl.enableVertexAttribArray(shaderProgram.positionAttribute);

shaderProgram.colourAttribute =
gl.getAttribLocation(shaderProgram, 'aVertexColor');
gl.enableVertexAttribArray(shaderProgram.colourAttribute);

shaderProgram.textureAttribute =
gl.getAttribLocation(shaderProgram, 'aVertexTexture');
gl.enableVertexAttribArray(shaderProgram.textureAttribute);

shaderProgram.pMatrixUniform =
gl.getUniformLocation(shaderProgram, 'uPMatrix');
shaderProgram.mvMatrixUniform =
gl.getUniformLocation(shaderProgram, 'uMVMatrix');

return shaderProgram;
}

function setupGLState(gl) {
gl.clearColor(0.0, 0.0, 0.0, 0.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.enable(gl.BLEND);

// assuming that we'll be using pre-multiplied alpha
// see http://www.realtimerendering.com/blog/gpus-prefer-premultiplication/
gl.blendEquation(gl.FUNC_ADD);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

// gl.disable(gl.DEPTH_TEST);
}


function handleTextureLoaded(gl, image, texture, shaderProgram) {
console.log(`handleTextureLoaded ${texture}`);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

// gl.generateMipmap(gl.TEXTURE_2D);
gl.bindTexture(gl.TEXTURE_2D, null);

gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(gl.getUniformLocation(shaderProgram, 'uSampler'), 0);
}

export class GLRenderer {
constructor(gameConfig, canvasElement) {
this.glDomElement = canvasElement;

// webgl setup
const gl = initGL(this.glDomElement);
this.gl = gl;

this.shaderProgram = setupShaders(gl);
setupGLState(gl);

this.glVertexBuffer = gl.createBuffer();
this.glColourBuffer = gl.createBuffer();
this.glTextureBuffer = gl.createBuffer();

this.mvMatrix = Matrix.create();
this.pMatrix = Matrix.create();
Matrix.ortho(this.pMatrix, 0, gameConfig.frustrum_width, 0, gameConfig.frustrum_height, 10, -10);
}

loadTexture(src) {
let that = this;

return new Promise(function(resolve, reject) {

const gl = that.gl;
that.texture = gl.createTexture();
const image = new Image();

image.addEventListener('load', () => {
handleTextureLoaded(that.gl, image, that.texture, that.shaderProgram);
resolve([image.width, image.height]);
});

image.addEventListener('error', () => {
reject();
});

image.src = src;
});
}

preDrawScene(frustrumWidth, frustrumHeight) {
const gl = this.gl;

gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);


// render the entirety of the scene
Matrix.ortho(this.pMatrix, 0, frustrumWidth, 0, frustrumHeight, 10, -10);

gl.uniformMatrix4fv(this.shaderProgram.pMatrixUniform,
false,
this.pMatrix);

gl.uniformMatrix4fv(this.shaderProgram.mvMatrixUniform,
false,
this.mvMatrix);
}

drawBuffer(memoryF32, geo_len, geo_ptr) {
const gl = this.gl;
const shaderProgram = this.shaderProgram;

const glVertexBuffer = this.glVertexBuffer;
const glColourBuffer = this.glColourBuffer;
const glTextureBuffer = this.glTextureBuffer;

const vertexItemSize = 2;
const colourItemSize = 4;
const textureItemSize = 2;
const totalSize = (vertexItemSize + colourItemSize + textureItemSize);

const gbuf = memorySubArray(memoryF32, geo_ptr, geo_len);

gl.bindBuffer(gl.ARRAY_BUFFER, glVertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, gbuf, gl.STATIC_DRAW);
gl.vertexAttribPointer(shaderProgram.positionAttribute,
vertexItemSize,
gl.FLOAT, false, totalSize * 4,
0);

gl.bindBuffer(gl.ARRAY_BUFFER, glColourBuffer);
gl.bufferData(gl.ARRAY_BUFFER, gbuf, gl.STATIC_DRAW);
gl.vertexAttribPointer(shaderProgram.colourAttribute,
colourItemSize,
gl.FLOAT, false, totalSize * 4,
vertexItemSize * 4);

gl.bindBuffer(gl.ARRAY_BUFFER, glTextureBuffer);
gl.bufferData(gl.ARRAY_BUFFER, gbuf, gl.STATIC_DRAW);
gl.vertexAttribPointer(shaderProgram.textureAttribute,
textureItemSize,
gl.FLOAT, false, totalSize * 4,
(vertexItemSize + colourItemSize) * 4);

gl.drawArrays(gl.TRIANGLE_STRIP, 0, geo_len / totalSize);
}
}

+ 259
- 0
web/js/Matrix.js View File

@@ -0,0 +1,259 @@
/**
* Code taken from gl-matrix (http://glmatrix.net/)

Copyright (c) 2015, Brandon Jones, Colin MacKenzie IV.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
**/

function create() {
const out = new Float32Array(16);
out[0] = 1;
out[1] = 0;
out[2] = 0;
out[3] = 0;
out[4] = 0;
out[5] = 1;
out[6] = 0;
out[7] = 0;
out[8] = 0;
out[9] = 0;
out[10] = 1;
out[11] = 0;
out[12] = 0;
out[13] = 0;
out[14] = 0;
out[15] = 1;

return out;
}

function identity(out) {
out[0] = 1;
out[1] = 0;
out[2] = 0;
out[3] = 0;
out[4] = 0;
out[5] = 1;
out[6] = 0;
out[7] = 0;
out[8] = 0;
out[9] = 0;
out[10] = 1;
out[11] = 0;
out[12] = 0;
out[13] = 0;
out[14] = 0;
out[15] = 1;

return out;
}

function ortho(out, left, right, bottom, top, near, far) {
const lr = 1 / (left - right);
const bt = 1 / (bottom - top);
const nf = 1 / (near - far);

out[0] = -2 * lr;
out[1] = 0;
out[2] = 0;
out[3] = 0;
out[4] = 0;
out[5] = -2 * bt;
out[6] = 0;
out[7] = 0;
out[8] = 0;
out[9] = 0;
out[10] = 2 * nf;
out[11] = 0;
out[12] = (left + right) * lr;
out[13] = (top + bottom) * bt;
out[14] = (far + near) * nf;
out[15] = 1;

return out;
}

function clone(a) {
const out = new Float32Array(16);
out[0] = a[0];
out[1] = a[1];
out[2] = a[2];
out[3] = a[3];
out[4] = a[4];
out[5] = a[5];
out[6] = a[6];
out[7] = a[7];
out[8] = a[8];
out[9] = a[9];
out[10] = a[10];
out[11] = a[11];
out[12] = a[12];
out[13] = a[13];
out[14] = a[14];
out[15] = a[15];

return out;
}

function scale(out, a, v) {
const x = v[0], y = v[1], z = v[2];

out[0] = a[0] * x;
out[1] = a[1] * x;
out[2] = a[2] * x;
out[3] = a[3] * x;
out[4] = a[4] * y;
out[5] = a[5] * y;
out[6] = a[6] * y;
out[7] = a[7] * y;
out[8] = a[8] * z;
out[9] = a[9] * z;
out[10] = a[10] * z;
out[11] = a[11] * z;
out[12] = a[12];
out[13] = a[13];
out[14] = a[14];
out[15] = a[15];

return out;
}

function multiply(out, a, b) {
const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3];
const a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7];
const a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11];
const a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];

// Cache only the current line of the second matrix
const b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3];
out[0] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
out[1] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
out[2] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
out[3] = b0*a03 + b1*a13 + b2*a23 + b3*a33;

b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7];
out[4] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
out[5] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
out[6] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
out[7] = b0*a03 + b1*a13 + b2*a23 + b3*a33;

b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11];
out[8] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
out[9] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
out[10] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
out[11] = b0*a03 + b1*a13 + b2*a23 + b3*a33;

b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15];
out[12] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
out[13] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
out[14] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
out[15] = b0*a03 + b1*a13 + b2*a23 + b3*a33;

return out;
}

function translate(out, a, v) {
const x = v[0], y = v[1], z = v[2];
// let a00, a01, a02, a03, a10, a11, a12, a13, a20, a21, a22, a23;

if (a === out) {
out[12] = a[0] * x + a[4] * y + a[8] * z + a[12];
out[13] = a[1] * x + a[5] * y + a[9] * z + a[13];
out[14] = a[2] * x + a[6] * y + a[10] * z + a[14];
out[15] = a[3] * x + a[7] * y + a[11] * z + a[15];
} else {
const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3];
const a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7];
const a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11];

out[0] = a00; out[1] = a01; out[2] = a02; out[3] = a03;
out[4] = a10; out[5] = a11; out[6] = a12; out[7] = a13;
out[8] = a20; out[9] = a21; out[10] = a22; out[11] = a23;

out[12] = a00 * x + a10 * y + a20 * z + a[12];
out[13] = a01 * x + a11 * y + a21 * z + a[13];
out[14] = a02 * x + a12 * y + a22 * z + a[14];
out[15] = a03 * x + a13 * y + a23 * z + a[15];
}

return out;
}

function rotateZ(out, a, rad) {
const s = Math.sin(rad), c = Math.cos(rad);
const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3];
const a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7];

if (a !== out) {
out[8] = a[8];
out[9] = a[9];
out[10] = a[10];
out[11] = a[11];
out[12] = a[12];
out[13] = a[13];
out[14] = a[14];
out[15] = a[15];
}

// Perform axis-specific matrix multiplication
out[0] = a00 * c + a10 * s;
out[1] = a01 * c + a11 * s;
out[2] = a02 * c + a12 * s;
out[3] = a03 * c + a13 * s;
out[4] = a10 * c - a00 * s;
out[5] = a11 * c - a01 * s;
out[6] = a12 * c - a02 * s;
out[7] = a13 * c - a03 * s;

return out;
}

function transformVec2(out, a, m) {
const x = a[0];
const y = a[1];
out[0] = m[0] * x + m[4] * y + m[12];
out[1] = m[1] * x + m[5] * y + m[13];

return out;
}

function transformVec3(out, a, m) {
const x = a[0], y = a[1], z = a[2];
let w = m[3] * x + m[7] * y + m[11] * z + m[15];
w = w || 1.0;
out[0] = (m[0] * x + m[4] * y + m[8] * z + m[12]) / w;
out[1] = (m[1] * x + m[5] * y + m[9] * z + m[13]) / w;
out[2] = (m[2] * x + m[6] * y + m[10] * z + m[14]) / w;

return out;
}

export default {
create,
identity,
ortho,
clone,
scale,
multiply,
translate,
rotateZ,
transformVec2,
transformVec3
};

+ 4
- 0
web/js/bootstrap.js View File

@@ -0,0 +1,4 @@
const index = import("./index");
index.then(() => {
console.log("Loaded...");
});

+ 103
- 0
web/js/index.js View File

@@ -0,0 +1,103 @@
import { GameBridge } from "../wasm_tetris";
import { memory } from "../wasm_tetris_bg";
import { GLRenderer } from "./GLRenderer";

let gState = {
canvasId: 'render-canvas',

canvasWidth: 1280, // 4:3 ratio
canvasHeight: 960,

// the number of tiles that can be shown on screen
viewFrameWidth: 40, // 4:3 ratio
viewFrameHeight: 30, // 15

tileset: '/web/img/tileset.png',

gameBridge: undefined,
animationId: undefined,
renderer: undefined
};

const tick = () => {
const delta = 0.033;

const gameBridge = gState.gameBridge;

// game state + time + user input -> game state
const tickAgain = gameBridge.tick(delta);

render();

if (tickAgain) {
gState.animationId = requestAnimationFrame(tick);
}
};

const render = () => {
const gameBridge = gState.gameBridge;
const renderer = gState.renderer;

// gameBridge state -> geometry
gameBridge.update_geometry();

// render geometry
const memoryF32 = new Float32Array(memory.buffer);
const geo_len = gameBridge.geo_len();
const geo_ptr = gameBridge.geo_ptr();

renderer.preDrawScene(gState.canvasWidth, gState.canvasHeight);
renderer.drawBuffer(memoryF32, geo_len, geo_ptr);
};

function main() {
const canvasElement = document.getElementById(gState.canvasId);
canvasElement.width = gState.canvasWidth;
canvasElement.height = gState.canvasHeight;

let renderer = new GLRenderer(gState, canvasElement);
renderer.loadTexture(gState.tileset).then(([tilesetWidth, tilesetHeight]) => {
gState.gameBridge = new GameBridge(gState.canvasWidth,
gState.canvasHeight,
gState.viewFrameWidth,
gState.viewFrameHeight,
tilesetWidth,
tilesetHeight);

gState.renderer = renderer;

document.addEventListener('keydown', event => {
let key_event_return = gState.gameBridge.event_key_down(event.key);

if (key_event_return.prevent_default()) {
event.preventDefault();
}
if (key_event_return.call_tick()) {
tick();
}

key_event_return.free();
});
document.addEventListener('keyup', event => {
let key_event_return = gState.gameBridge.event_key_up(event.key);

if (key_event_return.prevent_default()) {
event.preventDefault();
}
if (key_event_return.call_tick()) {
tick();
}

key_event_return.free();
});


gState.gameBridge.init();
tick();
}).catch(error => console.error(error));

// isg: NOTE: remember to free GameBridge
// gState.gameBridge.free();
}

main();

+ 13
- 0
webpack.config.js View File

@@ -0,0 +1,13 @@
const path = require("path");

module.exports = {
entry: "./web/js/bootstrap.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bootstrap.js",
},
optimization: {
minimize: false
},
mode: "development"
};

Loading…
Cancel
Save