Repository: PacktPublishing/Game-Development-with-Rust-and-WebAssembly Branch: main Commit: e7122da5435a Files: 24 Total size: 82.8 KB Directory structure: gitextract_9erurgo8/ ├── .github/ │ └── workflows/ │ └── build.yml ├── .gitignore ├── .rustfmt ├── .tool-versions ├── Cargo.toml ├── LICENSE ├── README.md ├── js/ │ └── index.js ├── package.json ├── rust-toolchain.toml ├── rustfmt.toml ├── src/ │ ├── browser.rs │ ├── engine.rs │ ├── game.rs │ ├── lib.rs │ ├── segments.rs │ ├── sound.rs │ └── test_browser.rs ├── static/ │ ├── index.html │ ├── rhb.json │ ├── styles.css │ └── tiles.json ├── tests/ │ └── app.rs └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/build.yml ================================================ on: [push] name: build jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: 1.57.0 target: wasm32-unknown-unknown override: true components: clippy - uses: actions-rs/install@v0.1 with: crate: wasm-pack version: 0.9.1 use-tool-cache: true - name: Annotate commit with clippy warnings uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - uses: actions/setup-node@v2 with: node-version: '16.13.0' - run: npm install - run: npm test - run: npm run build - name: Deploy to Netlify uses: nwtgck/actions-netlify@v1.2 with: publish-dir: './dist' production-branch: main github-token: ${{ secrets.GITHUB_TOKEN }} deploy-message: "Deploy from GitHub Actions" enable-pull-request-comment: true enable-commit-comment: true overwrites-pull-request-comment: true env: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} timeout-minutes: 1 ================================================ FILE: .gitignore ================================================ node_modules /dist /target /pkg /wasm-pack.log # Local Netlify folder .netlify .DS_Store ================================================ FILE: .rustfmt ================================================ edition = "2018" ================================================ FILE: .tool-versions ================================================ nodejs 16.13.0 ================================================ FILE: Cargo.toml ================================================ # You must change these to your own details. [package] name = "rust-webpack-template" description = "Walk the Dog - the game for the Rust Games with WebAssembly book" version = "0.1.0" authors = ["Eric Smith "] categories = ["wasm"] readme = "README.md" edition = "2021" [lib] crate-type = ["cdylib"] [profile.release] # This makes the compiled code faster and smaller, but it makes compiling slower, # so it's only enabled in release mode. lto = true [features] # If you uncomment this line, it will enable `wee_alloc`: #default = ["wee_alloc"] [dependencies] # The `wasm-bindgen` crate provides the bare minimum functionality needed # to interact with JavaScript. wasm-bindgen = { version = "0.2.78", features = ["serde-serialize"] } console_error_panic_hook = "0.1.7" rand = "0.8.4" getrandom = { version = "0.2.3", features = ["js"] } futures = "0.3.17" wasm-bindgen-futures = "0.4.28" serde = {version = "1.0.131", features = ["derive"] } anyhow = "1.0.51" async-trait = "0.1.52" js-sys = "0.3.55" # The `web-sys` crate allows you to interact with the various browser APIs, # like the DOM. [dependencies.web-sys] version = "0.3.55" features = ["console", "Window", "Document", "HtmlCanvasElement", "CanvasRenderingContext2d", "Element", "HtmlImageElement", "Response", "Performance", "KeyboardEvent", "AudioContext", "AudioBuffer", "AudioBufferSourceNode", "AudioDestinationNode", "AudioBufferOptions", ] # These crates are used for running unit tests. [dev-dependencies] wasm-bindgen-test = "0.3.28" ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Packt 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. ================================================ FILE: README.md ================================================ # Game Development with Rust and WebAssembly Game Development with Rust and WebAssembly, published by Packt Game Development with Rust and WebAssembly This is the code repository for [Game Development with Rust and WebAssembly](https://www.packtpub.com/product/game-development-with-rust-and-webassembly/9781801070973), published by Packt. **Learn how to run Rust on the web while building a game** ## What is this book about? The Rust programming language has held the most-loved technology ranking on Stack Overflow for 6 years running, while JavaScript has been the most-used programming language for 9 years straight as it runs on every web browser. Now, thanks to WebAssembly (or Wasm), you can use the language you love on the platform that's everywhere. This book covers the following exciting features: * Build and deploy a Rust application to the web using WebAssembly * Use wasm-bindgen and the Canvas API to draw real-time graphics * Write a game loop and take keyboard input for dynamic action * Explore collision detection and create a dynamic character that can jump on and off platforms and fall down holes * Manage animations using state machines * Generate levels procedurally for an endless runner * Load and display sprites and sprite sheets for animations If you feel this book is for you, get your [copy](https://www.amazon.com/dp/1801070970) today! https://www.packtpub.com/ ## Instructions and Navigation You're currently looking the main branch of this repository, which represents the "completed" state of this book. I say completed because development on this branch is ongoing - specifically the challenges cited in the book are being implemented here. If you want to see the end state of any chapter those are stored as tags, such as https://github.com/PacktPublishing/Game-Development-with-Rust-and-WebAssembly/tree/chapter_1. **Following is what you need for this book:** This game development book is for developers interested in Rust who want to create and deploy 2D games to the web. Game developers looking to build a game on the web platform using WebAssembly without C++ programming or web developers who want to explore WebAssembly along with JavaScript web will also find this book useful. The book will also help Rust developers who want to move from the server side to the client side by familiarizing them with the WebAssembly toolchain. Basic knowledge of Rust programming is assumed. With the following software and hardware list you can run all code files present in the book (Chapter 1-11). ### Software and Hardware List | Chapter | Software required | version | OS required | |----------|--------------------------------------------|---------|-------------| | (1 - 11) | Rust Toolchains via Rustup | 1.57.0 | Any OS | | (1 - 11) | NodeJS | 16.13.0 | Any OS | | (1 - 11) | Rust Compile target wasm32-unknown-unknown | NA | NA | I use https://asdf-vm.com to install Node and a .tool-versions file is present but you don't have to. Instructions for creating a new project are found in the book (chapter 1) but the project can also be setup by cloning this repository and running the commands for building and running. Speaking of that: ### Running this App #### Installation `npm install` Will install the Node dependencies (primarily WebPack). Don't worry you don't have to think about those much. #### Running in debug `npm start` Will compile the application to Wasm and start a server, running it at localhost:8080 by default. This will also ensure `wasm-pack` is setup and running and run `cargo build`. #### Building for release `npm run build` Creates a release build and puts it in the `dist` directory. #### Running Tests `npm run test` You can use a lot of the `cargo` commands as well - but those do not go through the process of bundling up the built assembly for distribution. #### Deployment This branch is setup for continuous deployment with GitHub Actions, as is the tag for chapter_10. Something to keep in mind when forking the repository. The current production version of this game can be found at: https://rust-games-webassembly.netlify.app ## Challenges At the end of the book (Further Resources and What's Next?) there are six challenges for you, the reader. I'll be completing them on and off [my stream](www.twitch.tv/paytonrules), and making a note here when they are complete. Challenge #6: - Challenge #6 was completed two ways. I displayed the score via the canvas's render text function, as well as via the DOM. There are two branches with the solutions: [add-score](https://github.com/PacktPublishing/Game-Development-with-Rust-and-WebAssembly/tree/add-score) and [add-score-html](https://github.com/PacktPublishing/Game-Development-with-Rust-and-WebAssembly/tree/add-score-via-html). The HTML version looks a lot better, and is also implemented in the main branch. ## More Information We also provide a PDF file that has color images of the screenshots/diagrams used in this book. [Click here to download it](https://static.packt-cdn.com/downloads/9781801070973_ColorImages.pdf). The Code in Action videos for this book can be viewed at https://bit.ly/3uxXl4W. ### Related products * Creative Projects for Rust Programmers [[Packt]](https://www.packtpub.com/product/creative-projects-for-rust-programmers/9781789346220) [[Amazon]](https://www.amazon.com/Creative-Projects-Rust-Programmers-WebAssembly/dp/1789346223) * Rust Web Programming [[Packt]](https://www.packtpub.com/product/rust-web-programming/9781800560819) [[Amazon]](https://www.amazon.com/Rust-Web-Programming-hands-programming-dp-1800560818/dp/1800560818/ref=mt_other?_encoding=UTF8&me=&qid=) ## Get to Know the Author **Eric Smith** is a software crafter with over 20 years of software development experience. Since 2005, he's worked at 8th Light, where he consults for companies big and small by delivering software, mentoring developers, and coaching teams. He's a frequent speaker at conferences speaking on topics such as educating developers and test-driven development, and holds a master's degree in video game development from DePaul University. Eric wrote much of the code for this book live on his Twitch stream. When he's not at the computer, you can find Eric running obstacle races and traveling with his family. ### Download a free PDF If you have already purchased a print or Kindle version of this book, you can get a DRM-free PDF version at no cost.
Simply click on the link to claim your free PDF.

https://packt.link/free-ebook/9781801070973

