diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 2adc1c9e..29adcd0e --- a/README.md +++ b/README.md @@ -1 +1,37 @@ -# FinalProject \ No newline at end of file +# Procedural Pokemon +no memes allowed + + +## Milestone 2 Accomplishments +### David +- Made the game multiplayer. Networking for proceduralism. (this is like a 4/5 on the difficulty scale) +- Slick differential updates to sync game state +- Fixed all (I think) server-side race conditions and bugs +- Implemented randomized behavior based off of a global seed variables defined by the user, so instaces of our randomized pokemon game are standardized for each player on the network +- Refactors + +### Joseph +- Updated sprites for the character, terrtain, and Pokemon +- Implemented biomes - world is split into four quadrants (grass, desert, water, snow), and the sprite textures now have color to better display the distinct biomes +- Expanded Tile data structure to hold pokemon information, as well as whether or not certain parts of the terrain are traversable +- Completed random pokemon spawn algorithm to spawn specific pokemon types in each biome. For example, Charmanders will not spawn in the desert, Bulbasaur's will not spawn in the water, etc. +- Changed Sprite class so it is able to handle sprites not just of size 16x16 +-Files changed: Tile.js, World.js, RenderEngine.js, App.js, Sprite.js + +#### All tasks specified in the milestone (updated doc we pinged to rachel and later forwarded to Adam) were completed + +## Milestone 1 Accomplishments + +### David +- Configured infrastructure and set up the project. +- Implemented sprite importing and rendering. +- Created a base world using the current sprite set. +- Implemented player.js and enabled player movement throughout the scene. +- Implemented the viewport so we only see a certain poprtion of the world. +- File changed: Made changes in all files (see commits). + +### Joseph +- Implemented Tile logic for use by the Grid. Tiles will be used to later determine which parts of the world belong to which biome, as well as which parts of the world are traversable, or will hold wild pokemon, etc. Currently used to holds symbols that tells the render engine what to render. +- Implemented render engine to render information in a tile. For now just the ability to render a single sprite to the canvas. David expanded this functionality to create the vanilla world you see. +- Developed logic for randomly spawning 'pokemon' throughout the world. Each index in the grid currently has some percentage change of spawning a pokemon. Currently the pokemon are represented by a symbol. +- Files changed: Tile.js, Grid.js, RenderEngine.js, App.js diff --git a/assets/README.md b/assets/README.md new file mode 100755 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 100755 index 00000000..13ad4bf1 Binary files /dev/null and b/assets/biomes.png differ diff --git a/assets/biomes.psd b/assets/biomes.psd new file mode 100755 index 00000000..72406b02 Binary files /dev/null and b/assets/biomes.psd differ diff --git a/assets/overworld.png b/assets/overworld.png new file mode 100755 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 100755 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 100755 index 00000000..49bdb56b Binary files /dev/null and b/assets/player.png differ diff --git a/assets/player2.png b/assets/player2.png new file mode 100755 index 00000000..d411710b Binary files /dev/null and b/assets/player2.png differ diff --git a/assets/pokemon.png b/assets/pokemon.png new file mode 100755 index 00000000..69e64ccd Binary files /dev/null and b/assets/pokemon.png differ diff --git a/assets/sprites.png b/assets/sprites.png new file mode 100755 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 100755 index 00000000..e69de29b diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 00000000..7d7c7a04 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,35 @@ +PROJ_DIR=~/workspace/procedural-pokemon +BUILD_DIR=$PROJ_DIR/build +DEPLOY_DIR=$PROJ_DIR/../procedural_pokemon_build +set -o errexit + +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 +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 100755 index 00000000..5b3cc050 --- /dev/null +++ b/index.html @@ -0,0 +1,10 @@ + + + + Procedural Pokemon + + + + + + diff --git a/package.json b/package.json new file mode 100755 index 00000000..c92ddc28 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "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", + "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 100755 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 100755 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 100755 index 00000000..b07236f9 --- /dev/null +++ b/server/controller.go @@ -0,0 +1,116 @@ +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 + Game Game +} + +// NewController creates a new controller +func NewController() Controller { + g := Game{ + World: World{ + Size: 100, + Seed: 0, + Players: []Player{}, + }, + } + c := Controller{ + ReceiveChan: make(chan InboundMessage, 128), + SendChan: make(chan OutboundMessage, 128), + Clients: make(map[int32]*Client), + Game: g, + } + 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 "sync": + C.handleSync(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 + 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 100755 index 00000000..1322d3a8 --- /dev/null +++ b/server/gameinstance.go @@ -0,0 +1,6 @@ +package main + +// GameInstance encapsulates game instance data +type GameInstance struct { + Players []Player +} diff --git a/server/handler.go b/server/handler.go new file mode 100755 index 00000000..5a4835a1 --- /dev/null +++ b/server/handler.go @@ -0,0 +1,45 @@ +package main + +func (C *Controller) handleInit(msg InboundMessage) { + p := Player{ + Pos: Point{X: 10, Y: 10}, + ID: msg.Sender, + } + players := C.Game.World.Players + players = append(players, p) + C.Game.World.Players = players + d := Data{ + Message: "alrighty, here you go", + Game: C.Game, + } + + C.sendMessage("init", d, msg.Sender) + C.broadcastMessage("sync", d, msg.Sender) +} + +func (C *Controller) handleSync(msg InboundMessage) { + C.Game = msg.Data.Game + d := Data{ + Message: "catch up plz", + Game: C.Game, + } + C.broadcastMessage("sync", d, msg.Sender) +} + +func (C *Controller) handleDisconnect(id int32) { + players := C.Game.World.Players + index := -1 + for i, p := range players { + if p.ID == id { + index = i + } + } + players = append(players[:index], players[index+1:]...) + C.Game.World.Players = players + delete(C.Clients, id) + d := Data{ + Message: "catch up plz", + Game: C.Game, + } + C.broadcastMessage("sync", d, -1) +} diff --git a/server/main.go b/server/main.go new file mode 100755 index 00000000..fa56f0e2 --- /dev/null +++ b/server/main.go @@ -0,0 +1,67 @@ +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 + }, + } + c = NewController() +) + +func main() { + env := os.Getenv("environment") + + go c.run() + r := mux.NewRouter() + + r.HandleFunc("/health", 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("server.crt", "server.key")) + } 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, "we healthy bois") +} + +// 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 100755 index 00000000..d2e6614a --- /dev/null +++ b/server/models.go @@ -0,0 +1,44 @@ +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 payload of each message +type Data struct { + Message string `json:"message"` + Game Game `json:"game"` +} + +// Player encapsulates data for users +type Player struct { + Pos Point `json:"pos"` + ID int32 `json:"id"` +} + +// World encapsulates data for the world state +type World struct { + Players []Player `json:"players"` + Size int32 `json:"size"` + Seed int32 `json:"seed"` +} + +type Game struct { + World World `json:"world"` +} + +// Point is a cartesian tuple +type Point struct { + X int32 `json:"x"` + Y int32 `json:"y"` +} diff --git a/src/App.js b/src/App.js new file mode 100755 index 00000000..3916fae2 --- /dev/null +++ b/src/App.js @@ -0,0 +1,124 @@ +import World from './World.js' +import Player from './Player.js' +import RenderEngine from './RenderEngine.js' +import Sprite from './Sprite.js' + +const ASSETS = './assets'; +const SERVER_URL = 'wss://davidliao.me:8000/play'; +const RESOLUTION_SCALE = 3; +const DEFAULT_WORLD_SIZE = 100; +const DEFAULT_PLAYER_POS = {x: 10, y: 10}; +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.terrainSpriteSrc = `${ASSETS}/biomes.png`; + this.pokemonSpriteSrc = `${ASSETS}/pokemon.png`; + this.playerSpriteSrc = `${ASSETS}/player.png`; + this.clientID = -1; // default null value for client ID + } + + setup() { + this.setupWebsocket(); + this.setupGame(); + this.setupEventListeners(); + } + + setupGame() { + this.world = new World(DEFAULT_WORLD_SIZE); + this.re = new RenderEngine( + this.canvas, + this.terrainSprite, + this.playerSprite, + this.pokemonSprite, + this.world); + } + + setupEventListeners() { + window.addEventListener('keydown', (event) => { + let { me } = this.world; + switch (event.keyCode) { + case 32: + break; + case 37: + me.move("left", this.world); + break; + case 38: + me.move("down", this.world); + break; + case 39: + me.move("right", this.world); + break; + case 40: + me.move("up", this.world); + break; + } + this.sendEvent('sync', { + message: 'syncing shit', + game: { + world: this.world.serialize() + } + }); + this.re.render(); + }); + } + + onLoad() { + document.body.appendChild(this.canvas); + this.terrainSprite = new Sprite(this.terrainSpriteSrc, 16, 16, () => { + this.pokemonSprite = new Sprite(this.pokemonSpriteSrc, 64, 64, () => { + this.playerSprite = new Sprite(this.playerSpriteSrc, 19, 24, () => { + this.setup(); + }); + }); + }); + } + + onUpdate() { + } + + onResize() { + + } + +/********************** + 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.game.world, id); + break; + case 'sync': + this.world.syncPlayers(data.game.world.players, id); + break; + default: + console.log('event not handled', e.data); + return; + } + this.re.render(); + } +} diff --git a/src/Player.js b/src/Player.js new file mode 100755 index 00000000..d9a97c48 --- /dev/null +++ b/src/Player.js @@ -0,0 +1,55 @@ +export default class Player { + constructor(pos, id) { + this.pos = {x: pos.x, y: pos.y}; + this.id = id; + } + + move(dir, world) { + 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 < world.size + && world.getTile(this.pos.x, this.pos.y+1).traversable) { + this.pos.y += 1; + } + break; + case 'down': + if (this.pos.y - 1 >= 0 + && 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(player) { + this.pos = player.pos; + this.id = player.id; + } + + serialize() { + return { + pos: { + x: this.pos.x, + y: this.pos.y + }, + id: this.id, + }; + } +} diff --git a/src/RenderEngine.js b/src/RenderEngine.js new file mode 100755 index 00000000..769e0620 --- /dev/null +++ b/src/RenderEngine.js @@ -0,0 +1,126 @@ +const TILEMAP = { + '0': {x: 32, y: 304}, + '1': {x: 16, y: 16}, + '2': {x: 48, y: 16}, + 'P': {x: 7 * 16, y: 16}, + 'O': {x: 6 * 16, y: 2 * 16} +}; + +const TERRAIN_TILEMAP = { + '0': {x: 224, y: 224}, + 'G': {x: 0, y: 2 * 16}, // grass + 'S': {x: 144, y: 48}, // snow + 'W': {x: 432, y: 112}, // water + '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 + 'D': {x: 721, y: 48}, // sand + 'SB': {x: 192, y: 112}, // snow bush + 'WR': {x: 416, y: 128} // water rock + // more to come... +}; + +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} +}; + +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); + this.viewport = new Array(this.vpWidth); + for (let i = 0; i < this.vpWidth; i++) { + this.viewport[i] = new Array(this.vpHeight); + } + } + + render() { + this._renderTerrain(); + this._renderCharacters(); + this._renderPokemon(); + } + + _renderTerrain() { + let { pos } = this.world.me; + 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.symbol, 'terrain', i, j); + } + } + } + + _renderCharacters() { + let { players } = this.world; + for (let p in players) { + let player = players[p]; + let tile = this.world.getTile(player.pos.x, player.pos.y); + let { me } = this.world; + this.drawTile(null, 'player', player.pos.x - me.pos.x + this.halfWidth, player.pos.y - me.pos.y + this.halfHeight); + } + } + + _renderPokemon() { + let {pos} = this.world.me; + 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); + if (tile.pokemon !== undefined) { + this.drawTile(tile.pokemon, 'pokemon', i, j); + } + } + } + } + + drawTile(tile, type, x, y) { + let spritePos, sprite; + switch (type) { + case 'player': + spritePos = {x: 82, y: 125}; + sprite = this.playerSprite; + break; + case 'terrain': + spritePos = TERRAIN_TILEMAP[tile]; + sprite = this.terrainSprite; + break; + case 'pokemon': + spritePos = POKE_TILEMAP[tile]; + sprite = this.pokemonSprite; + break; + } + let { tileHeight, tileWidth } = sprite; + let canvasTileWidth = this.canvas.width / this.vpWidth; + let canvasTileHeight = this.canvas.height / this.vpHeight; + let canvasPosx = x * canvasTileWidth; + let canvasPosy = y * canvasTileHeight; + this.ctx.drawImage( + sprite.image, + spritePos.x, spritePos.y, + tileWidth, tileHeight, + canvasPosx, canvasPosy, + canvasTileWidth, canvasTileHeight + ); + } +} diff --git a/src/Sprite.js b/src/Sprite.js new file mode 100755 index 00000000..f6e18299 --- /dev/null +++ b/src/Sprite.js @@ -0,0 +1,15 @@ +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/Tile.js b/src/Tile.js new file mode 100755 index 00000000..9749da8b --- /dev/null +++ b/src/Tile.js @@ -0,0 +1,7 @@ +export default class Tile { + constructor(sym, t) { + this.symbol = sym; + this.pokemon = undefined; + this.traversable = t; + } +} diff --git a/src/Util.js b/src/Util.js new file mode 100755 index 00000000..ba395b41 --- /dev/null +++ b/src/Util.js @@ -0,0 +1,23 @@ +export default class Util { + constructor() { + this.randSeed = 0; + } + + seed(seed) { + this._seed = seed % 2147483647; + if (this._seed <= 0) { + this._seed += 2147483646; + } + this.randSeed = seed; + } + + randInt() { + return this._seed = this._seed * 16807 % 2147483647 - 1; + } + + random() { + return (this.randInt() - 1) / 2147483646; + } +} + +export let util = new Util(); diff --git a/src/Viewport.js b/src/Viewport.js new file mode 100755 index 00000000..41ac92f5 --- /dev/null +++ b/src/Viewport.js @@ -0,0 +1,39 @@ +import Tile from './Tile.js' +const PLAYER = 'P'; + +export default class Viewport { + constructor(world, pos) { + this.width = 15; + this.height = 11; + this.halfWidth = Math.floor(this.width / 2); + this.halfHeight = Math.floor(this.height / 2); + + this.cells = new Array(this.width); + for (let i = 0; i < this.width; i++) { + this.cells[i] = new Array(this.height); + } + this.focus = {x: pos.x, y: pos.y}; + this.world = world; + this._sampleTiles(); + } + + updateFocus(x, y) { + this.focus.x = x; + this.focus.y = y; + this._sampleTiles(); + } + + _sampleTiles() { + this.world.resetGrid(); + for (let i = 0; i < this.width; i++) { + for (let j = 0; j < this.height; j++) { + let x = i + this.focus.x - this.halfWidth; + let y = j + this.focus.y - this.halfHeight; + this.cells[i][j] = this.world.getTile(x, y); + } + } + this.cells[Math.floor(this.width / 2)][Math.floor(this.height / 2)].hasPlayer = true; + } + + +} diff --git a/src/World.js b/src/World.js new file mode 100755 index 00000000..e018e3be --- /dev/null +++ b/src/World.js @@ -0,0 +1,155 @@ +import Tile from './Tile.js' +import Player from './Player.js' +import { util } from './Util.js' + +export default class World { + constructor() { + this.players = {}; + this.me = new Player({x: 0, y: 0}, -1); + } + + getTile(x, y) { + if (0 <= x && x < this.size && 0 <= y && y < this.size) { + return this.grid[x][y]; + } else { + return new Tile('0'); + } + } + + initWorld(world, id) { + let { players, size, seed } = world; + players.forEach(p => { + this.players[p.id] = p; + if (p.id === id) { + this.me.update(p); + } + }); + + util.seed(seed); + this.size = size; + this.grid = new Array(size); + for (let i = 0; i < size; i++) { + this.grid[i] = new Array(size); + } + + // generate four regions + // top-left: grassy plains + for (let i = 0; i < size / 2.0; i++) { + for (let j = 0; j < size / 2.0; j++) { + let rand = util.random(); + if (rand < 0.75) { + this.grid[i][j] = new Tile('G', true); + this.randomPokemon(i, j, 'grass') // can also not add a pokemon + } else if (rand < 0.9) { + this.grid[i][j] = new Tile('F', true); + } else if (rand < 0.95) { + this.grid[i][j] = new Tile('B', false); + } else { + this.grid[i][j] = new Tile('F2', true); + } + } + } + // top-right: snow region + for (let i = size / 2.0; i < size; i++) { + for (let j = 0; j < size / 2.0; j++) { + let rand = util.random(); + if (rand < 0.8) { + this.grid[i][j] = new Tile('S', true); + this.randomPokemon(i, j, 'snow') // can also not add a pokemon + } else { + this.grid[i][j] = new Tile('SB', false); + } + } + } + // bottom-left: desert rocky area + for (let i = 0; i < size; i++) { + for (let j = size / 2.0; j < size; j++) { + let rand = util.random(); + if (rand < 0.8) { + this.grid[i][j] = new Tile('D', true); + this.randomPokemon(i, j, 'sand') // can also not add a pokemon + } else { + this.grid[i][j] = new Tile('DR', false); + } + } + } + // bottom-right: water region + for (let i = size / 2.0; i < size; i++) { + for (let j = size / 2.0; j < size; j++) { + let rand = util.random(); + if (rand < 0.8) { + this.grid[i][j] = new Tile('W', true); + this.randomPokemon(i, j, 'water') // can also not add a pokemon + } else { + this.grid[i][j] = new Tile('WR', false); + } + } + } + } + + syncPlayers(players, id) { + // TODO: eliminate sync race condition serverside :( + this.players = {}; + players.forEach(p => { + if (p.id === id) { + return; + } + this.players[p.id] = p; + }); + this.players[this.me.id] = this.me; + } + + resetGrid() { + for (let i = 0; i < this.size; i++) { + for (let j = 0; j < this.size; j++) { + this.grid[i][j].hasPlayer = false; + } + } + } + + randomPokemon(i, j, region) { + let rand = util.random(); + switch(region) { + case 'grass': + if (rand < 0.1) { + this.grid[i][j].pokemon = 'g1'; + } else if (rand < 0.15) { + this.grid[i][j].pokemon = 'g2'; + } else if (rand < 0.2) { + this.grid[i][j].pokemon = 'g3'; + } + break; + case 'water': + if (rand < 0.1) { + this.grid[i][j].pokemon = 'w1'; + } + break; + case 'sand': + if (rand < 0.1) { + this.grid[i][j].pokemon = 's1'; + } else if (rand < 0.15) { + this.grid[i][j].pokemon = 's2'; + } else if (rand < 0.2) { + this.grid[i][j].pokemon = 's3'; + } + break; + case 'snow': + if (rand < 0.01) { + this.grid[i][j].pokemon = 'i1'; + } + break; + } + } + + serialize() { + let players = []; + for (let p in this.players) { + players.push(this.players[p]); + } + return { + players: players, + size: this.size, + seed: this.seed + }; + } +} diff --git a/src/main.js b/src/main.js new file mode 100755 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/webpack.config.js b/webpack.config.js new file mode 100755 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