import { ContextI } from "../ContextI" import { segments } from "../shared/segments" import { audio, playSong, disableSound, enableSound, stopSong, playSound } from '../shared/audio' import { StateI } from "./StateI" import * as PIXI from 'pixi.js' import * as planck from 'planck' import { DecorationInstance } from "../shared/decors" import { SpriteInstance, sprites } from "../shared/sprites" import { ShapeCircle, ShapePoints } from "../data/sprite" import { SegmentZone } from "../data/segment" import { Zone } from "../live/Zone" import { Entity, Sensor } from "../live/Entity" import { AnimalEntity, isAnimalEntity } from "../live/AnimalEntity" import { Action, adjustAction } from "../live/Action" import { WorldContext } from "../live/World" import { animals } from "../data/animals" import { GibletEntity } from "../live/GibletEntity" import { bodyTextStyle, buttonBlurStyle, buttonHoverStyle } from "../styles" export interface PIXIMissingColorMatrix extends PIXI.Filter { night(intensity: number, multiply: boolean): void contrast(amount: number, multiply: boolean): void brightness(amount: number, multiply: boolean): void saturate(amount: number, multiply: boolean): void hue(rotation: number, multiply: boolean): void colorTone(desaturation: number, toned: number, lightColor: number, darkColor: number, multiple: boolean): void reset(): void } export interface Layer { title: string container: PIXI.Container decorations: DecorationInstance[] colorMatrix: PIXIMissingColorMatrix } export function GameState(ctx: ContextI, selectedAnimal: string, selectedSegment: string): StateI { //disableSound() let isNight = false let modeTimer = 0 let nightTime = 30 * 1000 let dayTime = 30 * 1000 let daysSurvived = -1 let lastTime: number = performance.now() let player: AnimalEntity let nightIndicated = false // Game Over screenie let gameOverShown = false let gameOverPending = false let gameOverScreen = new PIXI.Container() let gameOverBackground = PIXI.Sprite.from(PIXI.Texture.WHITE) gameOverBackground.tint = 0x111111 gameOverBackground.alpha = 0.75 let gameOverTitle = new PIXI.Text('You Have Been Hunted', bodyTextStyle()) let gameOverKills = new PIXI.Text('Hunted: ', bodyTextStyle()) let gameOverKiller = new PIXI.Text('Slain By: ', bodyTextStyle()) let gameOverDays = new PIXI.Text('You survived', bodyTextStyle()) let returnToMain = new PIXI.Text('Hunt Again', buttonBlurStyle()) returnToMain.interactive = true returnToMain.on('pointerup', () => { ctx.pop() }) returnToMain.on('pointerover', () => { returnToMain.style = buttonHoverStyle() }) returnToMain.on('pointerout', () => { returnToMain.style = buttonBlurStyle() }) gameOverScreen.addChild(gameOverBackground) gameOverScreen.addChild(gameOverTitle) gameOverScreen.addChild(gameOverKills) gameOverScreen.addChild(gameOverKiller) gameOverScreen.addChild(gameOverDays) gameOverScreen.addChild(returnToMain) let updateGameOver = () => { gameOverScreen.width = ctx.app.view.width - ctx.app.view.width / 4 gameOverScreen.height = ctx.app.view.height - ctx.app.view.height / 4 gameOverScreen.x = ctx.app.view.width / 2 - gameOverScreen.width / 2 gameOverScreen.y = ctx.app.view.height / 2 - gameOverScreen.height / 2 let xPos = 0 let yPos = 32 gameOverBackground.width = gameOverScreen.width gameOverBackground.height = gameOverScreen.height gameOverTitle.x = gameOverScreen.width / 2 - gameOverTitle.width / 2 gameOverTitle.y = yPos yPos += gameOverTitle.height + 32 gameOverKiller.text = 'Slain By: ' + player.killer?.def.name gameOverKiller.x = gameOverScreen.width / 2 - gameOverKiller.width / 2 gameOverKiller.y = yPos yPos += gameOverKiller.height + 32 gameOverKills.text = 'Hunted: ' + player.kills gameOverKills.x = gameOverScreen.width / 2 - gameOverKills.width / 2 gameOverKills.y = yPos yPos += gameOverKills.height + 32 gameOverDays.text = 'You survived ' + daysSurvived + ' days.' gameOverDays.x = gameOverScreen.width / 2 - gameOverDays.width /2 gameOverDays.y = yPos yPos += gameOverDays.height + 32 returnToMain.x = gameOverScreen.width / 2 - returnToMain.width / 2 returnToMain.y = yPos yPos += returnToMain.height + 32 } // let clockCoverSprite = new SpriteInstance('ui.clock.cover.default.0') let clockMoonSprite = new SpriteInstance('ui.clock.moon.default.0') let clockSunSprite = new SpriteInstance('ui.clock.sun.default.0') let clockContainer = new PIXI.Container() { clockMoonSprite.container.position.set( 32, 0, ) clockMoonSprite.container.pivot.set( 24, 24, ) clockSunSprite.container.position.set( 32, 0, ) clockSunSprite.container.pivot.set( 24, 24, ) clockContainer.scale.set(2, 2) clockContainer.addChild(clockMoonSprite.container) clockContainer.addChild(clockSunSprite.container) clockContainer.addChild(clockCoverSprite.container) } let world: planck.World = planck.World({ gravity: planck.Vec2(0, 0), }) let worldBody: planck.Body = world.createBody({ type: 'static', linearDamping: 1, }) worldBody.setPosition(planck.Vec2(0, 0)) let worldFixture = worldBody.createFixture({ shape: planck.Box(1, 1), friction: 10, density: 100, }) world.on('begin-contact', (contact: planck.Contact) => { let a = contact.getFixtureA().getUserData() let b = contact.getFixtureB().getUserData() if (a instanceof Zone) { if (b instanceof Entity) { b.addZoneContact(a) } } else if (a instanceof Entity) { if (b instanceof Entity) { a.addEntityContact(b) b.addEntityContact(a) } } if (a instanceof Sensor && a.entity instanceof AnimalEntity) { if (b instanceof AnimalEntity) { a.entity.sense(a, b) } } if (b instanceof Sensor && b.entity instanceof AnimalEntity) { if (a instanceof AnimalEntity) { b.entity.sense(b, a) } } }) world.on('end-contact', (contact: planck.Contact) => { let a = contact.getFixtureA().getUserData() let b = contact.getFixtureB().getUserData() if (a instanceof Zone) { if (b instanceof Entity) { b.removeZoneContact(a) } } else if (a instanceof Entity) { if (b instanceof Entity) { a.removeEntityContact(b) b.removeEntityContact(a) } } if (a instanceof Sensor && a.entity instanceof AnimalEntity) { if (b instanceof AnimalEntity) { a.entity.lost(a, b) } } if (b instanceof Sensor && b.entity instanceof AnimalEntity) { if (a instanceof AnimalEntity) { b.entity.lost(b, a) } } }) let realRootContainer = new PIXI.Container() let rootContainer = new PIXI.Container() let playLayer: Layer let layers: Layer[] = [] let entities: Entity[] = [] let zones: Zone[] = [] let spawnZones: Record = {} let enter = () => { realRootContainer.addChild(rootContainer) lastTime = performance.now() hookKeyboard() // Load the world segment. let w = segments[selectedSegment] if (!w) return ctx.pop() // Add our world border. let borderShape = planck.Chain([planck.Vec2(0, 0), planck.Vec2(w.width, 0), planck.Vec2(w.width, w.height), planck.Vec2(0, w.height), planck.Vec2(0, 0)], true) let borderFixture = worldBody.createFixture({ shape: borderShape, }) rootContainer.width = w.width rootContainer.height = w.height rootContainer.scale.set(2, 2) for (let l of w.layers) { let layer: Layer = { title: l.title, container: new PIXI.Container(), decorations: [], colorMatrix: new PIXI.filters.ColorMatrixFilter() } layer.container.filters = [layer.colorMatrix] layer.container.width = w.width layer.container.height = w.height if (l.title === 'objects') { playLayer = layer layer.container.sortableChildren = true // Quick, make the gibs layer! let gibLayer: Layer = { title: 'gibs', container: new PIXI.Container(), decorations: [], colorMatrix: new PIXI.filters.ColorMatrixFilter() } gibLayer.container.filters = [gibLayer.colorMatrix] gibLayer.container.width = w.width gibLayer.container.height = w.height rootContainer.addChild(gibLayer.container) layers.push(gibLayer) } layers.push(layer) for (let d of l.decorations) { let di = new DecorationInstance(d.decor, d.decoration) di.elapsed = d.timeOffset di.container.x = d.x di.container.y = d.y if (d.rotation !== 0) { di.container.angle = d.rotation } if (d.flip) { di.container.pivot.y = 1 di.container.scale.y *= -1 if (d.rotation === 0) { di.container.position.y-- } } if (d.mirror) { di.container.pivot.x = 1 di.container.scale.x *= -1 di.container.position.x-- if (d.rotation !== 0) { di.container.angle = -d.rotation di.container.position.x++ di.container.position.y-- } } if (l.title === 'objects') { di.container.zIndex = di.container.position.y + di.container.height/4 } layer.container.addChild(di.container) layer.decorations.push(di) } rootContainer.addChild(layer.container) } for (let z of w.zones) { addZone(new Zone(z)) } let playerSpawn = getSpawnZone('spawn') if (!playerSpawn) { // return to menu with an error? } else { let bounds = playerSpawn.bounds let x = bounds[0] + Math.floor(Math.random() * bounds[2]) let y = bounds[1] + Math.floor(Math.random() * bounds[3]) // Add player entity player = new AnimalEntity(animals[selectedAnimal]) player.isPlayer = true addEntity(player, 'objects', x, y) player.turn() } progress() // Add clock ctx.app.stage.addChild(realRootContainer) ctx.app.stage.addChild(clockContainer) } let leave = () => { unhookKeyboard() for (let entity of entities) { removeEntity(entity) } for (let zone of zones) { removeZone(zone) } stopSong() ctx.app.stage.removeChild(realRootContainer) ctx.app.stage.removeChild(clockContainer) } let elapsed: number = 0 let update = (delta: number) => { let time = performance.now() let realDelta = time - lastTime lastTime = time checkGamepads() elapsed += delta modeTimer += realDelta if (player.dead) { if (!gameOverPending && !gameOverShown) { gameOverPending = true setTimeout(() => { gameOverPending = false gameOverShown = true realRootContainer.addChild(gameOverScreen) updateGameOver() }, 3000) } else if (gameOverShown) { updateGameOver() } } else { // If it's just the player at night, accelerate time. if (entities.filter(v=>v instanceof AnimalEntity).length === 1) { if (isNight) { modeTimer += realDelta * 4 } else { modeTimer += realDelta } } // Update clock { //clockContainer.width = ctx.app.renderer.width let w = ctx.app.renderer.width let h = ctx.app.renderer.height clockContainer.x = w / 2 - clockCoverSprite.container.width clockContainer.y = h - clockCoverSprite.container.height * 2 - 8 if (isNight) { clockMoonSprite.container.angle = 180 + modeTimer / nightTime * 180 clockSunSprite.container.angle = modeTimer / nightTime * 180 } else { clockMoonSprite.container.angle = modeTimer / dayTime * 180 clockSunSprite.container.angle = 180 + modeTimer / dayTime * 180 } clockMoonSprite.container.angle -= 230 clockSunSprite.container.angle -= 230 } // If we're getting within 3 seconds of night, play audio. if (!isNight && !nightIndicated && modeTimer >= nightTime - 5000) { playSound('action/warning', 1) nightIndicated = true } if (isNight && modeTimer >= nightTime) { modeTimer = 0 nightfall(false) } else if (!isNight && modeTimer >= dayTime) { modeTimer = 0 nightfall(true) } } // Run world sim. while (elapsed >= 1/6) { world.step(1/6) elapsed -= 1/6 } // Update/render. /*for (let layer of layers) { for (let decoration of layer.decorations) { decoration.update(realDelta) } }*/ for (let entity of entities) { if (isAnimalEntity(entity)) { if (entity.isPlayer) { entity.act(desiredActions) // FIXME: This isn't the right place for this. rootContainer.position.set(Math.round(ctx.app.renderer.width/2), Math.round(ctx.app.renderer.height/2)) rootContainer.pivot.set(entity.x, entity.y) //rootContainer.pivot.set(Math.max(w.width/6, Math.min(entity.x, w.width/3)), Math.max(w.height/6, Math.min(entity.y, w.height/3))) } else { entity.think(realDelta) } } else { //console.log('hmm, we do be tickin', entity, entity.x, entity.y) } entity.update(realDelta, worldContext) // I guess... entity.sprite.container.x = entity.x - entity.sprite.container.pivot.x entity.sprite.container.y = entity.y entity.sprite.container.zIndex = entity.sprite.container.y - entity.sprite.container.pivot.y //entity.y + entity.sprite.originY } // I guess this is okay. for (let entity of entities) { if (entity.shouldRemove) { if (entity instanceof AnimalEntity) { removeEntity(entity) } } } //entities = entities.filter(v=>v.shouldRemove) } let addEntity = (entity: Entity, layerTitle: string, x: number, y: number) => { if (entities.find(v=>v===entity)) return entities.push(entity) let layer = layers.find(v=>v.title===layerTitle) if (!layer) layer = playLayer layer.container.addChild(entity.sprite.container) // I guess this is a fair enough place to create physics and add it to the entity. let spriteShape = entity.sprite.getBodyShape() if (spriteShape) { let shape: planck.Shape|undefined if (spriteShape instanceof ShapeCircle) { shape = planck.Circle(planck.Vec2(spriteShape.x, spriteShape.y), spriteShape.radius) } else if (spriteShape instanceof ShapePoints) { shape = planck.Polygon(spriteShape.points.map(v=>planck.Vec2(v[0], v[1]))) } if (shape !== undefined) { entity.shape = shape let body = world.createDynamicBody({ position: planck.Vec2(entity.x, entity.y), fixedRotation: true, }) let fixture = body.createFixture({ shape, density: 1, friction: 0.9, restitution: 0.05, }) fixture.setUserData(entity) entity.body = body body.setUserData(entity) // Sensors let defaultShort = 50 let defaultLong = 100 if (entity instanceof AnimalEntity) { defaultLong = entity.def.animal.scent defaultShort = entity.def.animal.sight } if (entity instanceof AnimalEntity && !entity.isPlayer) { // Create a short sensor (vision). let senseFixture = body.createFixture({ shape: planck.Circle(planck.Vec2(entity.x, entity.y), defaultShort), isSensor: true, filterGroupIndex: -8 }) senseFixture.setUserData(new Sensor(entity, senseFixture, 'short')) // Create a long sensor (scent). let longSenseFixture = body.createFixture({ shape: planck.Circle(planck.Vec2(entity.x, entity.y), defaultLong), isSensor: true, filterGroupIndex: -8 }) longSenseFixture.setUserData(new Sensor(entity, longSenseFixture, 'long')) } } } entity.x = x entity.y = y } let removeEntity = (entity: Entity) => { entities = entities.filter(v=>v!==entity) entity.sprite.container.parent.removeChild(entity.sprite.container) if (entity.body) { world.destroyBody(entity.body) entity.body = undefined } } // Zonage let addZone = (zone: Zone) => { if (zones.find(v=>v===zone)) return // We're using triggers for spawning due to laziness. if (zone.type === 'trigger') { if (!spawnZones[zone.event]) { spawnZones[zone.event] = [] } spawnZones[zone.event].push(zone) } else { let shape = planck.Polygon(zone.points.map(v=>planck.Vec2(v[0],v[1]))) zone.fixture = worldBody.createFixture({ shape: shape, }) if (zone.type === 'fluid') { zone.fixture.setSensor(true) } zone.fixture.setUserData(zone) } zones.push(zone) } let removeZone = (zone: Zone) => { zones = zones.filter(v=>v!==zone) if (zone.fixture) { worldBody.destroyFixture(zone.fixture) } } let getSpawnZone = (name: string): Zone|undefined => { if (!spawnZones[name] || !spawnZones[name].length) { return undefined } return spawnZones[name][Math.floor(Math.random()*spawnZones[name].length)] } let worldContext: WorldContext = { addEntity, removeEntity, queryAABB: (aabb: planck.AABB, callback: (fixture: planck.Fixture) => boolean) => { world.queryAABB(aabb, callback) }, rayCast: (point1: planck.Vec2, point2: planck.Vec2, callback: (fixture: planck.Fixture, point: planck.Vec2, normal: planck.Vec2, fraction: number) => boolean) => { world.rayCast(point1, point2, callback) }, queryEntities: (min: [number, number], max: [number, number]): Entity[] => { let ret: Entity[] = [] for (let entity of entities) { if (!entity.shape) { continue } let p = entity.position let r = entity.shape.getRadius() /*let a = { x: min[0], w: max[0] - min[0], y: min[1], h: max[1] - min[1], } let b = { x: p[0], w: r, y: p[1], h: r, }*/ if ( (Math.abs(min[0] - (p[0]-r)) * 2 < ((max[0]-min[0]) + r)) && (Math.abs(min[1] - (p[1]-r)) * 2 < ((max[1]-min[1]) + r))) { ret.push(entity) } } return ret }, } let desiredActions: Action[] = [] let keyup = (e: KeyboardEvent) => { if (e.key === 'ArrowLeft' || e.key === 'h') { desiredActions = adjustAction(desiredActions, 'west', 0) } else if (e.key === 'ArrowRight' || e.key === 'l') { desiredActions = adjustAction(desiredActions, 'east', 0) } else if (e.key === 'ArrowUp' || e.key === 'k') { desiredActions = adjustAction(desiredActions, 'north', 0) } else if (e.key === 'ArrowDown' || e.key === 'j') { desiredActions = adjustAction(desiredActions, 'south', 0) } else if (e.key === 'z' || e.key === 'Control') { desiredActions = adjustAction(desiredActions, 'attack', 0) } else if (e.key === 'Escape') { ctx.pop() } } let keydown = (e: KeyboardEvent) => { if (e.repeat) return if (e.key === 'ArrowLeft' || e.key === 'h') { desiredActions = adjustAction(desiredActions, 'west', 1) } else if (e.key === 'ArrowRight' || e.key === 'l') { desiredActions = adjustAction(desiredActions, 'east', 1) } else if (e.key === 'ArrowUp' || e.key === 'k') { desiredActions = adjustAction(desiredActions, 'north', 1) } else if (e.key === 'ArrowDown' || e.key === 'j') { desiredActions = adjustAction(desiredActions, 'south', 1) } else if (e.key === 'z' || e.key === 'Control') { desiredActions = adjustAction(desiredActions, 'attack', 2) } } let hookKeyboard = () => { window.addEventListener('keyup', keyup) window.addEventListener('keydown', keydown) } let unhookKeyboard = () => { window.removeEventListener('keyup', keyup) window.removeEventListener('keydown', keydown) } let gamepads: Record = {} window.addEventListener('gamepadconnected', (ev: GamepadEvent) => { gamepads[ev.gamepad.id] = ev.gamepad }) window.removeEventListener('gamepaddisconnected', (ev: GamepadEvent) => { delete gamepads[ev.gamepad.id] }) function checkGamepads() { //for (let gp of Object.values(gamepads)) { for (let gp of navigator.getGamepads()) { if (!gp || !gp.connected) break if (gp.axes[0] < 0) { desiredActions = adjustAction(desiredActions, 'west', Math.abs(gp.axes[0])) } else if (gp.axes[0] > 0) { desiredActions = adjustAction(desiredActions, 'east', gp.axes[0]) } else { desiredActions = adjustAction(desiredActions, 'east', 0) desiredActions = adjustAction(desiredActions, 'west', 0) } if (gp.axes[1] < 0) { desiredActions = adjustAction(desiredActions, 'north', Math.abs(gp.axes[1])) } else if (gp.axes[1] > 0) { desiredActions = adjustAction(desiredActions, 'south', gp.axes[1]) } else { desiredActions = adjustAction(desiredActions, 'north', 0) desiredActions = adjustAction(desiredActions, 'south', 0) } let attackHeld = false for (let i = 0; i < 4; i++) { let btn = gp.buttons[i] if (btn.pressed) { attackHeld = true } } if (attackHeld) { desiredActions = adjustAction(desiredActions, 'attack', 2) } else if (desiredActions.find(v=>v.type==='attack')) { desiredActions = adjustAction(desiredActions, 'attack', 0) } } } let nightfall = (b: boolean) => { if (b) { playSong('GGJ-ScaryMusic') for (let l of layers) { //l.colorMatrix.brightness(0.25, false) l.colorMatrix.night(0.2, true) l.colorMatrix.saturate(-0.75, true) //l.colorMatrix.hue(160, true) } for (let e of entities) { if (e instanceof AnimalEntity) { e.turn() } } } else { nightIndicated = false playSong('GGJ-HappyMusic') for (let l of layers) { l.colorMatrix.reset() } for (let e of entities) { if (e instanceof AnimalEntity) { e.turn() } } progress() } isNight = b } let progress = () => { daysSurvived++ for (let animal of ['turkey','nutria','deer','salamander','rabbit']) { let count = 2 + Math.floor(Math.random() * daysSurvived*2) for (let i = 0; i < count; i++) { let spawn = getSpawnZone(animal) if (!spawn) break let bounds = spawn.bounds let x = bounds[0] + Math.floor(Math.random() * bounds[2]) let y = bounds[1] + Math.floor(Math.random() * bounds[3]) let entity = new AnimalEntity(animals[animal]) addEntity(entity, 'objects', x, y) } } nightTime = (30+daysSurvived) * 1000 dayTime = (30-daysSurvived) * 1000 } nightfall(false) return { enter, leave, update, } }