================================================ FILE: js/index.js ================================================ import("../pkg/index.js").catch(console.error); ================================================ FILE: package.json ================================================ { "author": "You ", "name": "rust-webpack-template", "version": "0.1.0", "scripts": { "build": "rimraf dist pkg && webpack", "start": "rimraf dist pkg && webpack-dev-server --open -d --host 0.0.0.0", "test": "cargo test && wasm-pack test --headless --chrome" }, "devDependencies": { "@wasm-tool/wasm-pack-plugin": "^1.1.0", "copy-webpack-plugin": "^5.0.3", "rimraf": "^3.0.0", "webpack": "^4.42.0", "webpack-cli": "^3.3.3", "webpack-dev-server": "^3.7.1" }, "dependencies": { "netlify-cli": "^8.0.1" } } ================================================ FILE: rust-toolchain.toml ================================================ [toolchain] channel = "1.57.0" targets = ["wasm32-unknown-unknown"] ================================================ FILE: rustfmt.toml ================================================ edition = "2018" ================================================ FILE: src/browser.rs ================================================ use anyhow::{anyhow, Result}; use js_sys::ArrayBuffer; use std::future::Future; use wasm_bindgen::{ closure::WasmClosure, closure::WasmClosureFnOnce, prelude::Closure, JsCast, JsValue, }; use wasm_bindgen_futures::JsFuture; use web_sys::{ CanvasRenderingContext2d, Document, Element, HtmlCanvasElement, HtmlElement, HtmlImageElement, Response, Window, }; // Straight taken from https://rustwasm.github.io/book/game-of-life/debugging.html macro_rules! log { ( $( $t:tt )* ) => { web_sys::console::log_1(&format!( $( $t )* ).into()); } } macro_rules! error { ( $( $t:tt )* ) => { web_sys::console::error_1(&format!( $( $t )* ).into()); } } pub fn window() -> Result { web_sys::window().ok_or_else(|| anyhow!("No Window Found")) } pub fn document() -> Result { window()? .document() .ok_or_else(|| anyhow!("No Document Found")) } pub fn canvas() -> Result { document()? .get_element_by_id("canvas") .ok_or_else(|| anyhow!("No Canvas Element found with ID 'canvas'"))? .dyn_into::() .map_err(|element| anyhow!("Error converting {:#?} to HtmlCanvasElement", element)) } pub fn context() -> Result { canvas()? .get_context("2d") .map_err(|js_value| anyhow!("Error getting 2d context {:#?}", js_value))? .ok_or_else(|| anyhow!("No 2d context found"))? .dyn_into::() .map_err(|element| { anyhow!( "Error converting {:#?} to CanvasRenderingContext2d", element ) }) } pub fn spawn_local(future: F) where F: Future + 'static, { wasm_bindgen_futures::spawn_local(future); } pub async fn fetch_with_str(resource: &str) -> Result { JsFuture::from(window()?.fetch_with_str(resource)) .await .map_err(|err| anyhow!("error fetching {:#?}", err)) } pub async fn fetch_response(resource: &str) -> Result { fetch_with_str(resource) .await? .dyn_into() .map_err(|err| anyhow!("error converting fetch to Response {:#?}", err)) } pub async fn fetch_json(json_path: &str) -> Result { let resp = fetch_response(json_path).await?; JsFuture::from( resp.json() .map_err(|err| anyhow!("Could not get JSON from response {:#?}", err))?, ) .await .map_err(|err| anyhow!("error fetching JSON {:#?}", err)) } pub async fn fetch_array_buffer(resource: &str) -> Result { let array_buffer = fetch_response(resource) .await? .array_buffer() .map_err(|err| anyhow!("Error loading array buffer {:#?}", err))?; JsFuture::from(array_buffer) .await .map_err(|err| anyhow!("Error converting array buffer into a future {:#?}", err))? .dyn_into() .map_err(|err| anyhow!("Error converting raw JSValue to ArrayBuffer {:#?}", err)) } pub fn new_image() -> Result { HtmlImageElement::new().map_err(|err| anyhow!("Could not create HtmlImageElement: {:#?}", err)) } pub type LoopClosure = Closure; pub fn create_raf_closure(f: impl FnMut(f64) + 'static) -> LoopClosure { closure_wrap(Box::new(f)) } pub fn request_animation_frame(callback: &LoopClosure) -> Result { window()? .request_animation_frame(callback.as_ref().unchecked_ref()) .map_err(|err| anyhow!("Cannot request animation frame {:#?}", err)) } pub fn closure_once(fn_once: F) -> Closure where F: 'static + WasmClosureFnOnce, { Closure::once(fn_once) } pub fn closure_wrap(data: Box) -> Closure { Closure::wrap(data) } pub fn now() -> Result { Ok(window()? .performance() .ok_or_else(|| anyhow!("Performance object not found"))? .now()) } pub fn draw_ui(html: &str) -> Result<()> { find_ui()? .insert_adjacent_html("afterbegin", html) .map_err(|err| anyhow!("Could not insert html {:#?}", err)) } pub fn hide_ui() -> Result<()> { let ui = find_ui()?; if let Some(child) = ui.first_child() { ui.remove_child(&child) .map(|_removed_child| ()) .map_err(|err| anyhow!("Failed to remove child {:#?}", err)) .and_then(|_unit| { canvas()? .focus() .map_err(|err| anyhow!("Could not set focus to canvas! {:#?}", err)) }) } else { Ok(()) } } fn find_ui() -> Result { document().and_then(|doc| { doc.get_element_by_id("ui") .ok_or_else(|| anyhow!("UI element not found")) }) } pub fn find_html_element_by_id(id: &str) -> Result { document() .and_then(|doc| { doc.get_element_by_id(id) .ok_or_else(|| anyhow!("Element with id {} not found", id)) }) .and_then(|element| { element .dyn_into::() .map_err(|err| anyhow!("Could not cast into HtmlElement {:#?}", err)) }) } #[cfg(test)] mod tests { use super::*; use wasm_bindgen_test::wasm_bindgen_test; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] async fn test_error_loading_json() { let json = fetch_json("not_there.json").await; assert_eq!(json.is_err(), true); } } ================================================ FILE: src/engine.rs ================================================ use crate::browser::{self, LoopClosure}; use crate::sound; use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::channel::{ mpsc::{unbounded, UnboundedReceiver}, oneshot::channel, }; use serde::Deserialize; use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Mutex}; use wasm_bindgen::{prelude::Closure, JsCast, JsValue}; use web_sys::AudioContext; use web_sys::{AudioBuffer, HtmlElement}; use web_sys::{CanvasRenderingContext2d, HtmlImageElement}; #[derive(Deserialize, Clone)] pub struct SheetRect { pub x: i16, pub y: i16, pub w: i16, pub h: i16, } #[derive(Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Cell { pub frame: SheetRect, pub sprite_source_size: SheetRect, } #[derive(Deserialize, Clone)] pub struct Sheet { pub frames: HashMap, } #[derive(Clone, Copy, Default)] pub struct Point { pub x: i16, pub y: i16, } #[derive(Default)] pub struct Rect { pub position: Point, pub width: i16, pub height: i16, } impl Rect { pub const fn new(position: Point, width: i16, height: i16) -> Self { Rect { position, width, height, } } pub const fn new_from_x_y(x: i16, y: i16, width: i16, height: i16) -> Self { Rect::new(Point { x, y }, width, height) } pub fn intersects(&self, rect: &Rect) -> bool { self.x() < rect.right() && self.right() > rect.x() && self.y() < rect.bottom() && self.bottom() > rect.y() } pub fn right(&self) -> i16 { self.x() + self.width } pub fn bottom(&self) -> i16 { self.y() + self.height } pub fn set_x(&mut self, x: i16) { self.position.x = x } pub fn x(&self) -> i16 { self.position.x } pub fn y(&self) -> i16 { self.position.y } } pub struct Renderer { context: CanvasRenderingContext2d, } impl Renderer { pub fn clear(&self, rect: &Rect) { self.context.clear_rect( rect.x().into(), rect.y().into(), rect.width.into(), rect.height.into(), ); } pub fn draw_image(&self, image: &HtmlImageElement, frame: &Rect, destination: &Rect) { self.context .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh( image, frame.x().into(), frame.y().into(), frame.width.into(), frame.height.into(), destination.x().into(), destination.y().into(), destination.width.into(), destination.height.into(), ) .expect("Drawing is throwing exceptions! Unrecoverable error."); } pub fn draw_entire_image(&self, image: &HtmlImageElement, position: &Point) { self.context .draw_image_with_html_image_element(image, position.x.into(), position.y.into()) .expect("Drawing is throwing exceptions! Unrecoverable error."); } #[allow(dead_code)] pub fn draw_rect(&self, bounding_box: &Rect) { self.context.set_stroke_style(&JsValue::from_str("#FF0000")); self.context.begin_path(); self.context.rect( bounding_box.x().into(), bounding_box.y().into(), bounding_box.width.into(), bounding_box.height.into(), ); self.context.stroke(); } #[allow(dead_code)] pub fn draw_text(&self, text: &str, location: &Point) -> Result<()> { self.context.set_font("16pt Ken Future"); self.context .fill_text(text, location.x.into(), location.y.into()) .map_err(|err| anyhow!("Error filling text {:#?}", err))?; Ok(()) } } pub async fn load_image(source: &str) -> Result { let image = browser::new_image()?; let (complete_tx, complete_rx) = channel::>(); let success_tx = Rc::new(Mutex::new(Some(complete_tx))); let error_tx = Rc::clone(&success_tx); let success_callback = browser::closure_once(move || { if let Some(success_tx) = success_tx.lock().ok().and_then(|mut opt| opt.take()) { if let Err(err) = success_tx.send(Ok(())) { error!("Could not send successful image loaded message! {:#?}", err); } } }); let error_callback: Closure = browser::closure_once(move |err| { if let Some(error_tx) = error_tx.lock().ok().and_then(|mut opt| opt.take()) { if let Err(err) = error_tx.send(Err(anyhow!("Error Loading Image: {:#?}", err))) { error!("Could not send error message on loading image! {:#?}", err); } } }); image.set_onload(Some(success_callback.as_ref().unchecked_ref())); image.set_onerror(Some(error_callback.as_ref().unchecked_ref())); image.set_src(source); complete_rx.await??; Ok(image) } #[async_trait(?Send)] pub trait Game { async fn initialize(&self) -> Result>; fn update(&mut self, keystate: &KeyState); fn draw(&self, renderer: &Renderer); } // Sixty Frames per second, converted to a frame length in milliseconds const FRAME_SIZE: f32 = 1.0 / 60.0 * 1000.0; pub struct GameLoop { last_frame: f64, accumulated_delta: f32, } type SharedLoopClosure = Rc>>; impl GameLoop { pub async fn start(game: impl Game) -> Result<()> { let mut keyevent_receiver = prepare_input()?; let mut game = game.initialize().await?; let mut game_loop = GameLoop { last_frame: browser::now()?, accumulated_delta: 0.0, }; let renderer = Renderer { context: browser::context()?, }; let f: SharedLoopClosure = Rc::new(RefCell::new(None)); let g = f.clone(); let mut keystate = KeyState::new(); *g.borrow_mut() = Some(browser::create_raf_closure(move |perf: f64| { process_input(&mut keystate, &mut keyevent_receiver); let frame_time = perf - game_loop.last_frame; game_loop.accumulated_delta += frame_time as f32; while game_loop.accumulated_delta > FRAME_SIZE { game.update(&keystate); game_loop.accumulated_delta -= FRAME_SIZE; } game_loop.last_frame = perf; game.draw(&renderer); if cfg!(debug_assertions) { unsafe { draw_frame_rate(&renderer, frame_time); } } browser::request_animation_frame(f.borrow().as_ref().unwrap()).unwrap(); })); browser::request_animation_frame( g.borrow() .as_ref() .ok_or_else(|| anyhow!("GameLoop: Loop is None"))?, )?; Ok(()) } } unsafe fn draw_frame_rate(renderer: &Renderer, frame_time: f64) { static mut FRAMES_COUNTED: i32 = 0; static mut TOTAL_FRAME_TIME: f64 = 0.0; static mut FRAME_RATE: i32 = 0; FRAMES_COUNTED += 1; TOTAL_FRAME_TIME += frame_time; if TOTAL_FRAME_TIME > 1000.0 { FRAME_RATE = FRAMES_COUNTED; TOTAL_FRAME_TIME = 0.0; FRAMES_COUNTED = 0; } if let Err(err) = renderer.draw_text( &format!("Frame Rate {}", FRAME_RATE), &Point { x: 400, y: 100 }, ) { error!("Could not draw text {:#?}", err); }; } #[derive(Debug)] pub struct KeyState { pressed_keys: HashMap, } impl KeyState { fn new() -> Self { KeyState { pressed_keys: HashMap::new(), } } pub fn is_pressed(&self, code: &str) -> bool { self.pressed_keys.contains_key(code) } fn set_pressed(&mut self, code: &str, event: web_sys::KeyboardEvent) { self.pressed_keys.insert(code.into(), event); } fn set_released(&mut self, code: &str) { self.pressed_keys.remove(code); } } enum KeyPress { KeyUp(web_sys::KeyboardEvent), KeyDown(web_sys::KeyboardEvent), } fn process_input(state: &mut KeyState, keyevent_receiver: &mut UnboundedReceiver) { loop { match keyevent_receiver.try_next() { Ok(None) => break, Err(_err) => break, Ok(Some(evt)) => match evt { KeyPress::KeyUp(evt) => state.set_released(&evt.code()), KeyPress::KeyDown(evt) => state.set_pressed(&evt.code(), evt), }, }; } } fn prepare_input() -> Result> { let (keydown_sender, keyevent_receiver) = unbounded(); let keydown_sender = Rc::new(RefCell::new(keydown_sender)); let keyup_sender = Rc::clone(&keydown_sender); let onkeydown = browser::closure_wrap(Box::new(move |keycode: web_sys::KeyboardEvent| { if let Err(err) = keydown_sender .borrow_mut() .start_send(KeyPress::KeyDown(keycode)) { error!("Could not send keyDown message {:#?}", err); } }) as Box); let onkeyup = browser::closure_wrap(Box::new(move |keycode: web_sys::KeyboardEvent| { if let Err(err) = keyup_sender .borrow_mut() .start_send(KeyPress::KeyUp(keycode)) { error!("Could not send keyUp message {:#?}", err); } }) as Box); browser::canvas()?.set_onkeydown(Some(onkeydown.as_ref().unchecked_ref())); browser::canvas()?.set_onkeyup(Some(onkeyup.as_ref().unchecked_ref())); onkeydown.forget(); onkeyup.forget(); Ok(keyevent_receiver) } pub fn add_click_handler(elem: HtmlElement) -> UnboundedReceiver<()> { let (mut click_sender, click_receiver) = unbounded(); let on_click = browser::closure_wrap(Box::new(move || { if let Err(err) = click_sender.start_send(()) { error!("Could not send click message {:#?}", err); } }) as Box); elem.set_onclick(Some(on_click.as_ref().unchecked_ref())); on_click.forget(); click_receiver } pub struct Image { element: HtmlImageElement, bounding_box: Rect, } impl Image { pub fn new(element: HtmlImageElement, position: Point) -> Self { let bounding_box = Rect::new(position, element.width() as i16, element.height() as i16); Self { element, bounding_box, } } pub fn draw(&self, renderer: &Renderer) { renderer.draw_entire_image(&self.element, &self.bounding_box.position) } pub fn bounding_box(&self) -> &Rect { &self.bounding_box } pub fn move_horizontally(&mut self, distance: i16) { self.set_x(self.bounding_box.x() + distance); } pub fn set_x(&mut self, x: i16) { self.bounding_box.set_x(x); } pub fn right(&self) -> i16 { self.bounding_box.right() } } pub struct SpriteSheet { sheet: Sheet, image: HtmlImageElement, } impl SpriteSheet { pub fn new(sheet: Sheet, image: HtmlImageElement) -> Self { SpriteSheet { sheet, image } } pub fn cell(&self, name: &str) -> Option<&Cell> { self.sheet.frames.get(name) } pub fn draw(&self, renderer: &Renderer, source: &Rect, destination: &Rect) { renderer.draw_image(&self.image, source, destination); } } #[derive(Clone)] pub struct Audio { context: AudioContext, } #[derive(Clone)] pub struct Sound { pub buffer: AudioBuffer, } impl Audio { pub fn new() -> Result { Ok(Audio { context: sound::create_audio_context()?, }) } pub async fn load_sound(&self, filename: &str) -> Result { let array_buffer = browser::fetch_array_buffer(filename).await?; let audio_buffer = sound::decode_audio_data(&self.context, &array_buffer).await?; Ok(Sound { buffer: audio_buffer, }) } pub fn play_sound(&self, sound: &Sound) -> Result<()> { sound::play_sound(&self.context, &sound.buffer, sound::LOOPING::No) } pub fn play_looping_sound(&self, sound: &Sound) -> Result<()> { sound::play_sound(&self.context, &sound.buffer, sound::LOOPING::Yes) } } #[cfg(test)] mod tests { use super::*; #[test] fn two_rects_that_intersect_on_the_left() { let rect1 = Rect { position: Point { x: 10, y: 10 }, height: 100, width: 100, }; let rect2 = Rect { position: Point { x: 0, y: 10 }, height: 100, width: 100, }; assert_eq!(rect2.intersects(&rect1), true); } } ================================================ FILE: src/game.rs ================================================ use std::rc::Rc; use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::channel::mpsc::UnboundedReceiver; use rand::prelude::*; use web_sys::HtmlImageElement; use self::red_hat_boy_states::*; use crate::{ browser, engine::{ self, Audio, Cell, Game, Image, KeyState, Point, Rect, Renderer, Sheet, Sound, SpriteSheet, }, segments::*, }; const HEIGHT: i16 = 600; const TIMELINE_MINIMUM: i16 = 1000; const OBSTACLE_BUFFER: i16 = 20; pub struct WalkTheDog { machine: Option, } impl WalkTheDog { pub fn new() -> Self { WalkTheDog { machine: None } } } enum WalkTheDogStateMachine { Ready(WalkTheDogState), Walking(WalkTheDogState), GameOver(WalkTheDogState), } impl WalkTheDogStateMachine { fn new(walk: Walk) -> Self { WalkTheDogStateMachine::Ready(WalkTheDogState::new(walk)) } fn update(self, keystate: &KeyState) -> Self { match self { WalkTheDogStateMachine::Ready(state) => state.update(keystate).into(), WalkTheDogStateMachine::Walking(state) => state.update(keystate).into(), WalkTheDogStateMachine::GameOver(state) => state.update().into(), } } fn draw(&self, renderer: &Renderer) { match self { WalkTheDogStateMachine::Ready(state) => state.draw(renderer), WalkTheDogStateMachine::Walking(state) => state.draw(renderer), WalkTheDogStateMachine::GameOver(state) => state.draw(renderer), }; } } struct WalkTheDogState { _state: T, walk: Walk, } impl WalkTheDogState { fn draw(&self, renderer: &Renderer) { self.walk.draw(renderer); } } struct Ready; impl WalkTheDogState { fn new(walk: Walk) -> WalkTheDogState { browser::draw_ui("

