diff --git a/package-lock.json b/package-lock.json index 8fc3d40..43faa12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,8 @@ "name": "svelte-app", "version": "1.0.0", "dependencies": { - "p5": "^1.6.0", - "p5-svelte": "^3.1.2", - "p5play": "^3.8.14", + "p5": "^1.11.4", + "p5play": "^3.22.0", "sirv-cli": "^2.0.0", "svelte-spa-router": "^3.3.0" }, @@ -194,12 +193,6 @@ "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", "dev": true }, - "node_modules/@types/p5": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@types/p5/-/p5-1.6.2.tgz", - "integrity": "sha512-iy1B7DBMc9e5j0DrWnIjSJh+Ro9UkKxPHspbx/GvIeGFTOIJF0H2lICnIN4go1OZEAQSHBinLd7t+oe8lrrLvA==", - "peer": true - }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -641,26 +634,30 @@ "dev": true }, "node_modules/p5": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/p5/-/p5-1.6.0.tgz", - "integrity": "sha512-RowF+RxfVUhJm/YKXL5TCFzTqnwAIwK6W1VGs9LAqSf3PCmLz9Igbxzlf0Ry5IMV71L42wipCdH/bDiNsqAstA==" - }, - "node_modules/p5-svelte": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/p5-svelte/-/p5-svelte-3.1.2.tgz", - "integrity": "sha512-lcfWh+cJ1/wRdIXHnjpYmDgj2h3TCy1QJVQnf/cBcFWS8CSkvyAN5F8u8H2U8qBUtZ4XaD3nd+1NoYUMHaMExQ==", - "dependencies": { - "p5": "^1.4.1" - }, - "peerDependencies": { - "@types/p5": "^1.4.2", - "p5": "^1.4.0" - } + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/p5/-/p5-1.11.4.tgz", + "integrity": "sha512-N7tM2XYSmuNX8S295RvgHoJS7kpYLYxLjVFeySkwkbxwVrGnrwY8yAwciTxlonBjP422W7WW9pihpUVP8bAVgg==", + "license": "LGPL-2.1" }, "node_modules/p5play": { - "version": "3.8.14", - "resolved": "https://registry.npmjs.org/p5play/-/p5play-3.8.14.tgz", - "integrity": "sha512-z2TjIIJ4td9KGIubsyftaJJsh9FXxI+Wl/JDXWfshwJyErYl9wjQpMHdeWxlC+zrg40PHXeYzxG6Gq939bVe4A==" + "version": "3.35.4", + "resolved": "https://registry.npmjs.org/p5play/-/p5play-3.35.4.tgz", + "integrity": "sha512-5C0QobV0a36JhFacV0rrMvgeJNFWYtIpS1EcvHYptmzGXFRt6x9/mvEegiWPqNq5LqRf30XeD3g1JJLfoHYzwQ==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/q5play" + }, + { + "type": "ko-fi", + "url": "https://ko-fi.com/q5play" + }, + { + "type": "github", + "url": "https://github.com/sponsors/quinton-ashley" + } + ], + "license": "p5play Personal License" }, "node_modules/path-parse": { "version": "1.0.7", @@ -1142,12 +1139,6 @@ "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", "dev": true }, - "@types/p5": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@types/p5/-/p5-1.6.2.tgz", - "integrity": "sha512-iy1B7DBMc9e5j0DrWnIjSJh+Ro9UkKxPHspbx/GvIeGFTOIJF0H2lICnIN4go1OZEAQSHBinLd7t+oe8lrrLvA==", - "peer": true - }, "@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -1478,22 +1469,14 @@ "dev": true }, "p5": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/p5/-/p5-1.6.0.tgz", - "integrity": "sha512-RowF+RxfVUhJm/YKXL5TCFzTqnwAIwK6W1VGs9LAqSf3PCmLz9Igbxzlf0Ry5IMV71L42wipCdH/bDiNsqAstA==" - }, - "p5-svelte": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/p5-svelte/-/p5-svelte-3.1.2.tgz", - "integrity": "sha512-lcfWh+cJ1/wRdIXHnjpYmDgj2h3TCy1QJVQnf/cBcFWS8CSkvyAN5F8u8H2U8qBUtZ4XaD3nd+1NoYUMHaMExQ==", - "requires": { - "p5": "^1.4.1" - } + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/p5/-/p5-1.11.4.tgz", + "integrity": "sha512-N7tM2XYSmuNX8S295RvgHoJS7kpYLYxLjVFeySkwkbxwVrGnrwY8yAwciTxlonBjP422W7WW9pihpUVP8bAVgg==" }, "p5play": { - "version": "3.8.14", - "resolved": "https://registry.npmjs.org/p5play/-/p5play-3.8.14.tgz", - "integrity": "sha512-z2TjIIJ4td9KGIubsyftaJJsh9FXxI+Wl/JDXWfshwJyErYl9wjQpMHdeWxlC+zrg40PHXeYzxG6Gq939bVe4A==" + "version": "3.35.4", + "resolved": "https://registry.npmjs.org/p5play/-/p5play-3.35.4.tgz", + "integrity": "sha512-5C0QobV0a36JhFacV0rrMvgeJNFWYtIpS1EcvHYptmzGXFRt6x9/mvEegiWPqNq5LqRf30XeD3g1JJLfoHYzwQ==" }, "path-parse": { "version": "1.0.7", diff --git a/package.json b/package.json index d6581bf..8df3fed 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ "svelte": "^3.55.0" }, "dependencies": { - "p5": "^1.6.0", - "p5-svelte": "^3.1.2", - "p5play": "^3.8.14", + "p5": "^1.11.4", + + "p5play": "^3.22.0", "sirv-cli": "^2.0.0", "svelte-spa-router": "^3.3.0" } diff --git a/public/assets/Fragment.png b/public/assets/Fragment.png new file mode 100644 index 0000000..9953864 Binary files /dev/null and b/public/assets/Fragment.png differ diff --git a/public/assets/enemy.png b/public/assets/enemy.png new file mode 100644 index 0000000..4f1cc78 Binary files /dev/null and b/public/assets/enemy.png differ diff --git a/public/assets/heart_empty.png b/public/assets/heart_empty.png new file mode 100644 index 0000000..af423ee Binary files /dev/null and b/public/assets/heart_empty.png differ diff --git a/public/assets/heart_full.png b/public/assets/heart_full.png new file mode 100644 index 0000000..0f8c7ee Binary files /dev/null and b/public/assets/heart_full.png differ diff --git a/public/assets/monster.png b/public/assets/monster.png deleted file mode 100644 index 9cb1c46..0000000 Binary files a/public/assets/monster.png and /dev/null differ diff --git a/public/assets/player.png b/public/assets/player.png new file mode 100644 index 0000000..c7f52d1 Binary files /dev/null and b/public/assets/player.png differ diff --git a/public/assets/player_jump.png b/public/assets/player_jump.png new file mode 100644 index 0000000..0167534 Binary files /dev/null and b/public/assets/player_jump.png differ diff --git a/public/assets/splat.png b/public/assets/splat.png new file mode 100644 index 0000000..0bb0ad7 Binary files /dev/null and b/public/assets/splat.png differ diff --git a/public/assets/tar.png b/public/assets/tar.png new file mode 100644 index 0000000..c1abe96 Binary files /dev/null and b/public/assets/tar.png differ diff --git a/public/assets/tile.png b/public/assets/tile.png new file mode 100644 index 0000000..3070c86 Binary files /dev/null and b/public/assets/tile.png differ diff --git a/public/backgrounds/temp-background.png b/public/backgrounds/temp-background.png new file mode 100644 index 0000000..3117dcc Binary files /dev/null and b/public/backgrounds/temp-background.png differ diff --git a/public/index.html b/public/index.html index 632be3a..c9dc30e 100644 --- a/public/index.html +++ b/public/index.html @@ -2,24 +2,22 @@ - - + + - Svelte app + ColorQuest - - - - - - - - - - + + + + + + + - - \ No newline at end of file + + + \ No newline at end of file diff --git a/src/App.svelte b/src/App.svelte index 6b045c3..d1e44be 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -7,6 +7,8 @@ import LevelSelect from './routes/LevelSelect.svelte'; import Game from './routes/Game.svelte'; import GameOver from './routes/GameOver.svelte'; + + // this is the application of the svelte-spa-router // e.g. /#/game will show the Game component // this is kind of like switching between HTML pages but we switch components instead @@ -18,8 +20,7 @@ const routes = { }; - - + \ No newline at end of file diff --git a/src/components/GameCanvas.svelte b/src/components/GameCanvas.svelte index e69de29..da0d4a1 100644 --- a/src/components/GameCanvas.svelte +++ b/src/components/GameCanvas.svelte @@ -0,0 +1,203 @@ + + +
+ + \ No newline at end of file diff --git a/src/components/HUD.svelte b/src/components/HUD.svelte index e69de29..6bf8b7d 100644 --- a/src/components/HUD.svelte +++ b/src/components/HUD.svelte @@ -0,0 +1,54 @@ + + +
+ +
+ {#each {length: 3} as _, i} + life + {/each} +
+ + +
+ {$fragmentsCollected} / {totalFragments} +
+
+ + \ No newline at end of file diff --git a/src/game/Enemy.js b/src/game/Enemy.js index e69de29..f303c44 100644 --- a/src/game/Enemy.js +++ b/src/game/Enemy.js @@ -0,0 +1,33 @@ +export class Enemy{ + constructor(p5, x, y) { + this.sprite = new p5.Sprite(x,y,36,36); + this.sprite.image = '/assets/enemy.png'; + this.sprite.collider = 'dynamic'; + this.sprite.rotationLock = true; + this.sprite.bounciness = 0; + + this.speed = 1.5; + this.direction = 1 // 1 is right, -1 is left + this.startX = x; + this.patrolDisctance = 100; // how far it can walk + } + + update(){ + // move in current direction + this.sprite.vel.x = this.speed * this.direction; + + // flip direction once it reaches patrol distance + if (this.sprite.x > this.startX + this.patrolDistance){ + this.direction = -1; + this.sprite.mirror.x = -1; // we flip the image + } + if (this.sprite.x < this.startX - this.patrolDistance){ + this.direction = 1; + this.sprite.mirror.x = 1; // flip image back + } + } + + overlapsPlayer(playerSprite){ + return playerSprite.overlaps(this.sprite); + } +} \ No newline at end of file diff --git a/src/game/Fragment.js b/src/game/Fragment.js index e69de29..e045468 100644 --- a/src/game/Fragment.js +++ b/src/game/Fragment.js @@ -0,0 +1,62 @@ +export class Fragment { + constructor(p5, x, y, hexColor){ + this.p5 = p5; + this.hexColor = hexColor; + this.collected = false; + this.baseY = y // this Y will be used for bobbing math + this.bobAngle = p5.random(0, 360); // so they are not all in sync + + // not a solid body so player can pass through + this.sprite = new p5.Sprite(x,y,20,20); + this.sprite.image = '/assets/Fragment.png'; + this.sprite.collider = 'none'; + this.sprite.rotationLock = true; + } + + // call it every frame + // callback function when picked up + update(playerSprite, onCollect){ + if (this.collected) return; + + // bob up and down based on sine wave + this.bobAngle += 2; + this.sprite.y = this.baseY + Math.sin(this.bobAngle * 0.04) * 7; + + // slow spin for funsies + this.sprite.rotation += 1; + + // check if player overlap with fragment + if (playerSprite.overlaps(this.sprite)){ + this.collect(onCollect); + } + } + + collect(onCollect) { + this.collected = true; + // save position BEFORE removing the sprite + const x = this.sprite.x; + const y = this.sprite.y; + this.sprite.remove(); + onCollect(this.hexColor, x, y); + } + // draw color glow around fragment + // before drawing the sprite + drawGlow(){ + if (this.collected) return; + + this.p5.push(); + this.p5.noStroke(); + + // glow pulsates based on sine wave + const pulse = Math.sin(this.bobAngle * 0.08) * 8; + const glowSize = 45 + pulse; + + const col = this.p5.color(this.hexColor); + col.setAlpha(60); + this.p5.fill(col); + this.p5.circle(this.sprite.x, this.sprite.y, glowSize); + + this.p5.pop(); + + } +} \ No newline at end of file diff --git a/src/game/Player.js b/src/game/Player.js index e69de29..983b528 100644 --- a/src/game/Player.js +++ b/src/game/Player.js @@ -0,0 +1,118 @@ +export class Player { + // p5 = the instance, x and y are start position + constructor(p5, x, y){ + this.p5 = p5; + this.spawnX = x; + this.spawnY = y; + + // p5play sprite - this is the body with physics + this.sprite = new p5.Sprite(x,y,28,28); + + // get the character image + this.sprite.image = '/assets/player.png'; + + // we dont want player to tip over + // lock rotation + this.sprite.rotationLock = true; + + // how slidey the movement is + // 0 is not friction 1 is very high friction + this.sprite.friction = 0; // we change later based on vibes + + // we dont want the player to be bouncy... for now at least + this.sprite.bounciness = 0; + + // game state + this.lives = 3; + this.collectedColors = []; // hex color strings + this.isInvincible = false; + this.invincibleTimer = 0; + } + + // this will be called every frame from the game loop + // KeysDwon is an object we can use it is {left, right, jump} + update(keysDown){ + const SPEED = 4; // we can change this later + + if (keysDown.left) this.sprite.vel.x = -SPEED; + if (keysDown.right) this.sprite.vel.x = SPEED; + + // If no key is pressed, slow down + if (!keysDown.left && !keysDown.right){ + this.sprite.vel.x *= 0.7; + } + + // jump but only if the character is on the ground + // p5play built in = sprite.touching.bottom + if (keysDown.jump && this.sprite.touching && this.sprite.touching.bottom){ + this.sprite.vel.y = -11; // negative is up in p5 + keysDown.jump = false; + } + + // for when the player is invincible + if (this.isInvincible){ + this.invincibleTimer--; + if (this.invincibleTimer <= 0){ + this.isInvincible = false; + } + } + } + + // if player hits tar or enemym they lose a life + /// if no lives left, game over + loseLife(){ + if (this.isInvincible) return false; + + this.lives--; + this.isInvincible = true; // wont gain damage a little bit after losign a life + this.invincibleTimer = 90; // 1.5 seconds if we are at 60fps + this.respawn(); + + return this.lives <=0; // if true = game over + } + + // respawn at level start spawn point and be static + respawn(){ + this.sprite.x = this.spawnX; + this.sprite.y = this.spawnY; + this.sprite.vel.x = 0; + this.sprite.vel.y = 0; + } + + // player walks into a frament + collectFragment(hexColor){ + this.collectedColors.push(hexColor); + // we start getting color! + } + + //glow effect around the player + // this should be called before drawing character so its under it + drawGlow(){ + if (this.collectedColors.length === 0) return; + + // we want the last collected color to be the glow + const c = this.collectedColors[this.collectedColors.length -1]; + + this.p5.push(); + this.p5.noStroke(); + + // draw a gradiet with circles that lessen in opacity + // fake flow effect lol + // measurements based on 64 by 64 character + for (let radius = 60; radius > 0; radius -= 8){ + const alpha = (radius / 60) * 40; // max is 40 + const col = this.p5.color(c); + col.setAlpha(alpha); + this.p5.fill(col); + this.p5.circle(this.sprite.x, this.sprite.y, radius*2); + } + this.p5.pop(); + } + + // flickery effect for invincibility + isVisible(){ + if (!this.isInvincible) return true; + // show/hide every 6 frames so it blinks + return Math.floor(this.invincibleTimer / 6) % 2 === 0; + } +} \ No newline at end of file diff --git a/src/game/TarPuddle.js b/src/game/TarPuddle.js index e69de29..93f7463 100644 --- a/src/game/TarPuddle.js +++ b/src/game/TarPuddle.js @@ -0,0 +1,14 @@ +// the tar puddle is like an enemy + +export class TarPuddle { + constructor(p5, x, y) { + this.sprite = new p5.Sprite(x, y, 64, 16); + this.sprite.image = '/assets/tar.png'; + this.sprite.collider = 'none'; // no physics needed just check for overlap + this.sprite.rotationLock = true; + } + + overlapsPlayer(playerSprite){ + return playerSprite.overlaps(this.sprite); + } +} \ No newline at end of file diff --git a/src/game/levelData.js b/src/game/levelData.js index e69de29..226cc25 100644 --- a/src/game/levelData.js +++ b/src/game/levelData.js @@ -0,0 +1,81 @@ +// level config will be in one array +// index 0 = level 1, etc +// edit level change calculations here +// x,y = center position. w, h = width and height of the platform + +export const LEVELS = [ + { + id: 1, + name: 'The Gray Beginning', + color: '#FF4136', + //bgFar: '/backgrounds/level1_far/png', + //bgMid: '/backgrounds/level1_mid.png', + spawnX: 80, + spawnY: 380, + + // each platform: {x,y,w,h} + platforms: [ + {x: 400, y: 440, w: 800, h: 20}, // ground + {x: 220, y: 360, w: 160, h: 16}, + {x: 430, y: 300, w: 140, h: 16 }, + {x: 640, y: 240, w: 160, h: 16 }, + {x: 320, y: 210, w: 120, h: 16 }, + ], + + // each fragment: {x,y,color} + fragments: [ + {x: 220, y: 330, color: '#FF4136'}, + {x: 430, y: 270, color: '#FF4136'}, + {x: 640, y: 210, color: '#FF4136'}, + ], + + // each enemy: {x,y, patrol} + enemies: [ + {x: 430, y: 280, patrol: 50}, + ], + + // each puddle: {x, y} + tar: [], + }, + { + id: 2, + name: 'Warmer skies', + color: '#FF851B', + bgFar: '/backgrounds/level2_far.png', + bgMid: '/backgrounds/level2_mid.png', + spawnX: 80, + spawnY: 380, + + platforms: [ + { x: 400, y: 440, w: 800, h: 20 }, + { x: 180, y: 370, w: 140, h: 16 }, + { x: 380, y: 310, w: 120, h: 16 }, + { x: 560, y: 250, w: 120, h: 16 }, + { x: 700, y: 330, w: 100, h: 16 }, + { x: 300, y: 200, w: 100, h: 16 }, + ], + + fragments: [ + { x: 180, y: 340, color: '#FF851B' }, + { x: 380, y: 280, color: '#FF851B' }, + { x: 300, y: 170, color: '#FF851B' }, + { x: 700, y: 300, color: '#FF851B' }, + ], + + enemies: [ + { x: 380, y: 290, patrol: 45 }, + { x: 560, y: 230, patrol: 40 }, + ], + + tar: [ + { x: 150, y: 432 }, + ], + }, + // Levels 3–5 will follow the same pattern +]; + + +// get level by ID (indexed) +export function getLevel(id){ + return LEVELS.find(level => level.id === id) +} \ No newline at end of file diff --git a/src/routes/Game.svelte b/src/routes/Game.svelte index 4432b31..e5e7ea2 100644 --- a/src/routes/Game.svelte +++ b/src/routes/Game.svelte @@ -1,7 +1,25 @@ -

- ColorQuest - Game (this is a placeholder) -

+ + +
+ + +
+ + \ No newline at end of file diff --git a/src/routes/GameOver.svelte b/src/routes/GameOver.svelte index 120c191..8b4cd16 100644 --- a/src/routes/GameOver.svelte +++ b/src/routes/GameOver.svelte @@ -1,7 +1,67 @@ -

- ColorQuest - Game Over (this is a placeholder) -

+ + +
+

the light faded...

+

the world stays gray a little longer

+
+ + +
+
+ + \ No newline at end of file diff --git a/src/routes/Home.svelte b/src/routes/Home.svelte index 146fdd9..e910fa6 100644 --- a/src/routes/Home.svelte +++ b/src/routes/Home.svelte @@ -1,7 +1,72 @@ -

- ColorQuest - Home (this is a placeholder) -

+ + +
+ + +
+

ColorQuest

+

Bring color back to your world

+ +
+
+ + \ No newline at end of file diff --git a/src/routes/LevelSelect.svelte b/src/routes/LevelSelect.svelte index 2d40e80..1c9e948 100644 --- a/src/routes/LevelSelect.svelte +++ b/src/routes/LevelSelect.svelte @@ -1,7 +1,100 @@ -

- ColorQuest - Level Select (this is a placeholder) -

+ + +
+

Choose a level

+ +
+ {#each LEVELS as level} + + {/each} +
+ + +
+ + \ No newline at end of file diff --git a/src/stores/colorStore.js b/src/stores/colorStore.js index ced6a95..42d1b1a 100644 --- a/src/stores/colorStore.js +++ b/src/stores/colorStore.js @@ -5,17 +5,17 @@ // so I can place my color states here so they are updated everywhere -// a writebale is a box that holds a value -// any svelte comonent can read from within it -//autupdated when value changes +// a writable is a box that holds a value +// any svelte component can read from within it +// autoupdated when value changes // its like the dollar sign notation // -- so these are the global variables ---- -import {writeable} from 'svelte/store'; +import { writable } from 'svelte/store'; // world starts gray so unlockedColors start as an empty array (there is none) -export const unlockedColors = writable({}); +export const unlockedColors = writable([]); // the current level number export const currentLevel = writable(1);