diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 00000000..8dd7ccc1
Binary files /dev/null and b/.DS_Store differ
diff --git a/README.md b/README.md
index 71a9296d..fe54658c 100644
--- a/README.md
+++ b/README.md
@@ -1,112 +1,93 @@
-# CIS700 Procedural Graphics: Final Project
+# Procedural Pokemon
+[Demo](https://davlia.github.io/procedural-pokemon)
-Time to show off your new bag of procedural tricks by creating one polished final project. For this assignment you will have four weeks to create and document a portfolio piece that demonstrates your mastery of procedural thinking and implementation. You may work in groups of up to three (working alone is fine too). You may use any language / platform you choose for this assignment (given our approval if it’s not JavaScript/WebGL or C++/OpenGL).
-
-As usual with this class, we want to encourage you to take this opportunity to explore and experiment. To get you started, however, we’ve provided a few open-ended prompts below. Interesting and complex visuals are the goal in all of these prompts, but they encourage focus on different aspects of proceduralism.
-
-## Prompts:
-
-- ### A classic 4k demo
- * In the spirit of the demo scene, create an animation that fits into a 4k executable that runs in real-time. Feel free to take inspiration from the many existing demos. Focus on efficiency and elegance in your implementation.
- * Examples: [cdak by Quite & orange](https://www.youtube.com/watch?v=RCh3Q08HMfs&list=PLA5E2FF8E143DA58C)
-
-- ### A forgery
- * Taking inspiration from a particular natural phenomenon or distinctive set of visuals, implement a detailed, procedural recreation of that aesthetic. This includes modeling, texturing and object placement within your scene. Does not need to be real-time. Focus on detail and visual accuracy in your implementation.
- * Examples:
- - [Snail](https://www.shadertoy.com/view/ld3Gz2), [Journey](https://www.shadertoy.com/view/ldlcRf), Big Hero 6 Wormhole: [Image 1](http://2.bp.blogspot.com/-R-6AN2cWjwg/VTyIzIQSQfI/AAAAAAAABLA/GC0yzzz4wHw/s1600/big-hero-6-disneyscreencaps.com-10092.jpg) , [Image 2](https://i.stack.imgur.com/a9RGL.jpg)
-
-- ### A game level
- * Like generations of game makers before us, create a game which generates an navigable environment (eg. a roguelike dungeon, platforms) and some sort of goal or conflict (eg. enemy agents to avoid or items to collect). Must run in real-time. Aim to create an experience that will challenge players and vary noticeably in different playthroughs, whether that means complex dungeon generation, careful resource management or a sophisticated AI model. Focus on designing a system that will generate complex challenges and goals.
- * Examples: Spore, Dwarf Fortress, Minecraft, Rogue
-
-- ### An animated environment / music visualizer
- * Create an environment full of interactive procedural animation. The goal of this project is to create an environment that feels responsive and alive. Whether or not animations are musically-driven, sound should be an important component. Focus on user interactions, motion design and experimental interfaces.
- * Examples: [Panoramical](https://www.youtube.com/watch?v=gBTTMNFXHTk), [Bound](https://www.youtube.com/watch?v=aE37l6RvF-c)
-- ### Own proposal
- * You are of course **welcome to propose your own topic**. Regardless of what you choose, you and your team must research your topic and relevant techniques and come up with a detailed plan of execution. You will meet with some subset of the procedural staff before starting implementation for approval.
-
-**Final grading will be individual** and will be based on both the final product and how well you were able to achieve your intended effect according to your execution plan. Plans change of course, and we don’t expect you to follow your execution plan to a T, but if your final project looks pretty good, but you cut corners and only did 30% of what you outlined in your design doc, you will be penalized.
-
-But overall, have fun! This is your opportunity to work on whatever procedural project inspires you. The best way to ensure a good result is to pick something you’re passionate about. :)
-
-## Timeline
-
-- 4/08 Design doc due / Have met with procedural staff
-- 4/18 Milestone 1 (short write-up + demo)
-- 4/25 Milestone 2 (short write-up + demo)
-- 5/3 Final presentations (3-5 pm, Siglab), final reports due
-
-## Design Doc
-
-Your design doc should follow the following template. Note, each section can be pretty short, but cover them all! This will serve as valuable documentation when showing this project off in the future AND doing a good job will make it much easier for you to succeed, so please take this seriously.
-
-### Design Doc Template:
-
-- #### Introduction
- * What motivates this project?
-- #### Goal
- * What do you intend to achieve with this project?
-- #### Inspiration/reference:
- * Attach some materials, visual or otherwise you intend as reference
-- #### Specification:
- * Outline the main features of your project
-- #### Techniques:
- * What are the main technical/algorithmic tools you’ll be using? Give an overview, citing specific papers/articles
-- #### Design:
- * How will your program fit together? Make a simple free-body diagram illustrating the pieces.
-- #### Timeline:
- * Create a week-by-week set of milestones for each person in your group.
-
-
-Along with your final project demo, you will submit a final report, in which you will update correct your original design doc as needed and add a few post-mortem items.
-
-## Milestones
-
-To keep you honest / on-track, we will be checking on your progress at weekly intervals, according to milestones you’ll define at the outset (pending our approval). For each of the two milestones prior to the final submission, you will submit a short write up explaining whether or not you individually achieved your goals (specifying the files where the work happened), along with a link to a demo / images. These don’t have to be super polished -- we just want to see that you’re getting things done.
-
-Example:
-
-“Milestone 1:
- Adam:
-Made some procedural terrain code in src/terrain.js. Implemented 3D simplex noise to do it. Also applied coloring via custom shader based on this cool paper X (see src/shaders/dirt.glsl). IMAGE
-
-Austin:
-I managed to set up my voronoi diagram shader (see src/shaders/voronoi.glsl).
-Experimented with different scattering techniques. It’s working with the euclidean distance metric. I’m using it in src/main.js to color stones. IMAGE
-
-Rachel:
-I tried really hard to make my toon shader work (src/shaders/toon.glsl), but I still have a bug! T_T BUGGY IMAGE. DEMO LINK”
+[Video](https://clips.twitch.tv/VibrantEasyDiscMcaT)
## Final Report
-
-In addition to your demo, you will create a final report documenting your project overall. This document should be clear enough to explain the value and details of your project to a random computer graphics person with no knowledge of this class.
-
-### Final Report Template:
-
-- #### Updated design doc:
- * All the sections of your original design doc, corrected if necessary
-- #### Results:
- * Provide images of your finished project
-- #### Evaluation (this is a big one!):
- * How well did you do? What parameters did you tune along the way? Include some WIP shots that compare intermediate results to your final. Explain why you made the decisions you did.
-- #### Future work:
- * Given more time, what would you add/improve
-- #### Acknowledgements:
- * Cite _EVERYTHING_. Implemented a paper? Used some royalty-free music? Talked to classmates / a professor in a way that influenced your project? Attribute everything!
-
-## Logistics
-
-Like every prior project, your code will be submitted via github. Fork the empty final project repo and start your code base from there. Take this as an opportunity to practice using git properly in a team setting if you’re a new user. For each weekly submission, provide a link to your pull request. Your repo will contain all the code and documentation associated with your project. The readme for your repo will eventually be your final report. At the top level, include a folder called “documentation”, where you’ll put your design doc and milestone write-ups.
-
-Don’t wait to merge your code! Seriously, there be dragons. Try to have a working version including all your code so that compatibility and merge issues don’t sneak up on you near the end.
-
-## Grading
-
-- 15% Design Doc (graded as a group)
-- 15% Milestone 1 (graded as a group)
-- 15% Milestone 2 (graded as a group)
-- 55% Final demo + report (graded individually)
-
-NOTE: We’ve been pretty lax about our late policy throughout the semester, but our margins on the final project are tight, therefore late submissions will NOT be accepted. If you have a significant reason for being unable to complete your goals, talk to us, and we’ll discuss getting you an incomplete and figure out an adjusted work plan with your group.
-
-
+### David
+- Implemented logic for populating an area with buildings, roads, doodads, etc. based on a biome
+- Implemented logic for populating a vertical route with bushes, trees, ledges based on a biome
+- Updated the player sprite to face the correct direction when moved
+- Added in ability for an area to regenerate (based on some random probability) once a user leaves that area
+- Implemented the minimap! :D
+- Networking things
+
+### Joseph
+- Removed previous algorithm for area generation (Minimum spanning tree based algorithm), and reimplemented area generation and connection using a probabilistic DFS method with specific rules so we don't accidentally generate new areas over old areas
+- Implemented logic for definding specific area bounds and attributes (entry/exit points based on where routes intersect an area, etc.) and generating believable terrain borders in non-playable areas
+- Added in ability to change the number of areas generated (must not have anyone else in the map with you, otherwise the server gets sad)
+- Adapted David's vertical route logic to work with horizontal routes
+
+### Both
+- Updated sprite sheets
+- Many, many, many refactors
+
+## Design Document
+
+### Goal
+Our goal for the final project of CIS 700-006 is to procedurally generate an interactive 2D Pokemon map that contains a number of 'Pokemon-Go' like features. The 2D world will be a grid-based map, where each 'block' of the grid corresponds to an explorable area. The blocks will each be themed to a specific biome, and the twist of this game is that when a player leaves a certain block and returns to that same block, the block will have changed its terrain based on some rule (currently, planning to cycle through the biomes in a random order). In addition, each biome will feature the original sprites for grass, trees, dirt, snow, etc., in order to recapture the authentic feel of the beloved Pokemon universe. Finally, we plan to rip off the original 150 Pokemon sprites from a 'trusted' resource, and randomly spawn these pokemon in 'wild pokemon areas' of the blocks (randomly determined in each block), where the pokemon available for spawn are determined by the specific biome (e.g. we will not spawn a water pokemon in the desert biome.)
+
+### Milestones
+- Milestone 1 - Develop framework with traversable and interactive grid. Grid should have basic placeholders for randomly generated terrain
+- Milestone 2 - Add in textures/sprites, updated algorithm for generating terrain and different biomes
+- Milestone 3 - Polish up the project, stitching together the different blocks so we can walk between them
+
+### End Product Summary
+- Traversable terrain
+ - Player controleld entity can move and naviagate around on a map
+- Proper terrain interaction
+ - Properly defined playable and non-playable areas will be implemented (can't walk through a mountain, trees, etc.)
+- Procedurally generated map
+ - Mimic original pokemon's style of maps
+ - Use various terrain objects (grass, roads, cliffs, mountains, water bodies, etc.)
+- Real-time generation
+ - Map generation is dynamic and will allow for playthrough with changing terrain
+- Map stiching
+ - Depending on implementation, the modularity of maps will allow for larger contiguous segments to be connected
+- Parameterized map generation
+ - Allows for user to control the number of areas generated, as well as some other factors
+
+## Results
+
+### Example snow biome area
+
+
+### Biome specific terrain
+
+
+### Transition between an area and a route
+
+
+### Example sand biome route with pokemon
+
+
+### Example water biome route transitioning to grass biome area
+
+
+### Example grass biome area with a doodad
+
+
+### Example snow biome route
+
+
+### Example water biome area
+
+
+## Evaluation
+With regards to the milestones, our group successfully completed all the points promised in each one. Based off of our end product summary, our group completed all of the promised points except for a few granular details we did not have time to polish up. More specifically, our team fell short on two features we had hoped to have working. The first is proper terrain interaction. While certain non-traversable terrain objects properly deny an agent attempting to walk over it, other terrain objects allow a player to ghost right through it (we're looking at you, evergreen trees in each grass and snow route). The reason for this was not because we couldn't figure out how to make a tree non-traversable (this is in fact just a boolean flag that we set when instantiating a new Tile object, which is a base building block of our map), but in fact due to the unforeseen complications that would arise when generating the tree objects for a route. The algorithm employed for populating a route with a believable amount of trees and grass is essentially two sin curves with amplitude noise carving out a walkable path for an agent, based off of some route attributes randomly assigned by the master process before render time. However, due to the random nature of the path carving, as we call it, sometimes the walkable route would only be one way, meaning a certain patch of land that was supposed to go both ways was blocked off by a ledge. We attempted to tweak our random seed, adding in certain rules so ledges can't be a certain length, among other strategies, but ultimately nothing came out that was satisfactory enough for the team, so we decided to allow an agent to ghost through a route region more as a proof of concept that the region was correctly generated, which also freed up time for us to go focus on other issues and bugs. In addition, we also fell short on the ability to alter more than one factor for the terrain generation. In the end, we were only able to allow a user to alter the number of areas generated by our engine, but denied users the ability to alter anything else. This was also a design decision that ultimately boiled down to us realizing certain parameters worked better than others, and tweaking even just one of these parameters wrong would cause the engine to either render a map very slowly, or not render at all due to the web browser crashing. We decided to allow for users to modify the number of areas generated because it was safe enough given the fact that an acceptable random seed and other parameters were set properly. Some examples of these 'other' parameters include, but are not limited to: average distance between areas (route length), average area size, average area density, and average area padding. All these attributes can be found in the `World.js` file.
+
+In summary, our team managed to complete 95% of the work promised, witht the other 5% being smaller features that we would love to iron out in the future. Unfortunately, our team did not capture any screenshots of our WIP or intermediate stages, and the screenshots you see in the above section is ultimately our final result. However, for those who did see our milestone demonstrations, (Adam, Rachel, Sally), you guys will know that we have come a long way from the black and while tree agent walking over a grass plane!
+
+## Future Work
+- Properly implement traversable and non-traversable terrain
+- Animate the walking/running behavior
+- Implement ability to actually enter the buildings spawn in an area
+- More cohesive terrain, so it's not just buildings randomly plopped down in an area
+- Build out the rest of the game, such as a pokemon battle and trading platform, items, etc.
+
+## Acknowledgements
+We would like to thank these sources for graciously providing the sprites we used!
+- [Terrain](http://fanart.pokefans.net/ressourcen/tilesets/tileset-wesley.png)
+- [More terrain](http://files.pokefans.net/images/fanart/mapping/ressourcen_neu/tileset-pokemon_dawn.png)
+- [Pokemon](https://veekun.com/dex/downloads)
+- [Players](http://img.photobucket.com/albums/v249/VaRuAs/DPsprites.png)
diff --git a/assets/.DS_Store b/assets/.DS_Store
new file mode 100644
index 00000000..5008ddfc
Binary files /dev/null and b/assets/.DS_Store differ
diff --git a/assets/README.md b/assets/README.md
new file mode 100644
index 00000000..8724ee14
--- /dev/null
+++ b/assets/README.md
@@ -0,0 +1 @@
+Assets that are used will be housed here
\ No newline at end of file
diff --git a/assets/biomes.png b/assets/biomes.png
new file mode 100644
index 00000000..47af6eb0
Binary files /dev/null and b/assets/biomes.png differ
diff --git a/assets/biomes.psd b/assets/biomes.psd
new file mode 100644
index 00000000..7d13432e
Binary files /dev/null and b/assets/biomes.psd differ
diff --git a/assets/characters.psd b/assets/characters.psd
new file mode 100644
index 00000000..3a7e9a15
Binary files /dev/null and b/assets/characters.psd differ
diff --git a/assets/moresprites.png b/assets/moresprites.png
new file mode 100644
index 00000000..1df61fbb
Binary files /dev/null and b/assets/moresprites.png differ
diff --git a/assets/overworld.png b/assets/overworld.png
new file mode 100644
index 00000000..a16c2231
Binary files /dev/null and b/assets/overworld.png differ
diff --git a/assets/pepe.png b/assets/pepe.png
new file mode 100644
index 00000000..0c36d9aa
Binary files /dev/null and b/assets/pepe.png differ
diff --git a/assets/player.png b/assets/player.png
new file mode 100644
index 00000000..94e3c870
Binary files /dev/null and b/assets/player.png differ
diff --git a/assets/player2.png b/assets/player2.png
new file mode 100644
index 00000000..d411710b
Binary files /dev/null and b/assets/player2.png differ
diff --git a/assets/player_sprites.jpg b/assets/player_sprites.jpg
new file mode 100644
index 00000000..3fffdbe7
Binary files /dev/null and b/assets/player_sprites.jpg differ
diff --git a/assets/player_sprites.png b/assets/player_sprites.png
new file mode 100644
index 00000000..c4efb4d5
Binary files /dev/null and b/assets/player_sprites.png differ
diff --git a/assets/pokemon.png b/assets/pokemon.png
new file mode 100644
index 00000000..2137690f
Binary files /dev/null and b/assets/pokemon.png differ
diff --git a/assets/sprites.png b/assets/sprites.png
new file mode 100644
index 00000000..3c94a806
Binary files /dev/null and b/assets/sprites.png differ
diff --git a/css/index.css b/css/index.css
new file mode 100644
index 00000000..4d9ba2e5
--- /dev/null
+++ b/css/index.css
@@ -0,0 +1,16 @@
+body {
+ background-color: black;
+}
+
+.viewport-container {
+ width: 100%;
+ text-align: center;
+}
+
+.viewport {
+ display: inline;
+}
+
+.debug-canvas {
+ vertical-align: top;
+}
diff --git a/deploy.sh b/deploy.sh
new file mode 100644
index 00000000..520ac6c1
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,37 @@
+PROJ_DIR=$(pwd)
+BUILD_DIR=$PROJ_DIR/build
+DEPLOY_DIR=$PROJ_DIR/../procedural_pokemon_build
+set -o errexit
+
+printf "Deploying from $PROJ_DIR\n"
+printf "Building...\n"
+git checkout master 1>/dev/null 2>/dev/null
+git pull origin master 1>/dev/null 2>/dev/null
+npm run build > /dev/null
+
+printf "Creating deploy environment...\n"
+if [ -d $DEPLOY_DIR ]; then
+ rm -rf $DEPLOY_DIR > /dev/null
+ printf "Deploy directory already exists, removing...\n"
+fi
+
+mkdir -p $DEPLOY_DIR
+cp -R $BUILD_DIR/* $DEPLOY_DIR
+cp -R $PROJ_DIR/assets $DEPLOY_DIR
+cp $PROJ_DIR/index.html $DEPLOY_DIR
+cp -R $PROJ_DIR/css $DEPLOY_DIR
+cd $DEPLOY_DIR
+
+printf "Deploying...\n"
+git init > /dev/null
+git remote add origin git@github.com:davlia/procedural-pokemon.git > /dev/null
+git checkout -b gh-pages 1>/dev/null 2>/dev/null
+git add -A > /dev/null
+git commit -am "deploying" > /dev/null
+git push -f origin gh-pages 1>/dev/null 2>/dev/null
+
+printf "Cleaning up...\n"
+rm -rf $BUILD_DIR
+rm -rf $DEPLOY_DIR
+
+printf "Success!\n"
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..fe704c0b
--- /dev/null
+++ b/index.html
@@ -0,0 +1,18 @@
+
+
+
+ Procedural Pokemon
+
+
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..da7e956c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,31 @@
+{
+ "scripts": {
+ "start": "webpack-dev-server --hot --inline",
+ "build": "webpack",
+ "deploy": "./deploy.sh"
+ },
+ "gh-pages-deploy": {
+ "prep": [
+ "build"
+ ],
+ "noprompt": true
+ },
+ "dependencies": {
+ "dat-gui": "^0.5.0",
+ "express": "^4.15.2",
+ "gl-matrix": "^2.3.2",
+ "seedrandom": "^2.4.3",
+ "stats-js": "^1.0.0-alpha1"
+ },
+ "devDependencies": {
+ "babel-core": "^6.18.2",
+ "babel-loader": "^6.2.8",
+ "babel-preset-es2015": "^6.18.0",
+ "colors": "^1.1.2",
+ "gh-pages-deploy": "^0.4.2",
+ "simple-git": "^1.65.0",
+ "webpack": "1.14.0",
+ "webpack-dev-server": "^1.16.3",
+ "webpack-glsl-loader": "^1.0.1"
+ }
+}
diff --git a/server/Makefile b/server/Makefile
new file mode 100644
index 00000000..b81fcd39
--- /dev/null
+++ b/server/Makefile
@@ -0,0 +1,2 @@
+all:
+ go run *.go
diff --git a/server/client.go b/server/client.go
new file mode 100644
index 00000000..4e4a3d76
--- /dev/null
+++ b/server/client.go
@@ -0,0 +1,33 @@
+package main
+
+import "github.com/gorilla/websocket"
+
+// Client encapsulates all player connection contexts
+type Client struct {
+ Conn *websocket.Conn
+ ID int32
+}
+
+// NewClient creates a new client
+func NewClient(conn *websocket.Conn, id int32) *Client {
+ c := &Client{
+ Conn: conn,
+ ID: id,
+ }
+ return c
+}
+
+// ReadJSON forwards the ReadJSON call
+func (C *Client) ReadJSON(v interface{}) error {
+ return C.Conn.ReadJSON(v)
+}
+
+// WriteJSON forwards the WriteJSON call
+func (C *Client) WriteJSON(v interface{}) error {
+ return C.Conn.WriteJSON(v)
+}
+
+// Close forwards the Close call
+func (C *Client) Close() error {
+ return C.Conn.Close()
+}
diff --git a/server/controller.go b/server/controller.go
new file mode 100644
index 00000000..21868382
--- /dev/null
+++ b/server/controller.go
@@ -0,0 +1,115 @@
+package main
+
+import (
+ "log"
+ "math/rand"
+
+ "github.com/gorilla/websocket"
+)
+
+// Controller handles all ws connection events and is the driver module
+type Controller struct {
+ ReceiveChan chan InboundMessage
+ SendChan chan OutboundMessage
+ Clients map[int32]*Client
+ World World
+}
+
+// NewController creates a new controller
+func NewController() Controller {
+ w := World{
+ Size: 512,
+ Seed: 0,
+ Agents: map[int32]Agent{},
+ }
+ c := Controller{
+ ReceiveChan: make(chan InboundMessage, 128),
+ SendChan: make(chan OutboundMessage, 128),
+ Clients: make(map[int32]*Client),
+ World: w,
+ }
+ return c
+}
+
+func (C *Controller) run() {
+ go C.handleInboundMessages()
+ go C.handleOutboundMessages()
+}
+
+func (C *Controller) handleInboundMessages() {
+ for {
+ msg := <-C.ReceiveChan
+ switch msg.Type {
+ case "init":
+ C.handleInit(msg)
+ case "update":
+ C.handleUpdate(msg)
+ default:
+ log.Printf("message unhandled: %+v\n", msg)
+ }
+ }
+}
+
+func (C *Controller) sendMessage(t string, data Data, id int32) {
+ send := OutboundMessage{
+ Type: t,
+ Data: data,
+ Receiver: id,
+ }
+ C.SendChan <- send
+}
+
+func (C *Controller) broadcastMessage(t string, data Data, sender int32) {
+ for id := range C.Clients {
+ if id == sender {
+ continue
+ }
+ C.sendMessage(t, data, id)
+ }
+}
+
+func (C *Controller) handleOutboundMessages() {
+ for {
+ msg := <-C.SendChan
+ client, ok := C.Clients[msg.Receiver]
+ if !ok {
+ log.Printf("error: could not find connection by id\n")
+ continue
+ }
+ client.WriteJSON(msg)
+ }
+}
+
+// AddConn adds connections to be tracked and listened to
+func (C *Controller) AddConn(conn *websocket.Conn) {
+ id := C.nextID()
+ client := NewClient(conn, id)
+ C.Clients[id] = client
+ go C.readFromClient(client)
+}
+
+func (C *Controller) readFromClient(client *Client) {
+ for {
+ var msg InboundMessage
+ err := client.ReadJSON(&msg)
+ if err != nil {
+ log.Printf("Connection closed by %d\n", client.ID)
+ C.handleDisconnect(client.ID)
+ return
+ }
+ // TODO: this is sort of hacky IMO? should refactor in future
+ // Fill in the ID as part of the incoming message
+ msg.Sender = client.ID
+ C.ReceiveChan <- msg
+ }
+}
+
+func (C *Controller) nextID() int32 {
+ // TODO: do something else here that doesn't put your 4 years of higher education to fucking shame
+ for {
+ id := rand.Int31()
+ if _, ok := C.Clients[id]; !ok {
+ return id
+ }
+ }
+}
diff --git a/server/gameinstance.go b/server/gameinstance.go
new file mode 100644
index 00000000..06ab7d0f
--- /dev/null
+++ b/server/gameinstance.go
@@ -0,0 +1 @@
+package main
diff --git a/server/handler.go b/server/handler.go
new file mode 100644
index 00000000..927a202c
--- /dev/null
+++ b/server/handler.go
@@ -0,0 +1,61 @@
+package main
+
+func (C *Controller) handleInit(msg InboundMessage) {
+ // Initialize newly connected user
+ p := Agent{
+ Type: "player",
+ Pos: Point{X: 256, Y: 256}, // Default spawn location is 10, 10 for now. We can change this later -- David
+ ID: msg.Sender,
+ SpriteID: "F",
+ Dir: "down",
+ }
+ C.World.Agents[msg.Sender] = p
+ init := Data{
+ Message: "alrighty, here you go",
+ Init: Init{C.World},
+ }
+
+ C.sendMessage("init", init, msg.Sender)
+
+ // Update all other players
+ u := Update{
+ Add: []Agent{p},
+ }
+ update := Data{
+ Message: "let's keep everyone on the same page",
+ Update: u,
+ }
+ C.broadcastMessage("add", update, msg.Sender)
+}
+
+func (C *Controller) handleUpdate(msg InboundMessage) {
+ // TODO: Apply state validation, more granular changes, copy by reference (maybe?)
+ // Apply update to server state
+ for _, p := range msg.Data.Update.Delta {
+ C.World.Agents[p.ID] = p
+ }
+
+ // Forward update to all players
+ d := Data{
+ Message: "catch up plz",
+ Update: msg.Data.Update,
+ }
+ C.broadcastMessage("update", d, msg.Sender)
+}
+
+func (C *Controller) handleDisconnect(id int32) {
+ // Remove the player from our server state and client from connections
+ removedPlayer := C.World.Agents[id]
+ delete(C.World.Agents, id)
+ delete(C.Clients, id)
+
+ // Update all other players
+ u := Update{
+ Delete: []Agent{removedPlayer},
+ }
+ d := Data{
+ Message: "catch up plz",
+ Update: u,
+ }
+ C.broadcastMessage("delete", d, -1)
+}
diff --git a/server/main.go b/server/main.go
new file mode 100644
index 00000000..3197c596
--- /dev/null
+++ b/server/main.go
@@ -0,0 +1,70 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "time"
+
+ "github.com/gorilla/mux"
+ "github.com/gorilla/websocket"
+)
+
+var (
+ addr = ":8000"
+ upgrader = websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+ CheckOrigin: func(r *http.Request) bool {
+ return true
+ },
+ }
+ certFile = "./ssl/davidliao_me.crt"
+ keyFile = "./ssl/davidliao.me.key"
+ c = NewController()
+)
+
+func main() {
+ env := os.Getenv("environment")
+
+ go c.run()
+ r := mux.NewRouter()
+
+ r.HandleFunc("/health", health)
+ r.HandleFunc("/", health)
+ r.HandleFunc("/play", handleConnection)
+
+ s := http.Server{
+ Handler: r,
+ Addr: addr,
+ WriteTimeout: 15 * time.Second,
+ ReadTimeout: 15 * time.Second,
+ }
+ log.Printf("Listening and serving on %s\n", addr)
+ if env == "production" {
+ log.Fatal(s.ListenAndServeTLS(certFile, keyFile))
+ } else {
+ log.Fatal(s.ListenAndServe())
+ }
+}
+
+// health reports 200 if services is up and running
+func health(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, "it's alive :O")
+}
+
+// handleConnection handles websocket requests from client
+func handleConnection(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "GET" {
+ http.Error(w, "Method not allowed", 405)
+ return
+ }
+
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ log.Println(err)
+ return
+ }
+ c.AddConn(conn)
+}
diff --git a/server/models.go b/server/models.go
new file mode 100644
index 00000000..f0b4422c
--- /dev/null
+++ b/server/models.go
@@ -0,0 +1,56 @@
+package main
+
+// InboundMessage is used to unmarshal incoming events
+type InboundMessage struct {
+ Type string `json:"type"`
+ Data Data `json:"data"`
+ Sender int32 `json:"id"`
+}
+
+// OutboundMessage is used to marshal outgoing events
+type OutboundMessage struct {
+ Type string `json:"type"`
+ Data Data `json:"data"`
+ Receiver int32 `json:"id"`
+}
+
+// Data stores the primary payload of each message
+type Data struct {
+ Message string `json:"message"`
+ Init Init `json:"init"`
+ Update Update `json:"update"`
+}
+
+// Init encapsulates data to initialize game state for new connections
+type Init struct {
+ World World `json:"world"`
+}
+
+// World encapsulates data for the world state
+type World struct {
+ Agents map[int32]Agent `json:"agents"`
+ Size int32 `json:"size"`
+ Seed int32 `json:"seed"`
+}
+
+// Agent encapsulates data for users
+type Agent struct {
+ Type string `json:"type"`
+ Pos Point `json:"pos"`
+ ID int32 `json:"id"`
+ SpriteID string `json:"spriteID"`
+ Dir string `json:"dir"`
+}
+
+// Point is a cartesian tuple
+type Point struct {
+ X int32 `json:"x"`
+ Y int32 `json:"y"`
+}
+
+// Update encapsulates all state differences between t_i and t_{i+1}
+type Update struct {
+ Add []Agent `json:"add"`
+ Delta []Agent `json:"delta"`
+ Delete []Agent `json:"delete"`
+}
diff --git a/server/server b/server/server
new file mode 100644
index 00000000..627fa812
Binary files /dev/null and b/server/server differ
diff --git a/src/Agent.js b/src/Agent.js
new file mode 100644
index 00000000..9cfc5333
--- /dev/null
+++ b/src/Agent.js
@@ -0,0 +1,60 @@
+export default class Agent {
+ constructor(agent) {
+ this.update(agent);
+ }
+
+ move(dir, world) {
+ this.dir = dir;
+ switch(dir) {
+ case 'right':
+ if (this.pos.x + 1 < world.size
+ && world.getTile(this.pos.x+1, this.pos.y).traversable) {
+ this.pos.x += 1;
+ }
+ break;
+ case 'left':
+ if (this.pos.x - 1 >= 0
+ && world.getTile(this.pos.x-1, this.pos.y).traversable) {
+ this.pos.x -= 1;
+ }
+ break;
+ case 'up':
+ if (this.pos.y - 1 >= 0
+ && world.getTile(this.pos.x, this.pos.y-1).traversable) {
+ this.pos.y -= 1;
+ }
+ break;
+ case 'down':
+ if (this.pos.y + 1 < world.size
+ && world.getTile(this.pos.x, this.pos.y+1).traversable) {
+ this.pos.y += 1;
+ }
+ break;
+ }
+ }
+
+
+ moveTo(pos) {
+ this.pos.x = pos.x;
+ this.pos.y = pos.y;
+ }
+
+ update(agent) {
+ this.type = agent.type;
+ this.pos = agent.pos;
+ this.id = agent.id;
+ this.spriteID = agent.spriteID;
+ this.dir = agent.dir;
+ }
+
+ // Deprecated and replaced by `update` which handles deserializing -- David
+ serialize() {
+ return {
+ pos: {
+ x: this.pos.x,
+ y: this.pos.y
+ },
+ id: this.id,
+ };
+ }
+}
diff --git a/src/App.js b/src/App.js
new file mode 100644
index 00000000..b914958f
--- /dev/null
+++ b/src/App.js
@@ -0,0 +1,176 @@
+import World from './World.js'
+import RenderEngine from './RenderEngine.js'
+import Sprite from './Sprite.js'
+
+window.DEBUG_MODE = 1;
+const ASSETS = './assets';
+const SERVER_URL = 'wss://davidliao.me:8000/play';
+const LOCAL_SERVER_URL = 'ws://localhost:8000/play';
+const RESOLUTION_SCALE = 3;
+export default class App {
+ constructor () {
+ this.canvas = document.createElement('canvas'); // Aspect ratio of 3:2
+ this.canvas.width = 240 * RESOLUTION_SCALE;
+ this.canvas.height = 160 * RESOLUTION_SCALE;
+ this.canvas.className = 'viewport'
+ this.terrainSpriteSrc = `${ASSETS}/biomes.png`;
+ this.pokemonSpriteSrc = `${ASSETS}/pokemon.png`;
+ this.playerSpriteSrc = `${ASSETS}/player.png`;
+ this.clientID = -1; // default null value for client ID
+
+ if (window.DEBUG_MODE === 1) {
+ window.debugCanvas = document.createElement('canvas');
+ window.debugCanvas.width = 256;
+ window.debugCanvas.height = 256;
+ window.debugCanvas.className = 'debug-canvas'
+ window.debugCanvas.style.visibility = 'hidden';
+ }
+ }
+
+ resolveParams() {
+ let url = window.location.href;
+ if (!url.includes("?")) {
+ this.seed = 0;
+ this.num_areas = 6;
+ }
+ else {
+ let params = url.split('?');
+ let tokens = params[1].split('&');
+ let seed = tokens[0].split('=');
+ let num_areas = tokens[1].split('=');
+ this.seed = seed[1];
+ this.num_areas = num_areas[1];
+ }
+ }
+
+ setup() {
+ this.setupWebsocket();
+ this.setupGame();
+ this.setupEventListeners();
+ }
+
+ setupGame() {
+ this.resolveParams();
+ this.world = new World(this.num_areas);
+ this.re = new RenderEngine(
+ this.canvas,
+ this.terrainSprite,
+ this.playerSprite,
+ this.pokemonSprite,
+ this.world);
+ }
+
+ setupEventListeners() {
+ window.addEventListener('keydown', (event) => {
+ let me = this.world.getMe();
+ switch (event.keyCode) {
+ case 32:
+ let style = window.debugCanvas.style;
+ style.visibility = style.visibility === 'visible' ? 'hidden' : 'visible';
+ console.log(me.pos, this.world.getTile(me.pos.x, me.pos.y));
+ break;
+ case 37:
+ me.move('left', this.world);
+ break;
+ case 38:
+ me.move('up', this.world);
+ break;
+ case 39:
+ me.move('right', this.world);
+ break;
+ case 40:
+ me.move('down', this.world);
+ break;
+ }
+ this.sendEvent('update', {
+ message: 'syncing shit',
+ update: {
+ delta: [me]
+ }
+ });
+ this.re.render();
+ });
+ }
+
+ onLoad() {
+ let container = document.createElement('div');
+ container.className = 'viewport-container';
+ container.appendChild(this.canvas);
+ document.body.appendChild(container);
+ if (window.DEBUG_MODE === 1) {
+ container.appendChild(window.debugCanvas);
+ document.body.appendChild(container);
+ }
+ // TODO: turn into Promise.All instead of callback chain
+ this.terrainSprite = new Sprite(this.terrainSpriteSrc, 16, 16, () => {
+ this.pokemonSprite = new Sprite(this.pokemonSpriteSrc, 64, 64, () => {
+ this.playerSprite = new Sprite(this.playerSpriteSrc, 25, 30, () => {
+ this.setup();
+ });
+ });
+ });
+ }
+
+ onResize() {
+
+ }
+
+ itsAlive() {
+ this.interval = setInterval(() => {
+ let rerender = this.world.morph();
+ if (rerender) {
+ this.re.render();
+ if (window.DEBUG_MODE === 1) {
+ this.re.debugRendered = false;
+ }
+ }
+ }, 20000);
+ }
+
+/**********************
+ WebSocket Shenanigans
+ **********************/
+
+ setupWebsocket() {
+ this.ws = new WebSocket(SERVER_URL);
+ this.ws.onopen = this.onWSOpen.bind(this);
+ this.ws.onmessage = this.receiveEvent.bind(this);
+ }
+
+ onWSOpen() {
+ this.sendEvent('init', {message: 'initializing connection and awaiting id assignment'});
+ }
+
+ sendEvent(type, data) {
+ let m = {
+ type: type,
+ data: data,
+ id: this.clientID
+ }
+ this.ws.send(JSON.stringify(m));
+ }
+
+ receiveEvent(e) {
+ let { type, data, id } = JSON.parse(e.data);
+ switch (type) {
+ case 'init':
+ this.clientID = id;
+ this.world.initWorld(data.init.world, id);
+ this.itsAlive();
+ break;
+ case 'add':
+ this.world.addAgents(data.update.add);
+ break;
+ case 'update':
+ this.world.updateAgents(data.update.delta);
+ break;
+ case 'delete':
+ this.world.deleteAgents(data.update.delete);
+ break;
+ default:
+ console.log('event not handled', e.data);
+ return;
+ }
+ this.re.render();
+ }
+}
diff --git a/src/Area.js b/src/Area.js
new file mode 100644
index 00000000..882dc6c9
--- /dev/null
+++ b/src/Area.js
@@ -0,0 +1,425 @@
+import Tile from './Tile.js'
+import Structure from './Structure.js'
+import { util } from './Util.js'
+
+
+export default class Area {
+ constructor(x, y, sx, sy, numHouses, pokemart, pokecenter) {
+ this.x = x;
+ this.y = y;
+ this.sx = sx; // TODO: figure out if rx is radius or diameter
+ this.sy = sy;
+ this.rx = Math.floor(sx / 2);
+ this.ry = Math.floor(sy / 2);
+ this.numHouses = numHouses;
+ this.pokemart = pokemart;
+ this.pokecenter = pokecenter;
+ this.structures = [];
+ this.neighbors = {north: false, south: false, east: false, west: false};
+ this.outlets = [];
+
+ // lol
+ this.waitThatWasntThereBeforeWTF = 0.1;
+ this.biome = this.getRandomBiome();
+ }
+
+ init(grid) {
+ this.biome = this.getRandomBiome();
+ this.fill(grid);
+ this.resolveSprites(grid);
+ if (['grass', 'water', 'snow'].includes(this.biome)) {
+ this.genEntryRoads(grid);
+ }
+ this.genPokecenter(grid);
+ this.genPokemart(grid);
+ this.genHouses(grid);
+ if (['grass', 'water', 'snow'].includes(this.biome)) {
+ this.repairRoads(grid);
+ }
+ if (['grass', 'snow'].includes(this.biome)) {
+ this.genTrees(grid);
+ this.repairTrees(grid);
+ }
+ if (this.biome === 'sand') {
+ this.genCacti(grid);
+ this.repairTrees(grid);
+ }
+ this.genDoodads(grid);
+ // this.genPonds(grid); // TODO: improve algorithm
+ }
+
+ fill(grid) {
+ for (let i = this.x - this.rx; i <= this.x + this.rx; i++) {
+ for (let j = this.y - this.ry; j <= this.y + this.ry; j++) {
+ grid[i][j].spriteID = this.biome;
+ grid[i][j].offset(0,0);
+ grid[i][j].traversable = true;
+ }
+ }
+ }
+
+ getRandomBiome() {
+ let rand = util.random();
+ if (rand < 0.25) {
+ return 'grass';
+ }
+ else if (rand < 0.50) {
+ return 'water';
+ }
+ else if (rand < 0.75) {
+ return 'sand';
+ }
+ else {
+ return 'snow';
+ }
+ }
+
+ resolveSprites(grid) {
+ // default values
+ this.treeSprite = 'T0';
+ this.roadSprite = 'R0';
+ this.pcSprite = 'PC';
+ this.bpcSprite = 'BPC';
+ this.pmSprite = 'PM';
+ this.pondSprite = 'W0';
+ this.doodads = ['D0', 'D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7'];
+
+ switch (this.biome) {
+ case 'water':
+ this.treeSprite = 'T1';
+ this.roadSprite = 'R1';
+ this.doodads = ['DW0', 'DW1', 'DW2', 'DW3']
+ break;
+ case 'snow':
+ this.treeSprite = 'T2';
+ this.roadSprite = 'R2';
+ this.pcSprite = 'PC1';
+ this.bpcSprite = 'BPC1';
+ this.pmSprite = 'PM1';
+ this.doodads = ['DS0', 'DS1', 'DS2', 'DS3'];
+ break;
+ case 'sand':
+ this.treeSprite = 'T3';
+ // this.roadSprite = 'sand';
+ this.doodads = ['DD0', 'DD1'];
+ }
+ }
+
+ genRoadx(grid, startx, starty, length, lw, rw) {
+ util.iterate(startx, startx + length, i => {
+ for (let w = -lw; w <= rw; w++) {
+ if (grid[i][starty + w].spriteID === this.biome) {
+ grid[i][starty + w] = new Tile(this.roadSprite, true, 1, 1);
+ }
+ }
+ });
+ }
+
+ genRoady(grid, startx, starty, length, lw, rw) {
+ util.iterate(starty, starty + length, i => {
+ for (let w = -lw; w <= rw; w++) {
+ if (grid[startx + w][i].spriteID === this.biome) {
+ grid[startx + w][i] = new Tile(this.roadSprite, true, 1, 1);
+ }
+ }
+ });
+ }
+
+ genEntryRoads(grid) {
+ this.outlets.forEach(outlet => {
+ let { x, y } = outlet;
+ let sdx = Math.sign(x - this.x);
+ this.genRoadx(grid, this.x + 2 * sdx, this.y, x - this.x, 2, 2);
+ this.genRoady(grid, x, this.y, y - this.y, 2, 2);
+ });
+ }
+
+ valid(grid, px, py) {
+ let r = 5;
+ for (let i = 0; i < this.structures.length; i++) {
+ let s = this.structures[i];
+ if (Math.abs(px - s.px) + Math.abs(py - s.py) < 1.5 * r) {
+ return false;
+ }
+ }
+ for (let i = px - r; i <= px + r; i++) {
+ for (let j = py - r; j <= py + r; j++) {
+ if (0 <= i && i < grid.length && 0 <= j && j < grid[0].length && grid[i][j].spriteID === this.roadSprite) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ validStructureLocation(grid) {
+ let px, py, locx, locy;
+ do {
+ let rd = util.randomDisk(this.rx / 1.3, this.ry / 1.3);
+ px = Math.floor(rd.px);
+ py = Math.floor(rd.py);
+ locx = this.x + px;
+ locy = this.y + py;
+ } while(!this.valid(grid, locx, locy))
+ return {locx, locy};
+ }
+
+ connectRoads(grid, locx, locy, structure) {
+ if (!['grass', 'water', 'snow'].includes(this.biome)) {
+ return;
+ }
+ let nearest = this.findNearest(this.roadSprite, locx, locy + structure.sy, grid);
+ this.genRoady(grid, locx, locy, structure.sy + 2, 1, 1); // protrude down a bit
+ locy += structure.sy;
+ this.genRoadx(grid, locx, locy, nearest.x - locx + 3 * Math.sign(nearest.x - locx), 1, 1);
+ locx = nearest.x + 1;
+ this.genRoady(grid, locx, locy, nearest.y - locy + 2 * Math.sign(nearest.y - locy), 1, 1);
+ }
+
+ genPokecenter(grid) {
+ if (!this.pokecenter) {
+ return;
+ }
+
+ let { locx, locy } = this.validStructureLocation(grid);
+ let sizeRand = util.random();
+ let structure = util.randChoice([
+ {
+ w: 3,
+ o: () => {return new Structure(this.pcSprite, locx, locy, 5, 5);}
+ },
+ {
+ w: 1,
+ o: () => {return new Structure(this.bpcSprite, locx, locy, 7, 5);}
+ }
+ ])();
+ this.connectRoads(grid, locx, locy, structure);
+ structure.init(grid);
+ this.structures.push(structure);
+ }
+
+ genPokemart(grid) {
+ if (!this.pokemart) {
+ return;
+ }
+ let { locx, locy } = this.validStructureLocation(grid);
+ let sizeRand = util.random();
+ let structure = new Structure(this.pmSprite, locx, locy, 4, 4);
+ this.connectRoads(grid, locx, locy, structure);
+ structure.init(grid);
+ this.structures.push(structure);
+ }
+
+ genHouses(grid) {
+ for (let i = 0; i < this.numHouses; i++) {
+ this.genHouse(grid);
+ }
+ }
+
+ genHouse(grid) {
+ let { locx, locy } = this.validStructureLocation(grid);
+ let sizeRand = util.random();
+ let structure = util.randChoice([
+ {
+ w: 1,
+ o: () => {return new Structure('H0', locx, locy, 4, 4);}
+ },
+ {
+ w: 1,
+ o: () => {return new Structure('H1', locx, locy, 4, 4);}
+ },
+ {
+ w: 1,
+ o: () => {return new Structure('H2', locx, locy, 4, 4);}
+ },
+ {
+ w: 1,
+ o: () => {return new Structure('H3', locx, locy, 4, 4);}
+ },
+ {
+ w: 1,
+ o: () => {return new Structure('H4', locx, locy, 4, 4);}
+ },
+ {
+ w: 1,
+ o: () => {return new Structure('H5', locx, locy, 5, 5);}
+ },
+ {
+ w: 1,
+ o: () => {return new Structure('H6', locx, locy, 7, 5);}
+ },
+ {
+ w: 1,
+ o: () => {return new Structure('H7', locx, locy, 5, 5);}
+ },
+ ])();
+ this.connectRoads(grid, locx, locy, structure);
+ structure.init(grid);
+ this.structures.push(structure);
+ }
+
+ genTrees(grid) {
+ let r = 5;
+ for (let i = this.x - this.rx; i <= this.x + this.rx; i++) {
+ for (let j = this.y - this.ry; j <= this.y + this.ry; j++) {
+ let valid = true;
+ for (let x = i - r; x <= i + r; x++) {
+ for (let y = j - r; y <= j + r; y++) {
+ if ([this.roadSprite, 'H0', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'H7', this.pcSprite, this.bpcSprite, this.pmSprite].includes(grid[x][y].spriteID)) {
+ valid = false;
+ }
+ }
+ }
+ if (valid && i % 2 === 0 && j % 3 === 0) {
+ grid[i][j].spriteID = this.treeSprite;
+ grid[i][j].traversable = false;
+ }
+ }
+ }
+ }
+
+ genCacti(grid) {
+ let r = 5;
+ for (let i = this.x - this.rx; i <= this.x + this.rx; i++) {
+ for (let j = this.y - this.ry; j <= this.y + this.ry; j++) {
+ let prob = 0.05;
+ let valid = true;
+ for (let x = i - r; x <= i + r; x++) {
+ for (let y = j - r; y <= j + r; y++) {
+ if (['0', 'H0', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'H7', this.pcSprite, this.bpcSprite, this.pmSprite].includes(grid[x][y].spriteID)) {
+ valid = false;
+ }
+ }
+ }
+ if (valid && i % 2 === 0 && j % 3 === 0 && util.random() < prob) {
+ grid[i][j].spriteID = this.treeSprite;
+ grid[i][j].traversable = false;
+ }
+ }
+ }
+ }
+
+ repairTrees(grid) {
+ let makeTree = (i, j, dx, dy) => {
+ let t = grid[i + dx][j + dy];
+ t.spriteID = this.treeSprite;
+ t.offset(dx, dy);
+ t.traversable = false;
+ }
+ for (let i = this.x - this.rx; i <= this.x + this.rx; i++) {
+ for (let j = this.y - this.ry; j <= this.y + this.ry; j++) {
+ let tile = grid[i][j];
+ if (tile.spriteID === this.treeSprite && tile.offx === 0 && tile.offy === 0) {
+ makeTree(i, j, 0, 1);
+ makeTree(i, j, 0, 2);
+ makeTree(i, j, 1, 0);
+ makeTree(i, j, 1, 1);
+ makeTree(i, j, 1, 2);
+ }
+ }
+ }
+ }
+
+ genPonds(grid) {
+ let r = 5;
+ let px = Math.floor(this.x + util.random() * this.sx - this.rx);
+ let py = Math.floor(this.y + util.random() * this.sy - this.ry);
+ let rx = 3;
+ let ry = 3;
+ let valid = true;
+ for (let i = px - rx - r; i <= px + rx + r; i++) {
+ for (let j = py - ry - r; j <= py + ry + r; j++) {
+ if ([this.roadSprite, 'H0', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'H7', this.pcSprite, this.bpcSprite, this.pmSprite].includes(grid[i][j].spriteID)) {
+ valid = false;
+ }
+ }
+ }
+ if (valid) {
+ for (let i = px - rx; i <= px + rx; i++) {
+ for (let j = py - ry; j <= py + ry; j++) {
+ grid[i][j].spriteID = this.pondSprite;
+ grid[i][j].traversable = false;
+ }
+ }
+ }
+ }
+
+ repairPonds(grid) {
+
+ }
+
+ findNearest(spriteID, x, y, grid) {
+ let cx = Infinity;
+ let cy = Infinity;
+ for (let i = this.x - this.rx; i <= this.x + this.rx; i++) {
+ for (let j = this.y - this.ry; j <= this.y + this.ry; j++) {
+ if (grid[i][j].spriteID === spriteID && (Math.abs(i - x) + Math.abs(j - y)) < (Math.abs(cx - x) + Math.abs(cy - y))) {
+ cx = i;
+ cy = j;
+ }
+ }
+ }
+ return {x: cx, y: cy};
+ }
+
+ repairRoads(grid) {
+ for (let i = this.x - this.rx; i <= this.x + this.rx; i++) {
+ for (let j = this.y - this.ry; j <= this.y + this.ry; j++) {
+ let tile = grid[i][j];
+ if (tile.spriteID === this.roadSprite) {
+ tile.offset(1, 1);
+ if (grid[i + 1][j].spriteID !== this.roadSprite) {
+ tile.offx += 1;
+ } else if (grid[i - 1][j].spriteID !== this.roadSprite) {
+ tile.offx -= 1;
+ }
+ if (grid[i][j - 1].spriteID !== this.roadSprite) {
+ tile.offy -= 1;
+ } else if (grid[i][j + 1].spriteID !== this.roadSprite) {
+ tile.offy += 1;
+ }
+ if (grid[i + 1][j + 1].spriteID === this.biome && tile.offx === 1 && tile.offy === 1) {
+ tile.offset(2, -1);
+ } else if (grid[i + 1][j - 1].spriteID === this.biome && tile.offx === 1 && tile.offy === 1) {
+ tile.offset(2, -2);
+ } else if (grid[i - 1][j + 1].spriteID === this.biome && tile.offx === 1 && tile.offy === 1) {
+ tile.offset(1, -1);
+ } else if (grid[i - 1][j - 1].spriteID === this.biome && tile.offx === 1 && tile.offy === 1) {
+ tile.offset(1, -2);
+ }
+ }
+ }
+ }
+ }
+
+ genDoodads(grid) {
+ let r = 1;
+ for (let i = this.x - this.rx; i <= this.x + this.rx; i++) {
+ for (let j = this.y - this.ry; j <= this.y + this.ry; j++) {
+ let tile = grid[i][j];
+ if (tile.spriteID === this.biome) {
+ let rand = util.random();
+ let spawnProb = 0.01;
+ let neighbors = 0;
+ let valid = true;
+ for (let x = i - r; x <= i + r; x++) {
+ for (let y = j - r; y <= j + r; y++) {
+ if (grid[x][y].spriteID === this.roadSprite) {
+ valid = false;
+ }
+ if (grid[x][y].spriteID[0] === 'D') {
+ neighbors++;
+ }
+ }
+ }
+ spawnProb += Math.sqrt(neighbors) * 0.15;
+
+ if (valid && rand < spawnProb) {
+ let doodad = util.choose(this.doodads);
+ tile.spriteID = doodad;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/RenderEngine.js b/src/RenderEngine.js
new file mode 100644
index 00000000..401fb6f0
--- /dev/null
+++ b/src/RenderEngine.js
@@ -0,0 +1,126 @@
+import Tile from './Tile.js'
+import Sprite, { TERRAIN_TILEMAP, POKE_TILEMAP, CHARACTER_TILEMAP } from './Sprite.js'
+
+export default class RenderEngine {
+ constructor(canvas, ts, pls, pks, world) {
+ // canvas is 960 x 640
+ this.canvas = canvas;
+ this.ctx = canvas.getContext('2d');
+ this.world = world;
+ this.terrainSprite = ts;
+ this.playerSprite = pls;
+ this.pokemonSprite = pks;
+ // Viewport
+ this.vpWidth = 15;
+ this.vpHeight = 11;
+ this.halfWidth = Math.floor(this.vpWidth / 2);
+ this.halfHeight = Math.floor(this.vpHeight / 2);
+ }
+
+ render() {
+ this._renderTerrain();
+ this._renderAgents();
+ if (window.DEBUG_MODE === 1 && !this.debugRendered) {
+ this._renderWorld();
+ }
+ }
+
+ _renderTerrain() {
+ let { pos } = this.world.getMe();
+ for (let i = 0; i < this.vpWidth; i++) {
+ for (let j = 0; j < this.vpHeight; j++) {
+ let x = i + pos.x - this.halfWidth;
+ let y = j + pos.y - this.halfHeight;
+ let tile = this.world.getTile(x, y);
+ this.drawTile(tile, 'terrain', i, j);
+ if (tile.pokemon !== undefined) {
+ this.drawTile(tile, 'pokemon', i, j);
+ }
+ }
+ }
+ }
+
+ _renderAgents() {
+ let { agents } = this.world;
+ let me = this.world.getMe();
+ for (let a in agents) {
+ let agent = agents[a];
+ let tile = new Tile(agent.spriteID, true);
+ tile.spriteID = agent.spriteID;
+ switch (agent.dir) {
+ case 'right':
+ tile.offx = 1;
+ break;
+ case 'up':
+ tile.offx = 2;
+ break;
+ case 'left':
+ tile.offx = 3;
+ break;
+ default:
+ tile.offx = 0;
+ }
+ this.drawTile(tile, 'agent', agent.pos.x - me.pos.x + this.halfWidth, agent.pos.y - me.pos.y + this.halfHeight);
+ }
+ }
+
+ drawTile(tile, type, x, y) {
+ let spritePos, spriteSheet;
+ switch (type) {
+ case 'agent':
+ spritePos = CHARACTER_TILEMAP[tile.spriteID];
+ spriteSheet = this.playerSprite;
+ break;
+ case 'terrain':
+ spritePos = TERRAIN_TILEMAP[tile.spriteID];
+ spriteSheet = this.terrainSprite;
+ break;
+ case 'pokemon':
+ spritePos = POKE_TILEMAP[tile.pokemon];
+ spriteSheet = this.pokemonSprite;
+ break;
+ }
+ let { tileHeight, tileWidth } = spriteSheet;
+ let canvasTileWidth = this.canvas.width / this.vpWidth;
+ let canvasTileHeight = this.canvas.height / this.vpHeight;
+ let canvasPosx = x * canvasTileWidth;
+ let canvasPosy = y * canvasTileHeight;
+ let sx = spritePos.x + tile.offx * tileWidth;
+ let sy = spritePos.y + tile.offy * tileHeight;
+ this.ctx.drawImage(
+ spriteSheet.image,
+ sx, sy,
+ tileWidth, tileHeight,
+ canvasPosx, canvasPosy,
+ canvasTileWidth, canvasTileHeight
+ );
+ }
+
+ _renderWorld() {
+ // TODO: this needs to get refactored or i will cry
+ let { pos } = this.world.getMe();
+ let { grid } = this.world.grid;
+ let tmpCanvas = this.canvas;
+ let tmpCtx = this.ctx;
+ this.canvas = window.debugCanvas;
+ this.ctx = window.debugCanvas.getContext('2d');
+ this.vpWidth = window.debugCanvas.width * 2;
+ this.vpHeight = window.debugCanvas.height * 2;
+ this.halfWidth = Math.floor(this.vpWidth / 2);
+ this.halfHeight = Math.floor(this.vpHeight / 2);
+ for (let i = 0; i < this.world.size; i++) {
+ for (let j = 0; j < this.world.size; j++) {
+ let tile = this.world.getTile(i, j);
+ this.drawTile(tile, 'terrain', i, j);
+ }
+ }
+ this.debugRendered = true;
+ // reset values
+ this.vpWidth = 15;
+ this.vpHeight = 11;
+ this.halfWidth = Math.floor(this.vpWidth / 2);
+ this.halfHeight = Math.floor(this.vpHeight / 2);
+ this.canvas = tmpCanvas;
+ this.ctx = tmpCtx;
+ }
+}
diff --git a/src/Route.js b/src/Route.js
new file mode 100644
index 00000000..3aca5c52
--- /dev/null
+++ b/src/Route.js
@@ -0,0 +1,326 @@
+import { util } from './Util.js'
+import Tile from './Tile.js'
+
+export default class Route {
+ constructor(a1, a2, biome, orientation) {
+ this.a1 = a1;
+ this.a2 = a2;
+ this.a1.height = 3; // TODO: mocked height;
+ this.a2.height = 0;
+ this.x = Math.floor((a1.x + a2.x) / 2);
+ this.y = Math.floor((a1.y + a2.y) / 2);
+ this.orientation = orientation;
+ if (orientation === 'v') {
+ this.sx = 16;
+ this.sy = Math.abs(a1.y - a2.y) - (a1.ry + a2.ry);
+ this.rx = Math.floor(this.sx / 2);
+ this.ry = Math.floor(this.sy / 2);
+ }
+ else {
+ this.sx = Math.abs(a1.x - a2.x) - (a1.rx + a2.rx);
+ this.sy = 16;
+ this.rx = Math.floor(this.sx / 2);
+ this.ry = Math.floor(this.sy / 2);
+ }
+ this.biome = biome; // TODO: mocked biome
+ this.sx += (this.sx % 2 === 0) ? 0 : 2 - (this.sx % 2);
+ this.sy += (this.sy % 3 === 0) ? 0 : 3 - (this.sy % 3);
+
+ this.waitThatWasntThereBeforeWTF = 0.1;
+ }
+
+ init(grid) {
+ this.resolveSprites();
+ if (this.orientation === 'v') {
+ this.genObstacles(grid);
+ this.repairObstacles(grid);
+ this.genWalkable(grid);
+ this.repairObstacles(grid);
+ this.genEncounterables(grid);
+ }
+ else {
+ this.genObstacles(grid);
+ this.repairObstacles(grid);
+ this.genHorizWalkable(grid);
+ this.repairObstacles(grid);
+ this.genEncounterables(grid);
+ this.genDoodads(grid);
+ }
+ this.assignHeights(grid);
+ this.genLedges(grid);
+ this.genPokemon(grid);
+ }
+
+ resolveSprites() {
+ switch(this.biome) {
+ case 'grass':
+ this.obstacleFill = 'OB0';
+ this.encounterable = 'EC0';
+ this.doodads = ['D0', 'D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7'];
+ this.ledge = 'LG0';
+ break;
+ case 'snow':
+ this.obstacleFill = 'OBS';
+ this.encounterable = 'ECS';
+ this.doodads = ['DS0', 'DS1', 'DS2', 'DS3'];
+ this.ledge = 'LGS';
+ break;
+ case 'sand':
+ this.obstacleFill = 'OBD';
+ this.encounterable = 'ECD';
+ this.doodads = ['DD0', 'DD1'];
+ this.ledge = 'sand';
+ break;
+ case 'water':
+ this.obstacleFill = 'water';
+ this.encounterable = 'water';
+ this.doodads = ['DW0', 'DW1', 'DW2', 'DW3'];
+ this.ledge = 'water';
+ break;
+ default:
+ this.obstacleFill = 'OB0';
+ this.encounterable = 'EC0';
+ this.doodads = ['D0', 'D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7'];
+ this.ledge = 'LG0';
+ break;
+ }
+ }
+
+ traverse(f) {
+ for (let i = this.x - this.rx + 1; i < this.x + this.rx; i++) {
+ for (let j = this.y - this.ry; j < this.y + this.ry; j++) {
+ f(i, j);
+ }
+ }
+ }
+
+ assignHeights(grid) {
+ this.traverse((i, j) => {
+ grid[i][j].height = Math.floor(util.lerp(this.y - this.ry, this.a1.height, this.y + this.ry, this.a2.height, j));
+ });
+ }
+
+ genObstacles(grid) {
+ this.traverse((i, j) => {
+ if (j % 2 === 0 && j !== 0) {
+ grid[i][j] = new Tile(this.obstacleFill, true);
+ }
+ });
+ }
+
+ repairObstacles(grid) {
+ this.traverse((i, j) => {
+ if (grid[i][j].spriteID === this.obstacleFill && grid[i][j].offy === 0) {
+ grid[i][j + 1] = new Tile(this.obstacleFill, true, 0, 1);
+ } else if (grid[i][j].spriteID === this.obstacleFill && grid[i][j].offy === 1) {
+ grid[i][j - 1] = new Tile(this.obstacleFill, true);
+ }
+ })
+ }
+
+ genWalkable(grid) {
+ let maxbx = this.x + this.rx;
+ let minbx = this.x - this.rx;
+ let maxby = this.y + this.ry;
+ let minby = this.y - this.ry;
+ let x = this.x, y;
+ // forward path
+ for (y = minby; y <= maxby; util.random() < 0.4 ? y : y++) {
+ let curve = util.random() * this.rx * Math.sin(y * 2 * Math.PI / 16);
+ x = util.clamp(minbx, Math.sign(curve) + x, maxbx - 1);
+ for (let i = -1; i <= 1; i++) {
+ for (let j = -1; j <= 1; j++) {
+ if (minbx <= x + i && x + i < maxbx &&
+ minby <= y + j && y + j <= maxby) {
+ grid[x + i][y + j] = new Tile(this.biome, true);
+ }
+ }
+ }
+ }
+ // backward path
+ for (y = minby; y <= maxby; util.random() < 0.4 ? y : y++) {
+ let curve = -util.random() * this.rx * Math.sin(y * 2 * Math.PI / 16);
+ x = util.clamp(minbx, Math.sign(curve) + x, maxbx - 1);
+ for (let i = -1; i <= 1; i++) {
+ for (let j = -1; j <= 1; j++) {
+ if (minbx <= x + i && x + i < maxbx &&
+ minby <= y + j && y + j <= maxby) {
+ grid[x + i][y + j] = new Tile(this.biome, true);
+ }
+ }
+ }
+ }
+ }
+
+ genHorizWalkable(grid) {
+ let maxbx = this.x + this.rx;
+ let minbx = this.x - this.rx;
+ let maxby = this.y + this.ry;
+ let minby = this.y - this.ry;
+ let y = this.y, x;
+
+ for (x = minbx; x <= maxbx; util.random() < 0.4 ? x : x++) {
+ let curve = util.random() * this.ry * Math.sin(x * 2 * Math.PI / 16);
+ y = util.clamp(minby, Math.sign(curve) + y, maxby - 1);
+ for (let i = -1; i <= 1; i++) {
+ for (let j = -1; j <= 1; j++) {
+ if (minbx <= x + i && x + i < maxbx &&
+ minby <= y + j && y + j <= maxby) {
+ grid[x + i][y + j] = new Tile(this.biome, true);
+ }
+ }
+ }
+ }
+
+ for (x = minbx; x <= maxbx; util.random() < 0.4 ? x : x++) {
+ let curve = -util.random() * this.ry * Math.sin(x * 2 * Math.PI / 16);
+ y = util.clamp(minby, Math.sign(curve) + y, maxby - 1);
+ for (let i = -1; i <= 1; i++) {
+ for (let j = -1; j <= 1; j++) {
+ if (minbx <= x + i && x + i < maxbx &&
+ minby <= y + j && y + j <= maxby) {
+ grid[x + i][y + j] = new Tile(this.biome, true);
+ }
+ }
+ }
+ }
+
+ }
+
+ genEncounterables(grid) {
+ let maxbx = this.x + this.rx;
+ let minbx = this.x - this.rx;
+ let maxby = this.y + this.ry;
+ let minby = this.y - this.ry;
+ for (let k = 0; k < 20; k++) {
+ let r = Math.floor(util.random() * 5) + 1;
+ let rsq = r * r;
+ let x = Math.floor(this.x + util.random() * this.sx - this.rx);
+ let y = Math.floor(this.y + util.random() * this.sy - this.ry);
+ for (let i = x - r; i <= x + r; i++) {
+ for (let j = y - r; j <= y + r; j++) {
+ let tile = grid[i][j];
+ if ((x - i) * (x - i) + (y - j) * (y - j) < rsq && tile.spriteID === this.biome &&
+ util.inBound(minbx, i, maxbx) && util.inBound(minby, j, maxby)) {
+ tile.spriteID = this.encounterable;
+ }
+ }
+ }
+ }
+ }
+
+ genHorizEncounterables(grid) {
+ let maxbx = this.x + this.rx;
+ let minbx = this.x - this.rx;
+ let maxby = this.y + this.ry;
+ let minby = this.y - this.ry;
+ for (let k = 0; k < 20; k++) {
+ let r = Math.floor(util.random() * 5) + 1;
+ let rsq = r * r;
+ let x = Math.floor(this.x + util.random() * this.sx - this.rx);
+ let y = Math.floor(this.y + util.random() * this.sy - this.ry);
+ for (let i = x - r; i <= x + r; i++) {
+ for (let j = y - r; j <= y + r; j++) {
+ let tile = grid[i][j];
+ if ((x - i) * (x - i) + (y - j) * (y - j) < rsq && tile.spriteID === this.biome &&
+ util.inBound(minbx, i, maxbx) && util.inBound(minby, j, maxby)) {
+ tile.spriteID = this.encounterable;
+ }
+ }
+ }
+ }
+ }
+
+
+ genDoodads(grid) {
+ this.traverse((i, j) => {
+ let tile = grid[i][j];
+ // spawn berries!!! omgggggg lawl
+ if (tile.spriteID === this.biome) {
+ if (util.random() < 0.01) {
+ grid[i][j] = new Tile('D8', false, util.randIntRange(1, 4), 0);
+ }
+
+ let prob = 0.01;
+ let neighbors = 0;
+ for (let x = i - 1; x <= i + 1; x++) {
+ for (let y = j - 1; y <= j + 1; y++) {
+ if (grid[x][y].spriteID[0] === 'D') {
+ neighbors++;
+ }
+ }
+ }
+ prob += Math.sqrt(neighbors) * 0.3;
+ if (util.random() < prob) {
+ grid[i][j] = new Tile(util.choose(this.doodads), true);
+ }
+ }
+
+ });
+ }
+
+ genLedges(grid) {
+ this.traverse((i, j) => {
+ let tile = grid[i][j];
+ let belowTile = grid[i][j + 1];
+ if ([this.biome, this.encounterable].includes(tile.spriteID) &&
+ [this.biome, this.encounterable].includes(belowTile.spriteID) && tile.height !== belowTile.height) {
+ let prob = 0.3;
+ let rand = util.random();
+ if (grid[i + 1][j].spriteID === this.ledge || grid[i - 1][j].spriteID === this.ledge) {
+ rand = 0;
+ }
+
+ if (rand < prob) {
+ tile.spriteID = this.ledge;
+ tile.offx = 0;
+ tile.offy = 0;
+ }
+ }
+ });
+ }
+
+ genPokemon(grid) {
+ // loop through cells of the route
+ for (let i = this.x - this.rx; i < this.x + this.rx; i++) {
+ for (let j = this.y - this.ry; j < this.y + this.ry; j++) {
+ if (grid[i][j].spriteID === 'EC0' || grid[i][j].spriteID === 'ECS' || grid[i][j].spriteID === 'ECD' || grid[i][j].biome === 'water') {
+ let rand = util.random();
+ if (grid[i][j].spriteID === 'EC0' && this.biome === 'grass') {
+ if (rand < 0.03) {
+ grid[i][j].pokemon = 'g1';
+ } else if (rand < 0.08) {
+ grid[i][j].pokemon = 'g2';
+ } else if (rand < 0.15) {
+ grid[i][j].pokemon = 'g3';
+ }
+ }
+ else if (grid[i][j].spriteID === 'ECD' && this.biome === 'sand') {
+ if (rand < 0.03) {
+ grid[i][j].pokemon = 's1';
+ } else if (rand < 0.08) {
+ grid[i][j].pokemon = 's2';
+ } else if (rand < 0.1) {
+ grid[i][j].pokemon = 's3';
+ }
+ }
+ else if (grid[i][j].biome === 'water') {
+ if (rand < 0.1) {
+ grid[i][j].pokemon = 'w1';
+ }
+ }
+ else {
+ if (rand < 0.01) {
+ grid[i][j].pokemon = 'i1';
+ }
+ else if (rand < 0.08) {
+ grid[i][j].pokemon = 'i2';
+ }
+ }
+
+ }
+ }
+ }
+
+ }
+}
diff --git a/src/Sprite.js b/src/Sprite.js
new file mode 100644
index 00000000..91ef930e
--- /dev/null
+++ b/src/Sprite.js
@@ -0,0 +1,101 @@
+export const TERRAIN_TILEMAP = {
+ '0': {x: 0, y: 0},
+ 'grass': {x: 0, y: 2 * 16}, // grass
+ 'snow': {x: 144, y: 48}, // snow
+ 'water': {x: 240, y: 1536}, // water
+ 'dwater': {x: 96, y: 1536},
+ 'DR': {x: 64, y: 224}, // dirt rock
+ 'F': {x:0, y: 9 * 16}, // flower
+ 'B': {x:16, y: 128}, // bush
+ 'F2': {x:16, y: 192}, // more flowers
+ 'sand': {x: 721, y: 48}, // sand
+ 'SB': {x: 192, y: 112}, // snow bush
+ 'WR': {x: 416, y: 128}, // water rock
+ 'PC': {x: 416, y: 384},
+ 'PC1': {x: 336, y: 384},
+ 'BPC': {x: 224, y: 384},
+ 'BPC1': {x: 112, y: 384},
+ 'PM': {x: 560, y: 400},
+ 'PM1': {x: 496, y: 400},
+ 'H0': {x: 0, y: 544},
+ 'H1': {x: 64, y: 544},
+ 'H2': {x: 64 * 2, y: 544},
+ 'H3': {x: 64 * 3, y: 544},
+ 'H4': {x: 64 * 4, y: 544},
+ 'H5': {x: 64 * 5, y: 544},
+ 'H6': {x: 400, y: 544},
+ 'H7': {x: 512, y: 544},
+ 'R0': {x: 368, y: 32},
+ 'R1': {x: 496, y: 224},
+ 'R2': {x: 80, y: 32},
+ 'R01': {x: 384, y: 0},
+ 'T0': {x: 16, y: 144},
+ 'T1': {x: 384, y: 224},
+ 'T2': {x: 304, y: 144},
+ 'T3': {x: 576, y: 144},
+ 'W0': {x: 464, y: 32},
+ 'mtn-d': {x: 432, y: 1328},
+ 'mtn-s': {x: 1056, y: 1200},
+ 'wtr-1': {x: 144, y: 1504},
+ 'wtr-2': {x: 244, y: 1504},
+ 'D0': {x: 0, y: 256},
+ 'D1': {x: 0, y: 272},
+ 'D2': {x: 0, y: 288},
+ 'D3': {x: 48, y: 208},
+ 'D4': {x: 64, y: 208},
+ 'D5': {x: 80, y: 208},
+ 'D6': {x: 96, y: 208},
+ 'D7': {x: 16, y: 192},
+ 'D8': {x: 48, y: 192},
+ 'OB0': {x: 0, y: 208},
+ 'OBS': {x: 752, y: 976},
+ 'OBD': {x: 32, y: 1568},
+ 'EC0': {x: 48, y: 32},
+ 'ECS': {x: 192, y: 80},
+ 'ECD': {x: 544, y: 96},
+ 'LG0': {x: 384, y: 96},
+ 'LGS': {x: 512, y: 1600},
+ 'DW0': {x: 416, y: 128},
+ 'DW1': {x: 464, y: 128},
+ 'DW2': {x: 480, y: 128},
+ 'DW3': {x: 496, y: 128},
+ 'DS0': {x: 160, y: 128},
+ 'DS1': {x: 176, y: 112},
+ 'DS2': {x: 192, y: 112},
+ 'DS3': {x: 192, y: 80},
+ 'DD0': {x: 544, y: 96},
+ 'DD1': {x: 560, y: 80},
+ // more to come...
+};
+
+export const POKE_TILEMAP = {
+ 'g1': {x: 0, y: 0},
+ 'g2': {x: 64, y: 384},
+ 'g3': {x: 384, y: 448},
+ 's1': {x: 192, y: 0},
+ 's2': {x: 192, y: 256},
+ 's3': {x: 192, y: 768},
+ 'w1': {x: 192, y: 320},
+ 'i1': {x: 18 * 64, y: 5 * 64},
+ 'i2': {x: 704, y: 896}
+};
+
+export const CHARACTER_TILEMAP = {
+ 'F': {x: 0, y: 0},
+};
+
+export default class Sprite {
+ constructor(src, w, h, onload) {
+ this.image = new Image();
+ this.image.src = src;
+ this.image.onload = onload
+ this.tileWidth = w;
+ this.tileHeight = h;
+ this.width = this.image.clientWidth / this.tileWidth;
+ this.height = this.image.clientHeight / this.tileHeight;
+ }
+
+ getTile(x, y) {
+ return {x: x * this.width, y: y * this.height};
+ }
+}
diff --git a/src/Structure.js b/src/Structure.js
new file mode 100644
index 00000000..e693f111
--- /dev/null
+++ b/src/Structure.js
@@ -0,0 +1,23 @@
+import Tile from './Tile.js'
+
+export default class Structure {
+ constructor(spriteID, px, py, sx, sy) {
+ this.spriteID = spriteID;
+ this.px = px;
+ this.py = py;
+ this.sx = sx;
+ this.sy = sy;
+ this.offx = Math.floor(this.sx / 2);
+ this.offy = Math.floor(this.sy / 2);
+ }
+
+ init(grid) {
+ for (let i = 0; i < this.sx; i++) {
+ for (let j = 0; j < this.sy; j++) {
+ let x = Math.floor(this.px + i - this.offx);
+ let y = Math.floor(this.py + j - this.offy);
+ grid[x][y] = new Tile(this.spriteID, false, i, j);
+ }
+ }
+ }
+}
diff --git a/src/Tile.js b/src/Tile.js
new file mode 100644
index 00000000..a0aef74e
--- /dev/null
+++ b/src/Tile.js
@@ -0,0 +1,14 @@
+export default class Tile {
+ constructor(spriteID, t, offx, offy) {
+ this.spriteID = spriteID;
+ this.pokemon = undefined;
+ this.traversable = t;
+ this.offx = offx || 0;
+ this.offy = offy || 0;
+ }
+
+ offset(x, y) {
+ this.offx = x;
+ this.offy = y;
+ }
+}
diff --git a/src/Util.js b/src/Util.js
new file mode 100644
index 00000000..54d3d55c
--- /dev/null
+++ b/src/Util.js
@@ -0,0 +1,98 @@
+import seedrandom from 'seedrandom'
+
+export default class Util {
+ constructor() {
+ this.randSeed = 0;
+ window.util = this;
+ }
+
+ seed(seed) {
+ this.randSeed = seed + 7;
+ this.rng = new Math.seedrandom(this.randSeed);
+ }
+
+ randInt() {
+ return this.random() * 2147483647;
+ }
+
+ randRange(a, b) {
+ return this.random() * (b - a) + a;
+ }
+
+ randIntRange(a, b) {
+ return Math.floor(this.randRange(a, b));
+ }
+
+ random() {
+ return this.rng();
+ }
+
+ randomDisk(rx, ry) {
+ let sqrtrx = Math.sqrt(this.random());
+ let sqrtry = Math.sqrt(this.random());
+ let theta = this.random() * 2 * Math.PI;
+ let px = sqrtrx * Math.cos(theta) * rx;
+ let py = sqrtry * Math.sin(theta) * ry;
+ return {px, py};
+ }
+
+ /*
+ Randomly selects an object from a list given weights. Weights do not have to sum to 1.
+ Follows this parameter format:
+ [
+ {w: 3, o: choice1},
+ {w: 10, o: choice2},
+ ...
+ ]
+ Return: `o`
+ */
+ randChoice(l) {
+ let sumWeights = 0;
+ l.forEach(item => {
+ item.w += sumWeights;
+ sumWeights = item.w;
+ });
+ let rand = this.random() * sumWeights;
+ for (let i = 0; i < l.length; i++) {
+ if (rand < l[i].w) {
+ return l[i].o;
+ }
+ }
+ }
+
+ choose(l) {
+ return l[Math.floor(this.random() * l.length)];
+ }
+
+ /*
+ Iterates integers starting from `start` and finishes at `end` inclusively
+ and applies callback function `f`. The callback follows this signature:
+ f(i) {
+ ...
+ }
+
+ Return: undefined
+ */
+
+ iterate(start, end, f) {
+ let d = end - start;
+ let sd = Math.sign(d);
+ for (let i = 0; i < Math.abs(d); i++) {
+ f(start + i * sd);
+ }
+ }
+
+ clamp(min, x, max) {
+ return Math.min(Math.max(x, min), max);
+ }
+
+ inBound(min, x, max) {
+ return min <= x && x < max;
+ }
+
+ lerp(x1, y1, x2, y2, x) {
+ return y1 + (x - x1) * (y2 - y1) / (x2 - x1);
+ }
+}
+
+export let util = new Util();
diff --git a/src/World.js b/src/World.js
new file mode 100644
index 00000000..9f3090a9
--- /dev/null
+++ b/src/World.js
@@ -0,0 +1,555 @@
+import Tile from './Tile.js'
+import Agent from './Agent.js'
+import Area from './Area.js'
+import { util } from './Util.js'
+import Route from './Route.js'
+
+export default class World {
+ constructor(num_areas) {
+ this.agents = {};
+ this.areas = [];
+ this.routes = [];
+ this.num_areas = num_areas;
+ }
+
+ getTile(x, y) {
+ if (0 <= x && x < this.size && 0 <= y && y < this.size) {
+ return this.grid[x][y];
+ } else {
+ return null;
+ }
+ }
+
+ initWorld(world, myID) {
+ let { agents, size, seed } = world;
+ this.me = myID;
+ for (let id in agents) {
+ let agentID = parseInt(id);
+ this.agents[agentID] = new Agent(agents[id]);
+ }
+
+ util.seed(seed);
+ this.size = size;
+
+ // creating grid[]
+ this.grid = new Array(size);
+ for (let i = 0; i < size; i++) {
+ this.grid[i] = new Array(size);
+ }
+
+ // creating areas
+ this.defineNPAreas();
+ this.defineAreas();
+ this.defineAreaContent();
+ this.defineNPContent();
+ this.fillAreas();
+ this.fillRoutes();
+ this.repairWorld();
+}
+
+ defineNPAreas() {
+ for (let i = 0; i < this.size; i++) {
+ for (let j = 0; j < this.size; j++) {
+ this.grid[i][j] = new Tile('0', false);
+ }
+ }
+ }
+
+ generateArea(x, y) {
+ let padding = 30;
+ let sx = Math.floor(util.random() * this.size / 16 + this.size / 16);
+ let sy = Math.floor(util.random() * this.size / 16 + this.size / 16);
+ while (x + sx/2 > this.size - padding || x - sx/2 < padding) {
+ sx = Math.floor(util.random() * this.size);
+ }
+ while (y + sy/2 > this.size - padding || y - sy/2 < padding) {
+ sy = Math.floor(util.random() * this.size);
+ }
+ sx += (sx % 2 === 0) ? 0 : 2 - (sx % 2);
+ sy += (sy % 3 === 0) ? 0 : 3 - (sy % 3);
+ let area = new Area(x, y, sx, sy, 3, true, true);
+ this.areas.push(area);
+ return area;
+ }
+
+ defineAreas() {
+ // init area
+ let numAreas = this.num_areas;
+ let areaCnt = 1;
+ let stack = [];
+ let area = this.generateArea(256, 256);
+
+ let route;
+ let prev;
+ stack.push(area);
+ while (stack.length !== 0) {
+ area = stack.shift();
+ let num = util.random();
+ // let num = 0;
+ if (num < 0.5) {
+ // one path
+ let len = Math.floor(util.random() * 64 + 32);
+ let dir = Math.floor(util.random() * 4);
+ while (area.prev === dir) {
+ dir = Math.floor(util.random() * 4);
+ }
+ switch(dir) {
+ case 0:
+ route = this.generateRoute(area, len, 'north');
+ route.a2.prev = 0;
+ break;
+ case 1:
+ route = this.generateRoute(area, len, 'south');
+ route.a2.prev = 1;
+ break;
+ case 2:
+ route = this.generateRoute(area, len, 'east');
+ route.a2.prev = 2;
+ break;
+ case 3:
+ route = this.generateRoute(area, len, 'west');
+ route.a2.prev = 3;
+ break;
+ }
+ this.routes.push(route);
+ areaCnt += 1;
+ if (areaCnt < numAreas) {
+ stack.push(route.a2);
+ }
+ else {
+ break;
+ }
+ }
+ else {
+ // two paths
+ // one path
+ let route1, route2;
+ let len = Math.floor(util.random() * 64 + 32);
+ if (area.prev === 2 || area.prev === 3) {
+ route1 = this.generateRoute(area, len, 'north');
+ route2 = this.generateRoute(area, len, 'south');
+ route1.a2.prev = 0;
+ route2.a2.prev = 1;
+ }
+ else {
+ route1 = this.generateRoute(area, len, 'east');
+ route2 = this.generateRoute(area, len, 'west');
+ route1.a2.prev = 2;
+ route2.a2.prev = 3;
+ }
+ this.routes.push(route1, route2);
+ areaCnt += 2;
+ if (areaCnt < numAreas) {
+ stack.push(route1.a2);
+ stack.push(route2.a2);
+ }
+ else {
+ break;
+ }
+ }
+ }
+ }
+
+ generateRoute(area, len, dir) {
+ let {x, y} = area;
+ let newX, newY, newArea, route;
+ switch(dir) {
+ case 'north':
+ while (area.y - len < 0) {
+ len = Math.floor(util.random() * len);
+ }
+ newX = x;
+ newY = y - len;
+ area.outlets.push({x: x, y: y - area.sy});
+ newArea = this.generateArea(newX, newY);
+ newArea.outlets.push({x: newX, y: newY + newArea.ry});
+ route = new Route(area, newArea, newArea.biome, 'v');
+ this.drawRoute(route, 'y');
+ break;
+ case 'south':
+ while (area.y + len > this.size) {
+ len = Math.floor(util.random() * len);
+ }
+ newX = x;
+ newY = y + len;
+ area.outlets.push({x: x, y: y + area.sy});
+ newArea = this.generateArea(newX, newY);
+ newArea.outlets.push({x: newX, y: newY - newArea.ry});
+ route = new Route(area, newArea, newArea.biome, 'v');
+ this.drawRoute(route, 'y');
+ break;
+ case 'east':
+ while (area.x + len > this.size) {
+ len = Math.floor(util.random() * len);
+ }
+ newX = x + len;
+ newY = y;
+ area.outlets.push({x: x + area.rx, y: y});
+ newArea = this.generateArea(newX, newY);
+ newArea.outlets.push({x: newX - newArea.rx, y: newY});
+ route = new Route(area, newArea, newArea.biome, 'h');
+ this.drawRoute(route, 'x');
+ break;
+ case 'west':
+ while (area.y - len < 0) {
+ len = Math.floor(util.random() * len);
+ }
+ newX = x - len;
+ newY = y;
+ area.outlets.push({x: x - area.rx, y: y});
+ newArea = this.generateArea(newX, newY);
+ newArea.outlets.push({x: newX + newArea.rx, y: newY});
+ route = new Route(area, newArea, newArea.biome, 'h');
+ this.drawRoute(route, 'x');
+ break;
+ }
+ return route;
+ }
+
+ // connect a1 to a2
+ drawRoute(route, dir) {
+ let {a1, a2} = route;
+ let pathRadius = 8;
+ let del;
+ let a1x = a1.x;
+ let a1y = a1.y;
+ if (dir === 'x') {
+ del = a2.x - a1.x;
+ for (let i = 0; i < Math.abs(del); i++) {
+ a1x += Math.sign(del);
+ for (let j = -pathRadius; j < pathRadius; j++) {
+ if (0 <= a1y + j && a1y + j < this.size) {
+ this.grid[a1x][a1y + j] = new Tile(route.biome, true);
+ }
+ }
+ }
+ }
+ else {
+ del = a2.y - a1.y;
+ for (let i = 0; i < Math.abs(del); i++) {
+ a1y += Math.sign(del);
+ for (let j = -pathRadius; j < pathRadius; j++) {
+ if (0 <= a1x + j && a1x + j < this.size) {
+ this.grid[a1x + j][a1y] = new Tile(route.biome, true);
+ }
+ }
+ }
+ }
+ }
+
+ defineAreaContent() {
+ // draw cities
+ for (let c = 0; c < this.areas.length; c++) {
+ let area = this.areas[c];
+ for (let i = Math.floor(area.x - area.rx); i < area.x + area.rx; i++) {
+ for (let j = Math.floor(area.y - area.ry); j < area.y + area.ry; j++) {
+ if (0 <= i && i < this.size && 0 <= j && j < this.size) {
+ this.grid[i][j] = new Tile(area.biome, true);
+ }
+ }
+ }
+ }
+ }
+
+ defineNPContent() {
+ for (let i = 0; i < this.size; i++) {
+ for (let j = 0; j < this.size; j++) {
+ // check if this is NP tile
+ let npTile = this.grid[i][j];
+ if (npTile.traversable === false && i + 1 < this.size && i - 1 >= 0) {
+ // check if this is a up, down, left, right
+ let uTile = this.grid[i][j-1];
+ if (uTile !== undefined && (uTile.spriteID === 'grass' || uTile.spriteID === 'sand')) {
+ this.grid[i][j] = new Tile('mtn-d', false, 0, -1);
+ for (let k = 1; k < 5; k++) {
+ this.grid[i][j+k] = new Tile('mtn-d', false, 0, 0);
+ }
+ continue;
+ }
+ else if (uTile !== undefined && (uTile.spriteID === 'water')) {
+ // let rand = util.random();
+ this.grid[i][j] = new Tile('wtr-1', false);
+ for (let k = 1; k < 5; k++) {
+ this.grid[i][j+k] = new Tile('dwater', false);
+ }
+ continue;
+ }
+ let dTile = this.grid[i][j+1];
+ if (dTile !== undefined && (dTile.spriteID === 'grass' || dTile.spriteID === 'sand')) {
+ this.grid[i][j] = new Tile('mtn-d', false, 0, 1);
+ for (let k = 1; k < 5; k++) {
+ this.grid[i][j-k] = new Tile('mtn-d', false, 0, 0);
+ }
+ continue;
+ }
+ else if (dTile !== undefined && (dTile.spriteID === 'water')) {
+ this.grid[i][j] = new Tile('wtr-1', false);
+ for (let k = 1; k < 5; k++) {
+ this.grid[i][j-k] = new Tile('dwater', false);
+ }
+ continue;
+ }
+ let lTile = this.grid[i-1][j];
+ if (lTile !== undefined && (lTile.spriteID === 'grass' || lTile.spriteID === 'sand')) {
+ this.grid[i][j] = new Tile('mtn-d', false, -1, 0);
+ for (let k = 1; k < 7; k++) {
+ this.grid[i+k][j] = new Tile('mtn-d', false, 0, 0);
+ }
+ continue;
+ }
+ else if (lTile !== undefined && (lTile.spriteID === 'water')) {
+ this.grid[i][j] = new Tile('wtr-1', false);
+ for (let k = 1; k < 7; k++) {
+ this.grid[i+k][j] = new Tile('dwater', false);
+ }
+ continue;
+ }
+
+ let rTile = this.grid[i+1][j];
+ if (rTile !== undefined && (rTile.spriteID === 'grass' || rTile.spriteID === 'sand')) {
+ this.grid[i][j] = new Tile('mtn-d', false, 1, 0);
+ for (let k = 1; k < 7; k++) {
+ this.grid[i-k][j] = new Tile('mtn-d', false, 0, 0);
+ }
+ continue;
+ }
+ else if (rTile !== undefined && (rTile.spriteID === 'water')) {
+ this.grid[i][j] = new Tile('wtr-1', false);
+ for (let k = 1; k < 7; k++) {
+ this.grid[i-k][j] = new Tile('dwater', false);
+ }
+ continue;
+ }
+
+ // check for corners
+ let urTile = this.grid[i+1][j+1];
+ if (urTile !== undefined && (urTile.spriteID === 'grass' || urTile.spriteID === 'sand')) {
+ this.grid[i][j] = new Tile('mtn-d', false, 2, -1);
+ // double for loop to fill in the 'rectangle'
+ for (let k = 0; k < 7; k++) {
+ for (let l = 0; l < 5; l++) {
+ if (i - k >= 0 && j - l >= 0 && this.grid[i-k][j-l].spriteID === '0') {
+ this.grid[i-k][j-l] = new Tile('mtn-d', false, 0, 0);
+ }
+ }
+ }
+ }
+
+ let ulTile = this.grid[i-1][j+1];
+ if (ulTile !== undefined && (ulTile.spriteID === 'grass' || ulTile.spriteID === 'sand')) {
+ this.grid[i][j] = new Tile('mtn-d', false, 4, -1);
+ // double for loop to fill in the 'rectangle'
+ for (let k = 0; k < 7; k++) {
+ for (let l = 0; l < 5; l++) {
+ if (k === 0 && l === 0) {
+ continue;
+ }
+ if (i + k < this.size && j - l >= 0 && this.grid[i+k][j-l].spriteID === '0') {
+ this.grid[i+k][j-l] = new Tile('mtn-d', false, 0, 0);
+ }
+ }
+ }
+ }
+
+ let brTile = this.grid[i+1][j-1];
+ if (brTile !== undefined && (brTile.spriteID === 'grass' || brTile.spriteID === 'sand')) {
+ this.grid[i][j] = new Tile('mtn-d', false, 2, 1);
+ // double for loop to fill in the 'rectangle'
+ for (let k = 0; k < 7; k++) {
+ for (let l = 0; l < 5; l++) {
+ if (i - k >= 0 && j + l < this.size && this.grid[i-k][j+l].spriteID === '0') {
+ this.grid[i-k][j+l] = new Tile('mtn-d', false, 0, 0);
+ }
+ }
+ }
+ }
+
+ let blTile = this.grid[i-1][j-1];
+ if (blTile !== undefined && (blTile.spriteID === 'grass' || blTile.spriteID === 'sand')) {
+ this.grid[i][j] = new Tile('mtn-d', false, 4, 1);
+ // double for loop to fill in the 'rectangle'
+ for (let k = 0; k < 7; k++) {
+ for (let l = 0; l < 5; l++) {
+ if (k === 0 && l === 0) {
+ continue;
+ }
+ if (i + k < this.size && j + l < this.size && this.grid[i+k][j+l].spriteID === '0') {
+ this.grid[i+k][j+l] = new Tile('mtn-d', false, 0, 0);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fillNPContent(tile, i, j) {
+
+ }
+
+ fillAreas() {
+ this.areas.forEach(area => {
+ area.init(this.grid);
+ });
+ }
+
+ fillRoutes() {
+ this.routes.forEach(route => {
+ route.init(this.grid);
+ });
+ }
+
+
+ findNearestArea(area, exclude) {
+ let {x, y, rx, ry} = area;
+ let min_dist = undefined;
+ let closest = undefined;
+ for (let i = 0; i < this.areas.length; i++) {
+ let a = this.areas[i];
+ if (a == area || exclude.indexOf(a) !== -1) {
+ continue;
+ }
+ let dist = Math.sqrt(Math.pow((a.x - x), 2) + Math.pow((a.y - y), 2));
+ if (dist < min_dist || min_dist === undefined) {
+ min_dist = dist;
+ closest = a;
+ }
+ }
+ return closest;
+ }
+
+ // avgDist defined by user
+ goodAvgDist(x, y) {
+ if (this.areas.length === 0) {
+ return true;
+ }
+ let a, min_dist, closest;
+ for (let i = 0; i < this.areas.length; i++) {
+ a = this.areas[i];
+ let dist = Math.sqrt(Math.pow((a.x - x), 2) + Math.pow((a.y - y), 2));
+ if (dist < min_dist || min_dist === undefined) {
+ min_dist = dist;
+ closest = a;
+ }
+ }
+ // calculate distance to closest area
+ let dist = Math.sqrt(Math.pow((closest.x - x), 2) + Math.pow((closest.y - y), 2));
+ if (dist >= 30) {
+ return true;
+ }
+ return false;
+ }
+
+ repairWorld() {
+ let { grid } = this;
+ for (let i = 1; i < this.size - 1; i++) {
+ for (let j = 1; j < this.size - 1; j++) {
+ let tile = grid[i][j];
+ let ntile = grid[i][j - 1];
+ let stile = grid[i][j + 1];
+ let wtile = grid[i - 1][j];
+ let etile = grid[i + 1][j];
+ if (tile.spriteID === 'sand') {
+ if (etile.spriteID === 'water' || etile.spriteID === 'R1') {
+ tile.spriteID = 'R1';
+ } else if (wtile.spriteID === 'water' || wtile.spriteID === 'R1') {
+ tile.spriteID = 'R1';
+ }
+ if (stile.spriteID === 'water' || stile.spriteID === 'R1') {
+ tile.spriteID = 'R1';
+ } else if (ntile.spriteID === 'water' || ntile.spriteID === 'R1') {
+ tile.spriteID = 'R1';
+ }
+ }
+
+ if (tile.spriteID === 'R1') {
+ tile.offset(1, 1);
+ if (grid[i + 1][j].spriteID === 'water') {
+ tile.offx += 1;
+ } else if (grid[i - 1][j].spriteID === 'water') {
+ tile.offx -= 1;
+ }
+ if (grid[i][j - 1].spriteID === 'water') {
+ tile.offy -= 1;
+ } else if (grid[i][j + 1].spriteID === 'water') {
+ tile.offy += 1;
+ }
+ if (grid[i + 1][j + 1].spriteID === 'water' && tile.offx === 1 && tile.offy === 1) {
+ tile.offset(2, -1);
+ } else if (grid[i + 1][j - 1].spriteID === 'water' && tile.offx === 1 && tile.offy === 1) {
+ tile.offset(2, -2);
+ } else if (grid[i - 1][j + 1].spriteID === 'water' && tile.offx === 1 && tile.offy === 1) {
+ tile.offset(1, -1);
+ } else if (grid[i - 1][j - 1].spriteID === 'water' && tile.offx === 1 && tile.offy === 1) {
+ tile.offset(1, -2);
+ }
+ }
+ }
+ }
+ }
+
+ // TODO: should we differentiate between agent types? :thinking:
+ addAgents(agents) {
+ agents.forEach(a => {
+ this.agents[a.id] = new Agent(a);
+ });
+ }
+
+ updateAgents(agents) {
+ agents.forEach(a => {
+ this.agents[a.id].update(a);
+ });
+ }
+
+ deleteAgents(agents) {
+ agents.forEach(a => {
+ delete this.agents[a.id];
+ });
+ }
+
+ getMe() {
+ return this.agents[this.me];
+ }
+
+ morph() {
+ let needsRerender = false;
+ this.routes.forEach(route => {
+ let valid = true;
+ Object.keys(this.agents).forEach(agent => {
+ let a = this.agents[agent];
+ valid &= !util.inBound(route.x - route.rx, a.pos.x, route.x + route.rx);
+ valid &= !util.inBound(route.y - route.ry, a.pos.y, route.y + route.ry);
+ });
+ if (valid && util.random() < route.waitThatWasntThereBeforeWTF) {
+ route.init(this.grid);
+ needsRerender = true;
+ }
+ });
+ this.areas.forEach(area => {
+ let valid = true;
+ Object.keys(this.agents).forEach(agent => {
+ let a = this.agents[agent];
+ valid &= !util.inBound(area.x - area.rx, a.pos.x, area.x + area.rx);
+ valid &= !util.inBound(area.y - area.ry, a.pos.y, area.y + area.ry);
+ });
+ if (valid && util.random() < area.waitThatWasntThereBeforeWTF) {
+ area.init(this.grid);
+ needsRerender = true;
+ }
+ });
+ return needsRerender;
+ }
+
+ serialize() {
+ let agents = [];
+ for (let p in this.agents) {
+ agents.push(this.agents[p]);
+ }
+ return {
+ agents: agents,
+ size: this.size,
+ seed: this.seed
+ };
+ }
+}
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 00000000..1b8d7d86
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,8 @@
+import App from './App';
+
+
+(function main() {
+ let app = new App();
+ window.addEventListener('load', app.onLoad.bind(app));
+ window.addEventListener('resize', app.onResize.bind(app), false);
+})();
diff --git a/ss1.png b/ss1.png
new file mode 100644
index 00000000..9fee16df
Binary files /dev/null and b/ss1.png differ
diff --git a/ss2.png b/ss2.png
new file mode 100644
index 00000000..216ad7e8
Binary files /dev/null and b/ss2.png differ
diff --git a/ss3.png b/ss3.png
new file mode 100644
index 00000000..0ddd902e
Binary files /dev/null and b/ss3.png differ
diff --git a/ss4.png b/ss4.png
new file mode 100644
index 00000000..b989777c
Binary files /dev/null and b/ss4.png differ
diff --git a/ss5.png b/ss5.png
new file mode 100644
index 00000000..7dbda259
Binary files /dev/null and b/ss5.png differ
diff --git a/ss6.png b/ss6.png
new file mode 100644
index 00000000..b3875352
Binary files /dev/null and b/ss6.png differ
diff --git a/ss7.png b/ss7.png
new file mode 100644
index 00000000..63363c95
Binary files /dev/null and b/ss7.png differ
diff --git a/ss8.png b/ss8.png
new file mode 100644
index 00000000..4ab6b450
Binary files /dev/null and b/ss8.png differ
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 00000000..12eb33f1
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,33 @@
+const path = require('path');
+
+module.exports = {
+ entry: path.join(__dirname, "src/main"),
+ output: {
+ path: path.join(__dirname, "build"),
+ filename: "bundle.js"
+ },
+ module: {
+ loaders: [
+ {
+ test: /\.js$/,
+ exclude: /(node_modules|bower_components)/,
+ loader: 'babel-loader',
+ query: {
+ presets: ['es2015']
+ }
+ },
+ {
+ test: /\.glsl$/,
+ loader: 'webpack-glsl-loader'
+ },
+ {
+ test: /\.(obj|bmp|gif|png)$/,
+ loader: 'file-loader?name=./assets/[name]-[hash:6].[ext]'
+ }
+ ]
+ },
+ devtool: 'source-map',
+ devServer: {
+ port: 7000
+ }
+}
\ No newline at end of file