0").unwrap(); WalkTheDogState { _state: Ready, walk, } } fn run_right(&mut self) { self.walk.boy.run_right(); } fn start_running(mut self) -> WalkTheDogState { self.run_right(); WalkTheDogState { _state: Walking, walk: self.walk, } } fn update(mut self, keystate: &KeyState) -> ReadyEndState { self.walk.boy.update(); if keystate.is_pressed("ArrowRight") { ReadyEndState::Complete(self.start_running()) } else { ReadyEndState::Continue(self) } } } enum ReadyEndState { Complete(WalkTheDogState), Continue(WalkTheDogState), } impl From for WalkTheDogStateMachine { fn from(state: ReadyEndState) -> Self { match state { ReadyEndState::Complete(walking) => walking.into(), ReadyEndState::Continue(ready) => ready.into(), } } } struct Walking; impl WalkTheDogState { fn end_game(self) -> WalkTheDogState { let receiver = browser::draw_ui("") .and_then(|_unit| browser::find_html_element_by_id("new_game")) .map(engine::add_click_handler) .unwrap(); WalkTheDogState { _state: GameOver { new_game_event: receiver, }, walk: self.walk, } } fn update(mut self, keystate: &KeyState) -> WalkingEndState { if keystate.is_pressed("Space") { self.walk.boy.jump(); } if keystate.is_pressed("ArrowDown") { self.walk.boy.slide(); } self.walk.boy.update(); let walking_speed = self.walk.velocity(); let [first_background, second_background] = &mut self.walk.backgrounds; first_background.move_horizontally(walking_speed); second_background.move_horizontally(walking_speed); if first_background.right() < 0 { first_background.set_x(second_background.right()); } if second_background.right() < 0 { second_background.set_x(first_background.right()); } self.walk.obstacles.retain(|obstacle| obstacle.right() > 0); self.walk.obstacles.iter_mut().for_each(|obstacle| { obstacle.move_horizontally(walking_speed); obstacle.check_intersection(&mut self.walk.boy); }); if self.walk.timeline < TIMELINE_MINIMUM { self.walk.generate_next_segment(); } else { self.walk.timeline += walking_speed; } self.walk.score += 1; if self.walk.knocked_out() { WalkingEndState::Complete(self.end_game()) } else { WalkingEndState::Continue(self) } } } enum WalkingEndState { Continue(WalkTheDogState), Complete(WalkTheDogState), } impl From for WalkTheDogStateMachine { fn from(state: WalkingEndState) -> Self { match state { WalkingEndState::Continue(walking) => walking.into(), WalkingEndState::Complete(game_over) => game_over.into(), } } } struct GameOver { new_game_event: UnboundedReceiver<()>, } impl WalkTheDogState { fn update(mut self) -> GameOverEndState { if self._state.new_game_pressed() { GameOverEndState::Complete(self.new_game()) } else { GameOverEndState::Continue(self) } } fn new_game(self) -> WalkTheDogState { if let Err(err) = browser::hide_ui() { error!("Error hiding the browser {:#?}", err); } WalkTheDogState { _state: Ready, walk: Walk::reset(self.walk), } } } enum GameOverEndState { Continue(WalkTheDogState), Complete(WalkTheDogState), } impl From for WalkTheDogStateMachine { fn from(state: GameOverEndState) -> Self { match state { GameOverEndState::Continue(game_over) => game_over.into(), GameOverEndState::Complete(ready) => ready.into(), } } } impl GameOver { fn new_game_pressed(&mut self) -> bool { matches!(self.new_game_event.try_next(), Ok(Some(()))) } } impl From> for WalkTheDogStateMachine { fn from(state: WalkTheDogState) -> Self { WalkTheDogStateMachine::Walking(state) } } impl From> for WalkTheDogStateMachine { fn from(state: WalkTheDogState) -> Self { WalkTheDogStateMachine::GameOver(state) } } impl From> for WalkTheDogStateMachine { fn from(state: WalkTheDogState) -> Self { WalkTheDogStateMachine::Ready(state) } } pub trait Obstacle { fn check_intersection(&self, boy: &mut RedHatBoy); fn draw(&self, renderer: &Renderer); fn move_horizontally(&mut self, x: i16); fn right(&self) -> i16; } pub struct Platform { sheet: Rc, sprites: Vec, bounding_boxes: Vec, position: Point, } impl Platform { pub fn new( sheet: Rc, position: Point, sprite_names: &[&str], bounding_boxes: &[Rect], ) -> Self { let sprites = sprite_names .iter() .filter_map(|sprite_name| sheet.cell(sprite_name).cloned()) .collect(); let bounding_boxes = bounding_boxes .iter() .map(|bounding_box| { Rect::new_from_x_y( bounding_box.x() + position.x, bounding_box.y() + position.y, bounding_box.width, bounding_box.height, ) }) .collect(); Platform { sheet, position, sprites, bounding_boxes, } } fn bounding_boxes(&self) -> &Vec { &self.bounding_boxes } } impl Obstacle for Platform { fn check_intersection(&self, boy: &mut RedHatBoy) { if let Some(box_to_land_on) = self .bounding_boxes() .iter() .find(|&bounding_box| boy.bounding_box().intersects(bounding_box)) { if boy.velocity_y() > 0 && boy.pos_y() < self.position.y { boy.land_on(box_to_land_on.y()); } else { boy.knock_out(); } } } fn draw(&self, renderer: &Renderer) { let mut x = 0; self.sprites.iter().for_each(|sprite| { self.sheet.draw( renderer, &Rect::new_from_x_y( sprite.frame.x, sprite.frame.y, sprite.frame.w, sprite.frame.h, ), // Just use position and the standard widths in the tileset &Rect::new_from_x_y( self.position.x + x, self.position.y, sprite.frame.w, sprite.frame.h, ), ); x += sprite.frame.w; }); } fn move_horizontally(&mut self, x: i16) { self.position.x += x; self.bounding_boxes.iter_mut().for_each(|bounding_box| { bounding_box.set_x(bounding_box.position.x + x); }) } fn right(&self) -> i16 { self.bounding_boxes() .last() .unwrap_or(&Rect::default()) .right() } } pub struct RedHatBoy { state_machine: RedHatBoyStateMachine, sprite_sheet: Sheet, image: HtmlImageElement, } impl RedHatBoy { fn new(sprite_sheet: Sheet, image: HtmlImageElement, audio: Audio, sound: Sound) -> Self { RedHatBoy { state_machine: RedHatBoyStateMachine::Idle(RedHatBoyState::new(audio, sound)), sprite_sheet, image, } } fn reset(boy: Self) -> Self { RedHatBoy::new( boy.sprite_sheet, boy.image, boy.state_machine.context().audio.clone(), boy.state_machine.context().jump_sound.clone(), ) } fn run_right(&mut self) { self.state_machine = self.state_machine.clone().transition(Event::Run); } fn slide(&mut self) { self.state_machine = self.state_machine.clone().transition(Event::Slide); } fn jump(&mut self) { self.state_machine = self.state_machine.clone().transition(Event::Jump); } fn knock_out(&mut self) { self.state_machine = self.state_machine.clone().transition(Event::KnockOut); } fn land_on(&mut self, position: i16) { self.state_machine = self.state_machine.clone().transition(Event::Land(position)); } fn update(&mut self) { self.state_machine = self.state_machine.clone().update(); } fn knocked_out(&self) -> bool { self.state_machine.knocked_out() } fn pos_y(&self) -> i16 { self.state_machine.context().position.y } fn velocity_y(&self) -> i16 { self.state_machine.context().velocity.y } fn walking_speed(&self) -> i16 { self.state_machine.context().velocity.x } fn frame_name(&self) -> String { format!( "{} ({}).png", self.state_machine.frame_name(), (self.state_machine.context().frame / 3) + 1 ) } fn current_sprite(&self) -> Option<&Cell> { self.sprite_sheet.frames.get(&self.frame_name()) } fn bounding_box(&self) -> Rect { const X_OFFSET: i16 = 18; const Y_OFFSET: i16 = 14; const WIDTH_OFFSET: i16 = 28; Rect::new_from_x_y( self.destination_box().x() + X_OFFSET, self.destination_box().y() + Y_OFFSET, self.destination_box().width - WIDTH_OFFSET, self.destination_box().height - Y_OFFSET, ) } fn destination_box(&self) -> Rect { let sprite = self.current_sprite().expect("Cell not found"); Rect::new_from_x_y( self.state_machine.context().position.x + sprite.sprite_source_size.x, self.state_machine.context().position.y + sprite.sprite_source_size.y, sprite.frame.w, sprite.frame.h, ) } fn draw(&self, renderer: &Renderer) { let sprite = self.current_sprite().expect("Cell not found"); renderer.draw_image( &self.image, &Rect::new_from_x_y( sprite.frame.x, sprite.frame.y, sprite.frame.w, sprite.frame.h, ), &self.destination_box(), ); } } #[derive(Clone)] enum RedHatBoyStateMachine { Idle(RedHatBoyState), Running(RedHatBoyState), Sliding(RedHatBoyState), Jumping(RedHatBoyState), Falling(RedHatBoyState), KnockedOut(RedHatBoyState), } pub enum Event { Run, Jump, Slide, KnockOut, Land(i16), Update, } impl RedHatBoyStateMachine { fn transition(self, event: Event) -> Self { match (self.clone(), event) { (RedHatBoyStateMachine::Idle(state), Event::Run) => state.run().into(), (RedHatBoyStateMachine::Running(state), Event::Jump) => state.jump().into(), (RedHatBoyStateMachine::Running(state), Event::Slide) => state.slide().into(), (RedHatBoyStateMachine::Running(state), Event::KnockOut) => state.knock_out().into(), (RedHatBoyStateMachine::Running(state), Event::Land(position)) => { state.land_on(position).into() } (RedHatBoyStateMachine::Jumping(state), Event::Land(position)) => { state.land_on(position).into() } (RedHatBoyStateMachine::Jumping(state), Event::KnockOut) => state.knock_out().into(), (RedHatBoyStateMachine::Sliding(state), Event::KnockOut) => state.knock_out().into(), (RedHatBoyStateMachine::Sliding(state), Event::Land(position)) => { state.land_on(position).into() } (RedHatBoyStateMachine::Idle(state), Event::Update) => state.update().into(), (RedHatBoyStateMachine::Running(state), Event::Update) => state.update().into(), (RedHatBoyStateMachine::Jumping(state), Event::Update) => state.update().into(), (RedHatBoyStateMachine::Sliding(state), Event::Update) => state.update().into(), (RedHatBoyStateMachine::Falling(state), Event::Update) => state.update().into(), _ => self, } } fn frame_name(&self) -> &str { match self { RedHatBoyStateMachine::Idle(state) => state.frame_name(), RedHatBoyStateMachine::Running(state) => state.frame_name(), RedHatBoyStateMachine::Jumping(state) => state.frame_name(), RedHatBoyStateMachine::Sliding(state) => state.frame_name(), RedHatBoyStateMachine::Falling(state) => state.frame_name(), RedHatBoyStateMachine::KnockedOut(state) => state.frame_name(), } } fn context(&self) -> &RedHatBoyContext { match self { RedHatBoyStateMachine::Idle(state) => state.context(), RedHatBoyStateMachine::Running(state) => state.context(), RedHatBoyStateMachine::Jumping(state) => state.context(), RedHatBoyStateMachine::Sliding(state) => state.context(), RedHatBoyStateMachine::Falling(state) => state.context(), RedHatBoyStateMachine::KnockedOut(state) => state.context(), } } fn knocked_out(&self) -> bool { matches!(self, RedHatBoyStateMachine::KnockedOut(_)) } fn update(self) -> Self { self.transition(Event::Update) } } impl From> for RedHatBoyStateMachine { fn from(state: RedHatBoyState) -> Self { RedHatBoyStateMachine::Idle(state) } } impl From> for RedHatBoyStateMachine { fn from(state: RedHatBoyState) -> Self { RedHatBoyStateMachine::Running(state) } } impl From> for RedHatBoyStateMachine { fn from(state: RedHatBoyState) -> Self { RedHatBoyStateMachine::Sliding(state) } } impl From> for RedHatBoyStateMachine { fn from(state: RedHatBoyState) -> Self { RedHatBoyStateMachine::Jumping(state) } } impl From> for RedHatBoyStateMachine { fn from(state: RedHatBoyState) -> Self { RedHatBoyStateMachine::Falling(state) } } impl From> for RedHatBoyStateMachine { fn from(state: RedHatBoyState) -> Self { RedHatBoyStateMachine::KnockedOut(state) } } impl From for RedHatBoyStateMachine { fn from(state: SlidingEndState) -> Self { match state { SlidingEndState::Sliding(sliding) => sliding.into(), SlidingEndState::Running(running) => running.into(), } } } impl From for RedHatBoyStateMachine { fn from(state: JumpingEndState) -> Self { match state { JumpingEndState::Jumping(jumping) => jumping.into(), JumpingEndState::Landing(landing) => landing.into(), } } } impl From for RedHatBoyStateMachine { fn from(state: FallingEndState) -> Self { match state { FallingEndState::Falling(falling) => falling.into(), FallingEndState::KnockedOut(knocked_out) => knocked_out.into(), } } } mod red_hat_boy_states { use super::{Audio, Sound, HEIGHT}; use crate::engine::Point; const FLOOR: i16 = 479; const PLAYER_HEIGHT: i16 = HEIGHT - FLOOR; const RUNNING_SPEED: i16 = 4; const STARTING_POINT: i16 = -20; const IDLE_FRAMES: u8 = 29; const RUNNING_FRAMES: u8 = 23; const JUMPING_FRAMES: u8 = 35; const SLIDING_FRAMES: u8 = 14; const FALLING_FRAMES: u8 = 29; const IDLE_FRAME_NAME: &str = "Idle"; const RUN_FRAME_NAME: &str = "Run"; const SLIDING_FRAME_NAME: &str = "Slide"; const JUMPING_FRAME_NAME: &str = "Jump"; const FALLING_FRAME_NAME: &str = "Dead"; const JUMP_SPEED: i16 = -25; const GRAVITY: i16 = 1; const TERMINAL_VELOCITY: i16 = 20; #[derive(Clone)] pub struct RedHatBoyState { context: RedHatBoyContext, _state: S, } impl RedHatBoyState { pub fn context(&self) -> &RedHatBoyContext { &self.context } fn update_context(&mut self, frames: u8) { self.context = self.context.clone().update(frames); } } #[derive(Copy, Clone)] pub struct Idle; impl RedHatBoyState { pub fn new(audio: Audio, jump_sound: Sound) -> Self { RedHatBoyState { context: RedHatBoyContext { frame: 0, position: Point { x: STARTING_POINT, y: FLOOR, }, velocity: Point { x: 0, y: 0 }, audio, jump_sound, }, _state: Idle {}, } } pub fn frame_name(&self) -> &str { IDLE_FRAME_NAME } pub fn update(mut self) -> RedHatBoyState { self.update_context(IDLE_FRAMES); self } pub fn run(self) -> RedHatBoyState { RedHatBoyState { context: self.context.reset_frame().run_right(), _state: Running {}, } } } #[derive(Copy, Clone)] pub struct Running; impl RedHatBoyState { pub fn frame_name(&self) -> &str { RUN_FRAME_NAME } pub fn update(mut self) -> RedHatBoyState { self.update_context(RUNNING_FRAMES); self } pub fn jump(self) -> RedHatBoyState { RedHatBoyState { context: self .context .reset_frame() .set_vertical_velocity(JUMP_SPEED) .play_jump_sound(), _state: Jumping {}, } } pub fn slide(self) -> RedHatBoyState { RedHatBoyState { context: self.context.reset_frame(), _state: Sliding {}, } } pub fn knock_out(self) -> RedHatBoyState { RedHatBoyState { context: self.context.reset_frame().stop(), _state: Falling {}, } } pub fn land_on(self, position: i16) -> RedHatBoyState { RedHatBoyState { context: self.context.set_on(position), _state: Running {}, } } } #[derive(Copy, Clone)] pub struct Jumping; pub enum JumpingEndState { Jumping(RedHatBoyState), Landing(RedHatBoyState), } impl RedHatBoyState { pub fn frame_name(&self) -> &str { JUMPING_FRAME_NAME } pub fn knock_out(self) -> RedHatBoyState { RedHatBoyState { context: self.context.reset_frame().stop(), _state: Falling {}, } } pub fn update(mut self) -> JumpingEndState { self.update_context(JUMPING_FRAMES); if self.context.position.y >= FLOOR { JumpingEndState::Landing(self.land_on(HEIGHT)) } else { JumpingEndState::Jumping(self) } } pub fn land_on(self, position: i16) -> RedHatBoyState { RedHatBoyState { context: self.context.reset_frame().set_on(position), _state: Running, } } } #[derive(Copy, Clone)] pub struct Sliding; pub enum SlidingEndState { Sliding(RedHatBoyState), Running(RedHatBoyState), } impl RedHatBoyState { pub fn frame_name(&self) -> &str { SLIDING_FRAME_NAME } pub fn stand(self) -> RedHatBoyState { RedHatBoyState { context: self.context.reset_frame(), _state: Running {}, } } pub fn knock_out(self) -> RedHatBoyState { RedHatBoyState { context: self.context.reset_frame().stop(), _state: Falling {}, } } pub fn update(mut self) -> SlidingEndState { self.update_context(SLIDING_FRAMES); if self.context.frame >= SLIDING_FRAMES { SlidingEndState::Running(self.stand()) } else { SlidingEndState::Sliding(self) } } pub fn land_on(self, position: i16) -> RedHatBoyState { RedHatBoyState { context: self.context.set_on(position), _state: Sliding {}, } } } #[derive(Copy, Clone)] pub struct Falling; impl RedHatBoyState { pub fn frame_name(&self) -> &str { FALLING_FRAME_NAME } pub fn knock_out(self) -> RedHatBoyState { RedHatBoyState { context: self.context, _state: KnockedOut {}, } } pub fn update(mut self) -> FallingEndState { self.update_context(FALLING_FRAMES); if self.context.frame >= FALLING_FRAMES { FallingEndState::KnockedOut(self.knock_out()) } else { FallingEndState::Falling(self) } } } pub enum FallingEndState { KnockedOut(RedHatBoyState), Falling(RedHatBoyState), } #[derive(Copy, Clone)] pub struct KnockedOut; impl RedHatBoyState { pub fn frame_name(&self) -> &str { FALLING_FRAME_NAME } } #[derive(Clone)] pub struct RedHatBoyContext { pub frame: u8, pub position: Point, pub velocity: Point, pub audio: Audio, pub jump_sound: Sound, } impl RedHatBoyContext { pub fn update(mut self, frame_count: u8) -> Self { if self.velocity.y < TERMINAL_VELOCITY { self.velocity.y += GRAVITY; } if self.frame < frame_count { self.frame += 1; } else { self.frame = 0; } self.position.y += self.velocity.y; if self.position.y > FLOOR { self.position.y = FLOOR; } self } fn reset_frame(mut self) -> Self { self.frame = 0; self } fn set_vertical_velocity(mut self, y: i16) -> Self { self.velocity.y = y; self } fn run_right(mut self) -> Self { self.velocity.x += RUNNING_SPEED; self } fn stop(mut self) -> Self { self.velocity.x = 0; self.velocity.y = 0; self } fn set_on(mut self, position: i16) -> Self { let position = position - PLAYER_HEIGHT; self.position.y = position; self } fn play_jump_sound(self) -> Self { if let Err(err) = self.audio.play_sound(&self.jump_sound) { log!("Error playing jump sound {:#?}", err); } self } } } pub struct Walk { obstacle_sheet: Rc, stone: HtmlImageElement, boy: RedHatBoy, backgrounds: [Image; 2], obstacles: Vec>, timeline: i16, score: u16, } impl Walk { fn knocked_out(&self) -> bool { self.boy.knocked_out() } fn reset(walk: Self) -> Self { let starting_obstacles = stone_and_platform(walk.stone.clone(), walk.obstacle_sheet.clone(), 0); let timeline = rightmost(&starting_obstacles); Walk { boy: RedHatBoy::reset(walk.boy), backgrounds: walk.backgrounds, obstacles: starting_obstacles, obstacle_sheet: walk.obstacle_sheet, stone: walk.stone, score: 0, timeline, } } fn draw(&self, renderer: &Renderer) { self.backgrounds.iter().for_each(|background| { background.draw(renderer); }); self.boy.draw(renderer); self.obstacles.iter().for_each(|obstacle| { obstacle.draw(renderer); }); browser::find_html_element_by_id("score") .map(|element| element.set_inner_html(&format!("Score: {}", self.score))) .unwrap(); } fn velocity(&self) -> i16 { -self.boy.walking_speed() } fn generate_next_segment(&mut self) { let mut rng = thread_rng(); let next_segment = rng.gen_range(0..2); let mut next_obstacles = match next_segment { 0 => stone_and_platform( self.stone.clone(), self.obstacle_sheet.clone(), self.timeline + OBSTACLE_BUFFER, ), 1 => platform_and_stone( self.stone.clone(), self.obstacle_sheet.clone(), self.timeline + OBSTACLE_BUFFER, ), _ => vec![], }; self.timeline = rightmost(&next_obstacles); self.obstacles.append(&mut next_obstacles); } } pub struct Barrier { image: Image, } impl Barrier { pub fn new(image: Image) -> Self { Barrier { image } } } impl Obstacle for Barrier { fn check_intersection(&self, boy: &mut RedHatBoy) { if boy.bounding_box().intersects(self.image.bounding_box()) { boy.knock_out() } } fn draw(&self, renderer: &Renderer) { self.image.draw(renderer); } fn move_horizontally(&mut self, x: i16) { self.image.move_horizontally(x); } fn right(&self) -> i16 { self.image.right() } } #[async_trait(?Send)] impl Game for WalkTheDog { async fn initialize(&self) -> Result> { match self.machine { None => { let sheet = browser::fetch_json("rhb.json") .await? .into_serde::()?; let background = engine::load_image("BG.png").await?; let stone = engine::load_image("Stone.png").await?; let tiles = browser::fetch_json("tiles.json").await?; let sprite_sheet = Rc::new(SpriteSheet::new( tiles.into_serde::()?, engine::load_image("tiles.png").await?, )); let audio = Audio::new()?; let sound = audio.load_sound("SFX_Jump_23.mp3").await?; let background_music = audio.load_sound("background_song.mp3").await?; audio.play_looping_sound(&background_music)?; let rhb = RedHatBoy::new(sheet, engine::load_image("rhb.png").await?, audio, sound); let background_width = background.width() as i16; let starting_obstacles = stone_and_platform(stone.clone(), sprite_sheet.clone(), 0); let timeline = rightmost(&starting_obstacles); let machine = WalkTheDogStateMachine::new(Walk { boy: rhb, backgrounds: [ Image::new(background.clone(), Point { x: 0, y: 0 }), Image::new( background, Point { x: background_width, y: 0, }, ), ], obstacles: starting_obstacles, obstacle_sheet: sprite_sheet, score: 0, stone, timeline, }); Ok(Box::new(WalkTheDog { machine: Some(machine), })) } Some(_) => Err(anyhow!("Error: Game is already initialized!")), } } fn update(&mut self, keystate: &KeyState) { if let Some(machine) = self.machine.take() { self.machine.replace(machine.update(keystate)); } assert!(self.machine.is_some()); } fn draw(&self, renderer: &Renderer) { renderer.clear(&Rect::new(Point { x: 0, y: 0 }, 600, HEIGHT)); if let Some(machine) = &self.machine { machine.draw(renderer); } } } fn rightmost(obstacle_list: &[Box]) -> i16 { obstacle_list .iter() .map(|obstacle| obstacle.right()) .max_by(|x, y| x.cmp(y)) .unwrap_or(0) } #[cfg(test)] mod tests { use super::*; use futures::channel::mpsc::unbounded; use std::collections::HashMap; use web_sys::{AudioBuffer, AudioBufferOptions}; use wasm_bindgen_test::wasm_bindgen_test; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] fn test_transition_from_game_over_to_new_game() { let (_, receiver) = unbounded(); let image = HtmlImageElement::new().unwrap(); let audio = Audio::new().unwrap(); let options = AudioBufferOptions::new(1, 3000.0); let sound = Sound { buffer: AudioBuffer::new(&options).unwrap(), }; let rhb = RedHatBoy::new( Sheet { frames: HashMap::new(), }, image.clone(), audio, sound, ); let sprite_sheet = SpriteSheet::new( Sheet { frames: HashMap::new(), }, image.clone(), ); let walk = Walk { boy: rhb, backgrounds: [ Image::new(image.clone(), Point { x: 0, y: 0 }), Image::new(image.clone(), Point { x: 0, y: 0 }), ], obstacles: vec![], obstacle_sheet: Rc::new(sprite_sheet), stone: image.clone(), score: 0, timeline: 0, }; let document = browser::document().unwrap(); document .body() .unwrap() .insert_adjacent_html("afterbegin", "

") .unwrap(); browser::draw_ui("

This is the UI

").unwrap(); let state = WalkTheDogState { _state: GameOver { new_game_event: receiver, }, walk: walk, }; state.new_game(); let ui = browser::find_html_element_by_id("ui").unwrap(); assert_eq!(ui.child_element_count(), 0); } } ================================================ FILE: src/lib.rs ================================================ #[macro_use] mod browser; mod engine; mod game; mod segments; mod sound; use engine::GameLoop; use game::WalkTheDog; use wasm_bindgen::prelude::*; // This is like the `main` function, except for JavaScript. #[wasm_bindgen(start)] pub fn main_js() -> Result<(), JsValue> { console_error_panic_hook::set_once(); browser::spawn_local(async move { let game = WalkTheDog::new(); GameLoop::start(game) .await .expect("Could not start game loop"); }); Ok(()) } ================================================ FILE: src/segments.rs ================================================ use std::rc::Rc; use web_sys::HtmlImageElement; use crate::engine::{Image, Point, Rect, SpriteSheet}; use crate::game::{Barrier, Obstacle, Platform}; const LOW_PLATFORM: i16 = 420; const HIGH_PLATFORM: i16 = 375; const FIRST_PLATFORM: i16 = 370; const STONE_ON_GROUND: i16 = 546; const FLOATING_PLATFORM_SPRITES: [&str; 3] = ["13.png", "14.png", "15.png"]; const PLATFORM_WIDTH: i16 = 384; const PLATFORM_HEIGHT: i16 = 93; const PLATFORM_EDGE_WIDTH: i16 = 60; const PLATFORM_EDGE_HEIGHT: i16 = 54; const FLOATING_PLATFORM_BOUNDING_BOXES: [Rect; 3] = [ Rect::new_from_x_y(0, 0, PLATFORM_EDGE_WIDTH, PLATFORM_EDGE_HEIGHT), Rect::new_from_x_y( PLATFORM_EDGE_WIDTH, 0, PLATFORM_WIDTH - (PLATFORM_EDGE_WIDTH * 2), PLATFORM_HEIGHT, ), Rect::new_from_x_y( PLATFORM_WIDTH - PLATFORM_EDGE_WIDTH, 0, PLATFORM_EDGE_WIDTH, PLATFORM_EDGE_HEIGHT, ), ]; fn create_floating_platform(sprite_sheet: Rc, position: Point) -> Platform { Platform::new( sprite_sheet, position, &FLOATING_PLATFORM_SPRITES, &FLOATING_PLATFORM_BOUNDING_BOXES, ) } pub fn stone_and_platform( stone: HtmlImageElement, sprite_sheet: Rc, offset_x: i16, ) -> Vec> { const INITIAL_STONE_OFFSET: i16 = 150; vec![ Box::new(Barrier::new(Image::new( stone, Point { x: offset_x + INITIAL_STONE_OFFSET, y: STONE_ON_GROUND, }, ))), Box::new(create_floating_platform( sprite_sheet, Point { x: offset_x + FIRST_PLATFORM, y: LOW_PLATFORM, }, )), ] } pub fn platform_and_stone( stone: HtmlImageElement, sprite_sheet: Rc, offset_x: i16, ) -> Vec> { const INITIAL_STONE_OFFSET: i16 = 400; const INITIAL_PLATFORM_OFFSET: i16 = 200; vec![ Box::new(Barrier::new(Image::new( stone, Point { x: offset_x + INITIAL_STONE_OFFSET, y: STONE_ON_GROUND, }, ))), Box::new(create_floating_platform( sprite_sheet, Point { x: offset_x + INITIAL_PLATFORM_OFFSET, y: HIGH_PLATFORM, }, )), ] } ================================================ FILE: src/sound.rs ================================================ use anyhow::{anyhow, Result}; use js_sys::ArrayBuffer; use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; use web_sys::{AudioBuffer, AudioBufferSourceNode, AudioContext, AudioDestinationNode, AudioNode}; pub fn create_audio_context() -> Result { AudioContext::new().map_err(|err| anyhow!("Could not create audio context: {:#?}", err)) } fn create_buffer_source(ctx: &AudioContext) -> Result { ctx.create_buffer_source() .map_err(|err| anyhow!("Error creating buffer source {:#?}", err)) } fn connect_with_audio_node( buffer_source: &AudioBufferSourceNode, destination: &AudioDestinationNode, ) -> Result { buffer_source .connect_with_audio_node(destination) .map_err(|err| anyhow!("Error connecting audio source to destination {:#?}", err)) } fn create_track_source(ctx: &AudioContext, buffer: &AudioBuffer) -> Result { let track_source = create_buffer_source(ctx)?; track_source.set_buffer(Some(buffer)); connect_with_audio_node(&track_source, &ctx.destination())?; Ok(track_source) } pub enum LOOPING { No, Yes, } pub fn play_sound(ctx: &AudioContext, buffer: &AudioBuffer, looping: LOOPING) -> Result<()> { let track_source = create_track_source(ctx, buffer)?; if matches!(looping, LOOPING::Yes) { track_source.set_loop(true); } track_source .start() .map_err(|err| anyhow!("Could not start sound! {:#?}", err)) } pub async fn decode_audio_data( ctx: &AudioContext, array_buffer: &ArrayBuffer, ) -> Result { JsFuture::from( ctx.decode_audio_data(array_buffer) .map_err(|err| anyhow!("Could not decode audio from array buffer {:#?}", err))?, ) .await .map_err(|err| anyhow!("Could not convert promise to future {:#?}", err))? .dyn_into() .map_err(|err| anyhow!("Could not cast into AudioBuffer {:#?}", err)) } ================================================ FILE: src/test_browser.rs ================================================ ================================================ FILE: static/index.html ================================================ My Rust + Webpack project!
Your browser does not support the Canvas. ================================================ FILE: static/rhb.json ================================================ {"frames": { "Dead (1).png": { "frame": {"x":0,"y":0,"w":71,"h":115}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":58,"y":8,"w":71,"h":115}, "sourceSize": {"w":160,"h":136} }, "Dead (2).png": { "frame": {"x":117,"y":0,"w":87,"h":114}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":45,"y":9,"w":87,"h":114}, "sourceSize": {"w":160,"h":136} }, "Dead (3).png": { "frame": {"x":234,"y":0,"w":97,"h":106}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":35,"y":18,"w":97,"h":106}, "sourceSize": {"w":160,"h":136} }, "Dead (4).png": { "frame": {"x":351,"y":0,"w":105,"h":91}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":22,"y":32,"w":105,"h":91}, "sourceSize": {"w":160,"h":136} }, "Dead (5).png": { "frame": {"x":468,"y":0,"w":107,"h":83}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":19,"y":45,"w":107,"h":83}, "sourceSize": {"w":160,"h":136} }, "Dead (6).png": { "frame": {"x":585,"y":0,"w":107,"h":70}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":17,"y":58,"w":107,"h":70}, "sourceSize": {"w":160,"h":136} }, "Dead (7).png": { "frame": {"x":702,"y":0,"w":109,"h":67}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":15,"y":59,"w":109,"h":67}, "sourceSize": {"w":160,"h":136} }, "Dead (8).png": { "frame": {"x":819,"y":0,"w":110,"h":68}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":13,"y":61,"w":110,"h":68}, "sourceSize": {"w":160,"h":136} }, "Dead (9).png": { "frame": {"x":936,"y":0,"w":115,"h":68}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":13,"y":61,"w":115,"h":68}, "sourceSize": {"w":160,"h":136} }, "Dead (10).png": { "frame": {"x":1053,"y":0,"w":117,"h":68}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":13,"y":61,"w":117,"h":68}, "sourceSize": {"w":160,"h":136} }, "Hurt (1).png": { "frame": {"x":1170,"y":0,"w":71,"h":115}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":58,"y":8,"w":71,"h":115}, "sourceSize": {"w":160,"h":136} }, "Hurt (2).png": { "frame": {"x":1287,"y":0,"w":69,"h":112}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":67,"y":11,"w":69,"h":112}, "sourceSize": {"w":160,"h":136} }, "Hurt (3).png": { "frame": {"x":1404,"y":0,"w":64,"h":103}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":78,"y":17,"w":64,"h":103}, "sourceSize": {"w":160,"h":136} }, "Hurt (4).png": { "frame": {"x":1521,"y":0,"w":63,"h":102}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":79,"y":18,"w":63,"h":102}, "sourceSize": {"w":160,"h":136} }, "Hurt (5).png": { "frame": {"x":1638,"y":0,"w":64,"h":102}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":79,"y":18,"w":64,"h":102}, "sourceSize": {"w":160,"h":136} }, "Hurt (6).png": { "frame": {"x":1755,"y":0,"w":64,"h":101}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":79,"y":19,"w":64,"h":101}, "sourceSize": {"w":160,"h":136} }, "Hurt (7).png": { "frame": {"x":1872,"y":0,"w":65,"h":101}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":79,"y":19,"w":65,"h":101}, "sourceSize": {"w":160,"h":136} }, "Hurt (8).png": { "frame": {"x":0,"y":122,"w":68,"h":111}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":69,"y":12,"w":68,"h":111}, "sourceSize": {"w":160,"h":136} }, "Idle (1).png": { "frame": {"x":117,"y":122,"w":71,"h":115}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":58,"y":8,"w":71,"h":115}, "sourceSize": {"w":160,"h":136} }, "Idle (2).png": { "frame": {"x":234,"y":122,"w":71,"h":115}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":58,"y":8,"w":71,"h":115}, "sourceSize": {"w":160,"h":136} }, "Idle (3).png": { "frame": {"x":351,"y":122,"w":70,"h":114}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":59,"y":9,"w":70,"h":114}, "sourceSize": {"w":160,"h":136} }, "Idle (4).png": { "frame": {"x":468,"y":122,"w":70,"h":114}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":59,"y":9,"w":70,"h":114}, "sourceSize": {"w":160,"h":136} }, "Idle (5).png": { "frame": {"x":585,"y":122,"w":70,"h":113}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":59,"y":10,"w":70,"h":113}, "sourceSize": {"w":160,"h":136} }, "Idle (6).png": { "frame": {"x":702,"y":122,"w":71,"h":113}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":59,"y":10,"w":71,"h":113}, "sourceSize": {"w":160,"h":136} }, "Idle (7).png": { "frame": {"x":819,"y":122,"w":71,"h":113}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":59,"y":10,"w":71,"h":113}, "sourceSize": {"w":160,"h":136} }, "Idle (8).png": { "frame": {"x":936,"y":122,"w":70,"h":113}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":59,"y":10,"w":70,"h":113}, "sourceSize": {"w":160,"h":136} }, "Idle (9).png": { "frame": {"x":1053,"y":122,"w":70,"h":114}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":59,"y":9,"w":70,"h":114}, "sourceSize": {"w":160,"h":136} }, "Idle (10).png": { "frame": {"x":1170,"y":122,"w":70,"h":114}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":59,"y":9,"w":70,"h":114}, "sourceSize": {"w":160,"h":136} }, "Jump (1).png": { "frame": {"x":1287,"y":122,"w":71,"h":115}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":58,"y":8,"w":71,"h":115}, "sourceSize": {"w":160,"h":136} }, "Jump (2).png": { "frame": {"x":1404,"y":122,"w":70,"h":110}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":69,"y":13,"w":70,"h":110}, "sourceSize": {"w":160,"h":136} }, "Jump (3).png": { "frame": {"x":1521,"y":122,"w":69,"h":109}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":72,"y":14,"w":69,"h":109}, "sourceSize": {"w":160,"h":136} }, "Jump (4).png": { "frame": {"x":1638,"y":122,"w":70,"h":119}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":58,"y":3,"w":70,"h":119}, "sourceSize": {"w":160,"h":136} }, "Jump (5).png": { "frame": {"x":1755,"y":122,"w":71,"h":119}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":58,"y":3,"w":71,"h":119}, "sourceSize": {"w":160,"h":136} }, "Jump (6).png": { "frame": {"x":1872,"y":122,"w":70,"h":119}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":59,"y":3,"w":70,"h":119}, "sourceSize": {"w":160,"h":136} }, "Jump (7).png": { "frame": {"x":0,"y":244,"w":70,"h":119}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":59,"y":3,"w":70,"h":119}, "sourceSize": {"w":160,"h":136} }, "Jump (8).png": { "frame": {"x":117,"y":244,"w":71,"h":119}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":58,"y":3,"w":71,"h":119}, "sourceSize": {"w":160,"h":136} }, "Jump (9).png": { "frame": {"x":234,"y":244,"w":70,"h":119}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":58,"y":3,"w":70,"h":119}, "sourceSize": {"w":160,"h":136} }, "Jump (10).png": { "frame": {"x":351,"y":244,"w":69,"h":114}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":64,"y":6,"w":69,"h":114}, "sourceSize": {"w":160,"h":136} }, "Jump (11).png": { "frame": {"x":468,"y":244,"w":73,"h":109}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":64,"y":11,"w":73,"h":109}, "sourceSize": {"w":160,"h":136} }, "Jump (12).png": { "frame": {"x":585,"y":244,"w":68,"h":111}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":67,"y":11,"w":68,"h":111}, "sourceSize": {"w":160,"h":136} }, "Run (1).png": { "frame": {"x":702,"y":244,"w":71,"h":115}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":58,"y":8,"w":71,"h":115}, "sourceSize": {"w":160,"h":136} }, "Run (2).png": { "frame": {"x":819,"y":244,"w":75,"h":122}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":55,"y":5,"w":75,"h":122}, "sourceSize": {"w":160,"h":136} }, "Run (3).png": { "frame": {"x":936,"y":244,"w":75,"h":117}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":56,"y":4,"w":75,"h":117}, "sourceSize": {"w":160,"h":136} }, "Run (4).png": { "frame": {"x":1053,"y":244,"w":71,"h":113}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":57,"y":7,"w":71,"h":113}, "sourceSize": {"w":160,"h":136} }, "Run (5).png": { "frame": {"x":1170,"y":244,"w":71,"h":115}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":58,"y":8,"w":71,"h":115}, "sourceSize": {"w":160,"h":136} }, "Run (6).png": { "frame": {"x":1287,"y":244,"w":70,"h":120}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":57,"y":6,"w":70,"h":120}, "sourceSize": {"w":160,"h":136} }, "Run (7).png": { "frame": {"x":1404,"y":244,"w":71,"h":115}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":55,"y":5,"w":71,"h":115}, "sourceSize": {"w":160,"h":136} }, "Run (8).png": { "frame": {"x":1521,"y":244,"w":70,"h":115}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":57,"y":6,"w":70,"h":115}, "sourceSize": {"w":160,"h":136} }, "Slide (1).png": { "frame": {"x":1638,"y":244,"w":85,"h":100}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":45,"y":28,"w":85,"h":100}, "sourceSize": {"w":160,"h":136} }, "Slide (2).png": { "frame": {"x":1755,"y":244,"w":86,"h":100}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":44,"y":27,"w":86,"h":100}, "sourceSize": {"w":160,"h":136} }, "Slide (3).png": { "frame": {"x":1872,"y":244,"w":87,"h":98}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":43,"y":27,"w":87,"h":98}, "sourceSize": {"w":160,"h":136} }, "Slide (4).png": { "frame": {"x":1872,"y":244,"w":87,"h":98}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":43,"y":27,"w":87,"h":98}, "sourceSize": {"w":160,"h":136} }, "Slide (5).png": { "frame": {"x":1755,"y":244,"w":86,"h":100}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":44,"y":27,"w":86,"h":100}, "sourceSize": {"w":160,"h":136} }}, "meta": { "app": "https://www.codeandweb.com/texturepacker", "version": "1.0", "image": "rhb_trimmed.png", "format": "RGBA8888", "size": {"w":1989,"h":366}, "scale": "1", "smartupdate": "$TexturePacker:SmartUpdate:57b52b5f31c0bdebc34af7514c40da17:cbdcd04de8b7f111714940a6eac7b511:521d204853d0d2bba515b142dc3ea799$" } } ================================================ FILE: static/styles.css ================================================ #ui { position: absolute; } @font-face { font-family: 'Ken Future'; src: url('kenney_future_narrow-webfont.woff2'); } #score { font-family: 'Ken Future'; font-size: 16pt; width: 200px; position: absolute; left: 400px; top: 40px; } button { font-family: 'Ken Future'; background: -72px -60px url('Button.svg'); border: none; width: 82px; height: 33px; position: absolute; transform: scale(1.8) translate(150px, 100px); } button:hover { background: -158px -60px url('Button.svg'); } button:active { background: -244px -60px url('Button.svg'); } ================================================ FILE: static/tiles.json ================================================ {"frames": { "1.png": { "frame": {"x":1,"y":132,"w":128,"h":128}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, "sourceSize": {"w":128,"h":128} }, "2.png": { "frame": {"x":262,"y":1,"w":128,"h":128}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, "sourceSize": {"w":128,"h":128} }, "3.png": { "frame": {"x":261,"y":261,"w":128,"h":128}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, "sourceSize": {"w":128,"h":128} }, "4.png": { "frame": {"x":1,"y":1,"w":129,"h":129}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":129,"h":129}, "sourceSize": {"w":129,"h":129} }, "5.png": { "frame": {"x":392,"y":1,"w":128,"h":128}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, "sourceSize": {"w":128,"h":128} }, "6.png": { "frame": {"x":391,"y":131,"w":128,"h":128}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, "sourceSize": {"w":128,"h":128} }, "7.png": { "frame": {"x":521,"y":131,"w":128,"h":128}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, "sourceSize": {"w":128,"h":128} }, "8.png": { "frame": {"x":391,"y":261,"w":128,"h":128}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, "sourceSize": {"w":128,"h":128} }, "9.png": { "frame": {"x":521,"y":261,"w":128,"h":128}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, "sourceSize": {"w":128,"h":128} }, "10.png": { "frame": {"x":1,"y":262,"w":128,"h":128}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, "sourceSize": {"w":128,"h":128} }, "11.png": { "frame": {"x":131,"y":132,"w":128,"h":128}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, "sourceSize": {"w":128,"h":128} }, "12.png": { "frame": {"x":132,"y":1,"w":128,"h":128}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, "sourceSize": {"w":128,"h":128} }, "13.png": { "frame": {"x":261,"y":391,"w":128,"h":93}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":128,"h":93}, "sourceSize": {"w":128,"h":93} }, "14.png": { "frame": {"x":391,"y":391,"w":128,"h":93}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":128,"h":93}, "sourceSize": {"w":128,"h":93} }, "15.png": { "frame": {"x":521,"y":391,"w":128,"h":93}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":128,"h":93}, "sourceSize": {"w":128,"h":93} }, "16.png": { "frame": {"x":131,"y":262,"w":128,"h":128}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, "sourceSize": {"w":128,"h":128} }, "17.png": { "frame": {"x":522,"y":1,"w":128,"h":99}, "rotated": true, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":128,"h":99}, "sourceSize": {"w":128,"h":99} }, "18.png": { "frame": {"x":261,"y":131,"w":128,"h":128}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, "sourceSize": {"w":128,"h":128} }}, "meta": { "app": "https://www.codeandweb.com/texturepacker", "version": "1.0", "image": "tiles.png", "format": "RGBA8888", "size": {"w":650,"h":485}, "scale": "1", "smartupdate": "$TexturePacker:SmartUpdate:6e3fdfd4ed3d5bfdbef834bd6d5c9225:fb784722f87c0e64fd62408e9c7c372e:accbe1e7e294ded8391337fc1c446319$" } } ================================================ FILE: tests/app.rs ================================================ use futures::prelude::*; use wasm_bindgen::JsValue; use wasm_bindgen_futures::JsFuture; use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; wasm_bindgen_test_configure!(run_in_browser); // This runs a unit test in native Rust, so it can only use Rust APIs. #[test] fn rust_test() { assert_eq!(1, 1); } // This runs a unit test in the browser, so it can use browser APIs. #[wasm_bindgen_test] fn web_test() { assert_eq!(1, 1); } ================================================ FILE: webpack.config.js ================================================ const path = require("path"); const CopyPlugin = require("copy-webpack-plugin"); const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); const dist = path.resolve(__dirname, "dist"); module.exports = { mode: "production", entry: { index: "./js/index.js" }, output: { path: dist, filename: "[name].js" }, devServer: { contentBase: dist, }, plugins: [ new CopyPlugin([ path.resolve(__dirname, "static") ]), new WasmPackPlugin({ crateDirectory: __dirname, }), ] };