CyberEgg 2077
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

617 lines
19 KiB

// lib/game-background.js
// Copyright (C) 2022 Rob Colbert @[email protected]
// License: Apache-2.0
'use strict';
import { NiceColor, NiceEasing, NiceSprite, NiceTween, NiceVector2d } from 'dtp-nice-game';
import GameButterfly from './game-buttterfly.js';
import GameObject from './game-object.js';
const SKY_HEIGHT = 200;
const WATER_HEIGHT = 150;
const WATER_END = SKY_HEIGHT + WATER_HEIGHT;
class OceanWave {
constructor (game) {
this.game = game;
function randomY ( ) {
return SKY_HEIGHT + (Math.random() * 20);
}
this.points = [ ];
this.startY = randomY();
const wavePointSpacing = game.playfield.width / 16;
for (let x = wavePointSpacing; x <= game.playfield.width; x += wavePointSpacing) {
this.points.push({
control: new NiceVector2d(x - (wavePointSpacing / 0.8), randomY()),
target: new NiceVector2d(x, randomY())
});
}
}
update (elapsed) {
const BASE_RATE = 10.0;
const VARIANCE = 2.0;
const timeScale = elapsed / 1000.0;
this.startY += (BASE_RATE + (VARIANCE * Math.random())) * timeScale;
let maxY = 0;
this.points.forEach((point) => {
point.control.y += (BASE_RATE + (VARIANCE * Math.random())) * timeScale;
point.target.y += (BASE_RATE + (VARIANCE * Math.random())) * timeScale;
if (point.target.y > maxY) {
maxY = point.target.y;
}
});
return maxY < WATER_END;
}
render (ctx) {
let maxY = 0;
let minY = this.game.playfield.height;
ctx.beginPath();
ctx.moveTo(0, this.startY);
if (this.startY > maxY) {
maxY = this.startY;
}
if (this.startY < minY) {
minY = this.startY;
}
this.points.forEach((point) => {
ctx.quadraticCurveTo(point.control.x, point.control.y, point.target.x, point.target.y);
if (point.target.y > maxY) {
maxY = point.target.y;
}
if (point.target.y < minY) {
minY = point.target.y;
}
});
const oldOpacity = ctx.globalAlpha;
ctx.globalAlpha = 1.0;
if (maxY > (WATER_END - 50)) {
let term = WATER_END - maxY;
if (term < 0) { term = 0; }
ctx.globalAlpha = term / 50.0;
}
let alpha = (maxY - SKY_HEIGHT) / WATER_HEIGHT;
const color = new NiceColor(255,255,255, alpha);
ctx.strokeStyle = color.toCssString();
ctx.lineWidth = 2.0;
ctx.stroke();
minY -= 80;
if (minY < SKY_HEIGHT) {
minY = SKY_HEIGHT;
}
ctx.lineTo(this.game.playfield.width, minY);
ctx.lineTo(0, minY);
ctx.closePath();
color.a /= 2.0;
if (color.a > 0.2) {
color.a = 0.4;
}
ctx.fillStyle = ctx.createLinearGradient(0, minY, 0, maxY);
ctx.fillStyle.addColorStop(0.0, '#ffffff00');
ctx.fillStyle.addColorStop(1.0, color.toCssString());
ctx.fill();
ctx.globalAlpha = oldOpacity;
}
}
/**
* GameBackground is responsible for rendering the background/environment, and
* it uses a variety of techniques to do that. Gradients are used to render the
* sky, beach, and ocean. Images are used to render the trees, plants, and
* foreground base.
*
* GameBackground has modes, and can reconfigure itself and animate between
* them on command using reusable NiceTween instances. It currently supports
* GameBackground.MODE_STORY and GameBackground.MODE_PLAY.
*/
export default class GameBackground {
static get MODE_STORY ( ) { return 'story'; }
static get MODE_PLAY ( ) { return 'play'; }
constructor (game) {
this.game = game;
this.sprites = { };
this.tweens = { };
this.sun = {
position: new NiceVector2d(480, 140),
};
this.waves = [ ];
this.boats = [ ];
this.clouds = [ ];
}
async load ( ) {
await this.loadImages();
this.buildSprites();
this.buildTweens();
this.setEnvironmentMode('day');
}
async loadImages ( ) {
const jobs = [ ];
jobs.push(this.game.loadImage('bg:foreground-base', '/dist/assets/img/beach/foreground-base.png', 960, 114));
jobs.push(this.game.loadImage('bg:sailboat', '/dist/assets/img/beach/sailboat.png', 116, 120));
jobs.push(this.game.loadImage('bg:rocky-island', '/dist/assets/img/beach/rocky-island.png', 385, 80));
jobs.push(this.game.loadImage('bg:tree-left', '/dist/assets/img/beach/tree-left.png', 361, 540));
jobs.push(this.game.loadImage('bg:tree-right', '/dist/assets/img/beach/tree-right.png', 351, 540));
jobs.push(this.game.loadImage('bg:plant-left', '/dist/assets/img/beach/plant-left.png', 148, 303));
jobs.push(this.game.loadImage('bg:plant-right', '/dist/assets/img/beach/plant-right.png', 170, 211));
jobs.push(this.game.loadImage('bg:cloud-001', '/dist/assets/img/beach/cloud-001.png', 132, 40));
jobs.push(this.game.loadImage('bg:cloud-002', '/dist/assets/img/beach/cloud-002.png', 115, 40));
jobs.push(this.game.loadImage('bg:cloud-003', '/dist/assets/img/beach/cloud-003.png', 105, 60));
jobs.push(this.game.loadImage('bg:cloud-004', '/dist/assets/img/beach/cloud-004.png', 118, 32));
jobs.push(this.game.loadImage('bg:cloud-005', '/dist/assets/img/beach/cloud-005.png', 141, 50));
jobs.push(this.game.loadImage('bg:cloud-006', '/dist/assets/img/beach/cloud-006.png', 119, 50));
jobs.push(this.game.loadImage('bg:butterfly-001', '/dist/assets/img/beach/butterfly-001.png', 27, 24));
jobs.push(this.game.loadImage('bg:butterfly-002', '/dist/assets/img/beach/butterfly-002.png', 27, 24));
jobs.push(this.game.loadImage('bg:butterfly-003', '/dist/assets/img/beach/butterfly-003.png', 27, 24));
jobs.push(this.game.loadImage('bg:butterfly-004', '/dist/assets/img/beach/butterfly-004.png', 27, 24));
await Promise.all(jobs);
}
buildSprites ( ) {
this.sprites.foreground = new NiceSprite(this.game, new NiceVector2d(this.game.playfield.width / 2, this.game.playfield.height));
this.sprites.foreground.image = this.game.images['bg:foreground-base'];
this.sprites.foreground.registration = new NiceVector2d(480, this.sprites.foreground.image.height);
this.sprites.treeLeft = new NiceSprite(this.game, new NiceVector2d(0, this.game.playfield.height));
this.sprites.treeLeft.image = this.game.images['bg:tree-left'];
this.sprites.treeLeft.registration = new NiceVector2d(0, this.sprites.treeLeft.image.height);
this.sprites.treeRight = new NiceSprite(this.game, new NiceVector2d(this.game.playfield.width, 0));
this.sprites.treeRight.image = this.game.images['bg:tree-right'];
this.sprites.treeRight.registration = new NiceVector2d(this.sprites.treeRight.image.width, 0);
this.sprites.plantLeft = new NiceSprite(this.game, new NiceVector2d(0, this.game.playfield.height));
this.sprites.plantLeft.image = this.game.images['bg:plant-left'];
this.sprites.plantLeft.registration = new NiceVector2d(0, this.sprites.plantLeft.image.height);
this.sprites.plantRight = new NiceSprite(this.game, new NiceVector2d(this.game.playfield.width, this.game.playfield.height));
this.sprites.plantRight.image = this.game.images['bg:plant-right'];
this.sprites.plantRight.registration = new NiceVector2d(
this.sprites.plantRight.image.width,
this.sprites.plantRight.image.height,
);
}
buildTweens ( ) {
const FOREGROUND_DURATION = 1000;
const TREE_DURATION = 1250;
const PLANT_DURATION = 1500;
/*
* FOREGROUND BASE
*/
this.tweens.foregroundIn = new NiceTween(
this.game,
this.sprites.foreground.position,
{ y: this.game.playfield.height + this.sprites.foreground.image.height},
{ y: this.game.playfield.height },
NiceEasing.Quadratic.Out,
);
this.tweens.foregroundIn.duration(FOREGROUND_DURATION);
this.tweens.foregroundOut = new NiceTween(
this.game,
this.sprites.foreground.position,
{ y: this.game.playfield.height },
{ y: this.game.playfield.height + this.sprites.foreground.image.height},
NiceEasing.Quadratic.In,
);
this.tweens.foregroundOut.duration(FOREGROUND_DURATION);
/*
* TREE LEFT
*/
this.tweens.treeLeftIn = new NiceTween(
this.game,
this.sprites.treeLeft.position,
{ x: -this.sprites.treeLeft.image.width },
{ x: 0 },
NiceEasing.Elastic.Out,
);
this.tweens.treeLeftIn.duration(TREE_DURATION);
this.tweens.treeLeftOut = new NiceTween(
this.game,
this.sprites.treeLeft.position,
{ x: 0 },
{ x: -this.sprites.treeLeft.image.width },
NiceEasing.Elastic.In,
);
this.tweens.treeLeftOut.duration(TREE_DURATION);
/*
* TREE RIGHT
*/
this.tweens.treeRightIn = new NiceTween(
this.game,
this.sprites.treeRight.position,
{ x: this.game.playfield.width + this.sprites.treeRight.image.width },
{ x: this.game.playfield.width },
NiceEasing.Elastic.Out,
);
this.tweens.treeRightIn.duration(TREE_DURATION);
this.tweens.treeRightOut = new NiceTween(
this.game,
this.sprites.treeRight.position,
{ x: this.game.playfield.width },
{ x: this.game.playfield.width + this.sprites.treeRight.image.width },
NiceEasing.Elastic.In,
);
this.tweens.treeRightOut.duration(TREE_DURATION);
/*
* PLANT LEFT
*/
this.tweens.plantLeftIn = new NiceTween(
this.game,
this.sprites.plantLeft.position,
{ x: -this.sprites.plantLeft.image.width },
{ x: 0 },
NiceEasing.Elastic.Out,
);
this.tweens.plantLeftIn.duration(PLANT_DURATION);
this.tweens.plantLeftOut = new NiceTween(
this.game,
this.sprites.plantLeft.position,
{ x: 0 },
{ x: -this.sprites.plantLeft.image.width },
NiceEasing.Elastic.In,
);
this.tweens.plantLeftOut.duration(PLANT_DURATION);
/*
* PLANT RIGHT
*/
this.tweens.plantRightIn = new NiceTween(
this.game,
this.sprites.plantRight.position,
{ x: this.game.playfield.width + this.sprites.plantRight.width },
{ x: this.game.playfield.width },
NiceEasing.Elastic.Out,
);
this.tweens.plantRightIn.duration(PLANT_DURATION);
this.tweens.plantRightOut = new NiceTween(
this.game,
this.sprites.plantRight.position,
{ x: this.game.playfield.width },
{ x: this.game.playfield.width + this.sprites.plantRight.width },
NiceEasing.Elastic.In,
);
this.tweens.plantRightOut.duration(PLANT_DURATION);
/*
* Environment: Day To Night transition
*/
this.tweens.environmentDayToNight = new NiceTween(
this.game,
this.applyEnvironmentTween.bind(this),
{
sun_y: 20,
sky_r0: 189, sky_g0: 213, sky_b0: 179, sky_a0: 1.0,
sky_r1: 240, sky_g1: 242, sky_b1: 220, sky_a1: 1.0,
beach_r0: 224, beach_g0: 164, beach_b0: 127, beach_a0: 1.0,
beach_r1: 246, beach_g1: 234, beach_b1: 182, beach_a1: 1.0,
},
{
sun_y: 140,
sky_r0: 1, sky_g0: 13, sky_b0: 66, sky_a0: 1.0,
sky_r1: 49, sky_g1: 56, sky_b1: 122, sky_a1: 1.0,
beach_r0: 47, beach_g0: 81, beach_b0: 152, beach_a0: 1.0,
beach_r1: 36, beach_g1: 51, beach_b1: 92, beach_a1: 1.0,
},
NiceEasing.Circular.InOut,
);
/*
* Environment: Night To Day transition
*/
this.tweens.environmentNightToDay = new NiceTween(
this.game,
this.applyEnvironmentTween.bind(this),
{
sun_y: 140,
sky_r0: 1, sky_g0: 13, sky_b0: 66, sky_a0: 1.0,
sky_r1: 49, sky_g1: 56, sky_b1: 122, sky_a1: 1.0,
beach_r0: 47, beach_g0: 81, beach_b0: 152, beach_a0: 1.0,
beach_r1: 36, beach_g1: 51, beach_b1: 92, beach_a1: 1.0,
},
{
sun_y: 20,
sky_r0: 189, sky_g0: 213, sky_b0: 179, sky_a0: 1.0,
sky_r1: 240, sky_g1: 242, sky_b1: 220, sky_a1: 1.0,
beach_r0: 224, beach_g0: 164, beach_b0: 127, beach_a0: 1.0,
beach_r1: 246, beach_g1: 234, beach_b1: 182, beach_a1: 1.0,
},
NiceEasing.Cubic.InOut,
);
}
applyEnvironmentTween (current) {
this.sun.position.y = current.sun_y;
this.skyGradient = this.game.gameDisplayCtx.createLinearGradient(0, 0, 0, SKY_HEIGHT);
this.skyGradient.addColorStop(0.0, `rgba(${current.sky_r0},${current.sky_g0},${current.sky_b0}, ${current.sky_a0})`);
this.skyGradient.addColorStop(1.0, `rgba(${current.sky_r1},${current.sky_g1},${current.sky_b1}, ${current.sky_a1})`);
this.beachGradient = this.game.gameDisplayCtx.createLinearGradient(0, SKY_HEIGHT, 0, this.game.playfield.height);
this.beachGradient.addColorStop(0.0, `rgba(${current.beach_r0},${current.beach_g0},${current.beach_b0}, ${current.beach_a0})`);
this.beachGradient.addColorStop(1.0, `rgba(${current.beach_r1},${current.beach_g1},${current.beach_b1}, ${current.beach_a1})`);
}
setMode (mode) {
this.mode = mode;
delete this.butterfly;
this.tweens.foregroundIn.stop();
this.tweens.treeLeftIn.stop();
this.tweens.treeRightIn.stop();
this.tweens.plantLeftIn.stop();
this.tweens.plantRightIn.stop();
switch (this.mode) {
case 'story':
this.tweens.foregroundIn.delay(2000).run();
this.tweens.treeLeftIn.delay(2000).run();
this.tweens.treeRightIn.delay(2000).run();
this.tweens.plantLeftIn.delay(2000).run();
this.tweens.plantRightIn.delay(2000).run();
this.butterfly = new GameButterfly(
this.game,
this.game.images['bg:butterfly-002'],
new NiceVector2d(320, 280),
);
this.butterfly.navigator.setMoveSpeed(40.0);
this.butterfly.navigator.setTargetPosition(new NiceVector2d(503, 386));
break;
case 'play':
this.tweens.foregroundOut.run();
this.tweens.treeLeftOut.run();
this.tweens.treeRightOut.run();
this.tweens.plantLeftOut.run();
this.tweens.plantRightOut.run();
break;
}
}
update (elapsed, now) {
if ((!this.nextWaveSpawnTime) || (now >= this.nextWaveSpawnTime)) {
this.nextWaveSpawnTime = now + 12000;
this.waves.push(new OceanWave(this.game));
}
this.waves = this.waves.filter((wave) => wave.update(elapsed, now));
if ((!this.nextBoatSpawnTime) || (now >= this.nextBoatSpawnTime)) {
this.nextBoatSpawnTime = now + 20000 + (Math.random() * 10000);
this.spawnBoat();
}
if ((!this.nextCloudSpawnTime) || (now >= this.nextCloudSpawnTime)) {
this.nextCloudSpawnTime = now + 30000 + (Math.random() + 50000);
this.spawnCloud();
}
for (const tweenKey in this.tweens) {
const tween = this.tweens[tweenKey];
if (tween.state !== 'running') {
continue;
}
tween.update(elapsed, now);
}
if (this.butterfly) {
this.butterfly.update(elapsed, now);
}
}
spawnBoat ( ) {
function randomBoatY ( ) {
return SKY_HEIGHT + 20 + (Math.random() * 40);
}
const { playfield } = this.game;
if (Math.random() > 0.5) {
const boat = new NiceSprite(this.game, { x: 0, y: randomBoatY() });
boat.image = this.game.images['bg:sailboat'];
boat.registration = new NiceVector2d(boat.image.width / 2, boat.image.height - 10);
this.boats.push(boat);
this.game
.createTween(
boat.position,
{ x: -boat.image.width },
{ x: playfield.width + boat.image.width },
NiceEasing.Linear.None,
)
.duration(30000 + (Math.random() * 10000))
.run()
;
} else {
const boat = new NiceSprite(this.game, { x: playfield.width, y: randomBoatY() });
boat.image = this.game.images['bg:sailboat'];
boat.registration = new NiceVector2d(boat.image.width / 2, boat.image.height - 10);
boat.mirror = true;
this.boats.push(boat);
this.game
.createTween(
boat.position,
{ x: this.game.playfield.width + boat.image.width },
{ x: -boat.image.width },
NiceEasing.Linear.None,
)
.duration(30000 + (Math.random() * 10000))
.run()
;
}
this.boats = this.boats.sort((a, b) => a.position.y - b.position.y);
}
spawnCloud ( ) {
function randomCloudY ( ) {
return 20 + (Math.random() * (SKY_HEIGHT - 50));
}
const image = this.game.images['bg:cloud-001'];
if (Math.random() > 0.5) {
const cloud = new NiceSprite(this.game, new NiceVector2d(-image.width / 2, randomCloudY()));
this.clouds.push(cloud);
cloud.image = image;
cloud.registration = new NiceVector2d(image.width / 2, image.height / 2);
this.game
.createTween(
cloud.position,
{ x: -image.width / 2 },
{ x: this.game.playfield.width + image.width / 2 },
NiceEasing.Linear.None,
)
.duration(60000 + (Math.random() * 20000))
.run()
;
} else {
const cloud = new NiceSprite(this.game, new NiceVector2d(-image.width / 2, randomCloudY()));
this.clouds.push(cloud);
cloud.image = image;
cloud.registration = new NiceVector2d(image.width / 2, image.height / 2);
this.game
.createTween(
cloud.position,
{ x: this.game.playfield.width + image.width / 2 },
{ x: -image.width / 2 },
NiceEasing.Linear.None,
)
.duration(60000 + (Math.random() * 20000))
.run()
;
}
}
render (ctx) {
/*
* Gradient Fills
*/
ctx.fillStyle = this.skyGradient;
ctx.fillRect(0, 0, this.game.playfield.width, SKY_HEIGHT);
ctx.fillStyle = this.beachGradient;
ctx.fillRect(0, SKY_HEIGHT, this.game.playfield.width, this.game.playfield.height - SKY_HEIGHT);
this.drawSun(ctx);
this.waves.forEach((wave) => wave.render(ctx));
ctx.fillStyle = this.oceanGradient;
ctx.fillRect(0, SKY_HEIGHT, this.game.playfield.width, WATER_HEIGHT);
this.clouds.forEach((cloud) => cloud.render(ctx));
this.game.images['bg:rocky-island'].draw(ctx, 0, SKY_HEIGHT - 70);
this.boats.forEach((boat) => boat.render(ctx));
/*
* Image/sprite overlays
*/
for (const spriteKey in this.sprites) {
const sprite = this.sprites[spriteKey];
sprite.render(ctx);
}
if (this.butterfly) {
this.butterfly.render(ctx);
}
}
setEnvironmentMode (mode) {
const ctx = this.game.gameDisplayCtx;
switch (mode) {
case 'day':
this.tweens.environmentNightToDay.duration(2000).run();
this.oceanGradient = ctx.createLinearGradient(0, SKY_HEIGHT, 0, SKY_HEIGHT + WATER_HEIGHT);
this.oceanGradient.addColorStop(0.0, '#72b5aeff');
this.oceanGradient.addColorStop(0.75, '#72b5ae40');
this.oceanGradient.addColorStop(1.0, '#edeec800');
break;
}
}
drawSun (ctx) {
ctx.beginPath();
ctx.ellipse(
this.sun.position.x,
this.sun.position.y,
32,
32,
0,
0,
Math.PI * 2,
);
this.sun.gradient = ctx.createRadialGradient(
this.sun.position.x,
this.sun.position.y,
32,
this.sun.position.x + 16,
this.sun.position.y - 16,
4
);
this.sun.gradient.addColorStop(0.0, '#e0a579');
this.sun.gradient.addColorStop(1.0, '#eabf8a');
ctx.fillStyle = this.sun.gradient;
ctx.lineWidth = 3.0;
ctx.strokeStyle = '#593117';
ctx.fill();
ctx.stroke();
}
}