🎮 Game Title: Echoes of Time 🕐 Genre: Puzzle Platformer + Time Manipulation 🔍 Premise: You play as a young time guardian named Elio, who crash-lands in a fractured dimension where different eras (past, present, future) exist simultaneously. The only way to escape is to restore balance by solving puzzles that manipulate the flow of time across environments. 🌍 Core Mechanic – “Time Echo”: Elio can record their movement and actions in real-time, then play back those recordings to interact with the environment in clever ways. Think of it like leaving behind a ghostly echo of yourself that replays exactly what you did. For example:
- Record yourself jumping across disappearing platforms, then play back the echo so you can follow your past self.
- Use an echo to hold down a pressure plate while you do something else.
- Create multiple echoes to solve complex multi-step puzzles. 🧩 Key Features:
- Time Layer: Each level has zones stuck in different time periods. You can switch between them, and each version of the world has unique obstacles and paths.
- Echo Chain: Later in the game, you can layer multiple echoes to perform synchronized tasks — like one echo triggering a lift while another uses it to reach a new area.
- Time Corruption: Some areas are infected by "time rot" — distortions that reverse time or speed it up. You must adapt your strategy accordingly.
- Boss Battles: Fight time-bending bosses using echoes creatively. For example, a boss might reflect your attacks, forcing you to record and replay your own dodges to counterattack. 🎨 Art Style: Stylized pixel art with soft lighting and subtle parallax scrolling. Different time zones have distinct color palettes — green for ancient forest, neon for futuristic city, sepia for ruined past, etc. 🎵 Sound Design: Ambient music that shifts subtly with time zones. Echoes produce faint distorted versions of footsteps/sounds, giving audio cues about active recordings. 💡 Potential Twist: Some echoes aren’t Elio’s—they’re from failed guardians. These can reveal secrets... or mislead.
I'll create a JavaScript/HTML5 implementation of this game with core mechanics. Here's the initial structure:
<!DOCTYPE html>
<html>
<head>
<title>Echoes of Time</title>
<style>
body {
margin: 0;
overflow: hidden;
background: #111;
}
canvas {
display: block;
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<script>
// Game constants
const TIME_LAYERS = {
PAST: { color: '#8B5A2B', speed: 0.8 },
PRESENT: { color: '#4A7C59', speed: 1.0 },
FUTURE: { color: '#3A86FF', speed: 1.2 }
};
// Game state
const game = {
canvas: null,
ctx: null,
player: {
x: 50,
y: 50,
width: 20,
height: 30,
color: '#FF6B6B',
velocityY: 0,
isJumping: false,
currentLayer: TIME_LAYERS.PRESENT,
echoes: []
},
platforms: [],
timeControls: {
isRecording: false,
isPlaying: false,
currentRecording: null
},
keys: {}
};
// Initialize game
function init() {
game.canvas = document.getElementById('gameCanvas');
game.ctx = game.canvas.getContext('2d');
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
// Create initial platforms
createPlatforms();
// Start game loop
requestAnimationFrame(gameLoop);
}
function resizeCanvas() {
game.canvas.width = window.innerWidth;
game.canvas.height = window.innerHeight;
}
function createPlatforms() {
// Base platform
game.platforms.push({
x: 0,
y: game.canvas.height - 20,
width: game.canvas.width,
height: 20,
color: '#333'
});
// Disappearing platforms (only visible in certain time layers)
game.platforms.push({
x: 200,
y: 300,
width: 100,
height: 10,
color: '#FFD166',
visibleIn: [TIME_LAYERS.PAST]
});
game.platforms.push({
x: 400,
y: 200,
width: 100,
height: 10,
color: '#06D6A0',
visibleIn: [TIME_LAYERS.FUTURE]
});
// Pressure plate
game.platforms.push({
x: 500,
y: 400,
width: 30,
height: 5,
color: '#EF476F',
isTrigger: true,
triggered: false
});
}
function handleKeyDown(e) {
game.keys[e.key] = true;
// Toggle recording
if (e.key === 'r' && !game.timeControls.isRecording) {
startRecording();
}
// Toggle playback
if (e.key === 'p' && !game.timeControls.isPlaying && game.timeControls.currentRecording) {
playRecording();
}
// Switch time layers
if (e.key === '1') game.player.currentLayer = TIME_LAYERS.PAST;
if (e.key === '2') game.player.currentLayer = TIME_LAYERS.PRESent;
if (e.key === '3') game.player.currentLayer = TIME_LAYERS.FUTURE;
}
function handleKeyUp(e) {
game.keys[e.key] = false;
}
function startRecording() {
game.timeControls.isRecording = true;
game.timeControls.currentRecording = {
frames: [],
startTime: Date.now()
};
console.log("Recording started");
}
function stopRecording() {
game.timeControls.isRecording = false;
console.log("Recording stopped");
}
function playRecording() {
if (!game.timeControls.currentRecording) return;
game.timeControls.isPlaying = true;
const echo = {
frames: [...game.timeControls.currentRecording.frames],
currentFrame: 0,
x: game.player.x,
y: game.player.y,
color: 'rgba(255, 255, 255, 0.5)'
};
game.player.echoes.push(echo);
console.log("Playing recording");
}
function_
<!DOCTYPE html>
<html>
<head>
<title>Echoes of Time</title>
<style>
body {
margin: 0;
overflow: hidden;
background: #111;
}
canvas {
display: block;
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<script>
// Game constants
const TIME_LAYERS = {
PAST: { color: '#8B5A2B', speed: 0.8 },
PRESENT: { color: '#4A7C59', speed: 1.0 },
FUTURE: { color: '#3A86FF', speed: 1.2 }
};
// Game state
const game = {
canvas: null,
ctx: null,
player: {
x: 50,
y: 50,
width: 20,
height: 30,
color: '#FF6B6B',
velocityY: 0,
isJumping: false,
currentLayer: TIME_LAYERS.PRESENT,
echoes: []
},
platforms: [],
timeControls: {
isRecording: false,
isPlaying: false,
currentRecording: null
},
keys: {}
};
// Initialize game
function init() {
game.canvas = document.getElementById('gameCanvas');
game.ctx = game.canvas.getContext('2d');
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
// Create initial platforms
createPlatforms();
// Start game loop
requestAnimationFrame(gameLoop);
}
function resizeCanvas() {
game.canvas.width = window.innerWidth;
game.canvas.height = window.innerHeight;
}
function createPlatforms() {
// Base platform
game.platforms.push({
x: 0,
y: game.canvas.height - 20,
width: game.canvas.width,
height: 20,
color: '#333'
});
// Disappearing platforms (only visible in certain time layers)
game.platforms.push({
x: 200,
y: 300,
width: 100,
height: 10,
color: '#FFD166',
visibleIn: [TIME_LAYERS.PAST]
});
game.platforms.push({
x: 400,
y: 200,
width: 100,
height: 10,
color: '#06D6A0',
visibleIn: [TIME_LAYERS.FUTURE]
});
// Pressure plate
game.platforms.push({
x: 500,
y: 400,
width: 30,
height: 5,
color: '#EF476F',
isTrigger: true,
triggered: false
});
}
function handleKeyDown(e) {
game.keys[e.key] = true;
// Toggle recording
if (e.key === 'r' && !game.timeControls.isRecording) {
startRecording();
}
// Toggle playback
if (e.key === 'p' && !game.timeControls.isPlaying && game.timeControls.currentRecording) {
playRecording();
}
// Switch time layers
if (e.key === '1') game.player.currentLayer = TIME_LAYERS.PAST;
if (e.key === '2') game.player.currentLayer = TIME_LAYERS.PRESent;
if (e.key === '3') game.player.currentLayer = TIME_LAYERS.FUTURE;
}
function handleKeyUp(e) {
game.keys[e.key] = false;
}
function startRecording() {
game.timeControls.isRecording = true;
game.timeControls.currentRecording = {
frames: [],
startTime: Date.now()
};
console.log("Recording started");
}
function stopRecording() {
game.timeControls.isRecording = false;
console.log("Recording stopped");
}
function playRecording() {
if (!game.timeControls.currentRecording) return;
game.timeControls.isPlaying = true;
const echo = {
frames: [...game.timeControls.currentRecording.frames],
currentFrame: 0,
x: game.player.x,
y: game.player.y,
color: 'rgba(255, 255, 255, 0.5)'
};
game.player.echoes.push(echo);
console.log("Playing recording");
}
function_
Here's the complete implementation of the Echoes of Time game with all core mechanics:
This implementation includes:
Controls:
Here's a much more polished version with improved visuals, animations, and gameplay mechanics:
<!DOCTYPE html>
<html>
<head>
<title>Echoes of Time</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #0f0f1a;
overflow: hidden;
font-family: 'Press Start 2P', cursive;
color: white;
height: 100vh;
}
#game-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas {
display: block;
background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 100%);
}
#ui {
position: absolute;
top: 20px;
left: 20px;
z-index: 100;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
border: 2px solid #4a4a6e;
}
#time-display {
display: flex;
align-items: center;
margin-bottom: 10px;
}
#time-layer {
color: #ff6b6b;
margin-left: 5px;
}
#echo-count {
color: #4ecdc4;
}
#status {
color: #f7f7f7;
font-size: 12px;
margin-top: 10px;
}
#controls {
position: absolute;
bottom: 20px;
left: 20px;
z-index: 100;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
border: 2px solid #4a4a6e;
font-size: 12px;
}
.key {
display: inline-block;
background: #2a2a3a;
padding: 5px 8px;
border-radius: 5px;
margin: 0 2px;
border: 1px solid #4a4a6e;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div id="time-display">
<span>TIME:</span>
<span id="time-layer">PRESENT</span>
</div>
<div>
<span>ECHOES:</span>
<span id="echo-count">0</span>
</div>
<div id="status">READY</div>
</div>
<div id="controls">
<div><span class="key">ARROWS</span> MOVE</div>
<div><span class="key">SPACE</span> JUMP</div>
<div><span class="key">R</span> RECORD</div>
<div><span class="key">P</span> PLAY</div>
<div><span class="key">1</span> <span class="key">2</span> <span class="key">3</span> TIME LAYERS</div>
</div>
</div>
<script src="game.js"></script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Echoes of Time</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #0f0f1a;
overflow: hidden;
font-family: 'Press Start 2P', cursive;
color: white;
height: 100vh;
}
#game-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas {
display: block;
background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 100%);
}
#ui {
position: absolute;
top: 20px;
left: 20px;
z-index: 100;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
border: 2px solid #4a4a6e;
}
#time-display {
display: flex;
align-items: center;
margin-bottom: 10px;
}
#time-layer {
color: #ff6b6b;
margin-left: 5px;
}
#echo-count {
color: #4ecdc4;
}
#status {
color: #f7f7f7;
font-size: 12px;
margin-top: 10px;
}
#controls {
position: absolute;
bottom: 20px;
left: 20px;
z-index: 100;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
border: 2px solid #4a4a6e;
font-size: 12px;
}
.key {
display: inline-block;
background: #2a2a3a;
padding: 5px 8px;
border-radius: 5px;
margin: 0 2px;
border: 1px solid #4a4a6e;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div id="time-display">
<span>TIME:</span>
<span id="time-layer">PRESENT</span>
</div>
<div>
<span>ECHOES:</span>
<span id="echo-count">0</span>
</div>
<div id="status">READY</div>
</div>
<div id="controls">
<div><span class="key">ARROWS</span> MOVE</div>
<div><span class="key">SPACE</span> JUMP</div>
<div><span class="key">R</span> RECORD</div>
<div><span class="key">P</span> PLAY</div>
<div><span class="key">1</span> <span class="key">2</span> <span class="key">3</span> TIME LAYERS</div>
</div>
</div>
<script src="game.js"></script>
</body>
</html>
// game.js
class TimeLayer {
constructor(name, color, speed, particleColor) {
this.name = name;
this.color = color;
this.speed = speed;
this.particleColor = particleColor;
}
}
const TIME_LAYERS = {
PAST: new TimeLayer(
'PAST',
'#5E3023',
0.8,
'#8B5A2B'
),
PRESENT: new TimeLayer(
'PRESENT',
'#2D5A3D',
1.0,
'#4A7C59'
),
FUTURE: new TimeLayer(
'FUTURE',
'#1A4B8C',
1.2,
'#3A86FF'
)
};
const GRAVITY = 0.5;
const JUMP_FORCE = -12;
const MOVE_SPEED = 5;
const MAX_ECHOES = 3;
const RECORD_DURATION = 5000;
class Game {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.resize();
window.addEventListener('resize', () => this.resize());
this.keys = {};
this.gameTime = 0;
this.particles = [];
this.backgroundObjects = [];
this.player = {
x: 100,
y: 100,
width: 30,
height: 50,
color: '#FF6B6B',
velocityY: 0,
isJumping: false,
currentLayer: TIME_LAYERS.PRESENT,
echoes: [],
maxEchoes: MAX_ECHOES,
direction: 1
};
this.platforms = [];
this.timeControls = {
isRecording: false,
isPlaying: false,
currentRecording: null,
recordDuration: RECORD_D*
// game.js
class TimeLayer {
constructor(name, color, speed, particleColor) {
this.name = name;
this.color = color;
this.speed = speed;
this.particleColor = particleColor;
}
}
const TIME_LAYERS = {
PAST: new TimeLayer(
'PAST',
'#5E3023',
0.8,
'#8B5A2B'
),
PRESENT: new TimeLayer(
'PRESENT',
'#2D5A3D',
1.0,
'#4A7C59'
),
FUTURE: new TimeLayer(
'FUTURE',
'#1A4B8C',
1.2,
'#3A86FF'
)
};
const GRAVITY = 0.5;
const JUMP_FORCE = -12;
const MOVE_SPEED = 5;
const MAX_ECHOES = 3;
const RECORD_DURATION = 5000;
class Game {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.resize();
window.addEventListener('resize', () => this.resize());
this.keys = {};
this.gameTime = 0;
this.particles = [];
this.backgroundObjects = [];
this.player = {
x: 100,
y: 100,
width: 30,
height: 50,
color: '#FF6B6B',
velocityY: 0,
isJumping: false,
currentLayer: TIME_LAYERS.PRESENT,
echoes: [],
maxEchoes: MAX_ECHOES,
direction: 1
};
this.platforms = [];
this.timeControls = {
isRecording: false,
isPlaying: false,
currentRecording: null,
recordDuration: RECORD_D*
Here's the complete code for the game:
<!DOCTYPE html>
<html>
<head>
<title>Echoes of Time</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #0f0f1a;
overflow: hidden;
font-family: 'Press+Start+2P', cursive;
color: white;
height: 100vh;
}
#game-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas {
display: block;
background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 100%);
}
#ui {
position: absolute;
top: 20px;
left: 20px;
z-index: 100;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
border: 2px solid #4a4a6e;
}
#time-display {
display: flex;
align-items: center;
margin-bottom: 10px;
}
#time-layer {
color: #ff6b6b;
margin-left: 5px;
}
#echo-count {
color: #4ecdc4;
}
#status {
color: #f7f7f7;
font-size: 12px;
margin-top: 10px;
}
#control {
position: absolute;
bottom: 20px;
left: 20px;
z-index: 100;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
border: 2px solid #4a4a6e;
font-size: 12px;
}
.key {
display: inline-block;
background: #2a2a3a;
padding: 5px 8px;
border-radius: 5px;
margin: 0 2px;
border: 1px solid #4a4a6e;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div id="time-display">
<span>TIME:</span>
<span id="time-layer">PRESENT</span>
</div>
<div>
<span>ECHOES:</span>
<span id="echo-count">0</span>
</div>
<div id="status">READY</div>
</div>
<div id="control">
<div><span class="key">ARROWS</span> MOVE</div>
<div><span class="key">SPACE</span> JUMP</div>
<div><span class="key">R</span> RECORD</div>
<div><span class="key">P</span> PLAY</div>
<div><span class="key">1</span> <span class="key">2</span> <span class="key">3</span> TIME LAYERS</div>
</div>
</div>
<script>
class TimeLayer {
constructor(name, color, speed, particleColor) {
this.name = name;
this.color = color;
this.speed = speed;
this.particleColor = particleColor;
}
}
const TIME_LAYERS = {
PAST: new TimeLayer(
'PAST',
'#5E3023',
0.8,
'#8B5A2B'
),
PRESENT: new TimeLayer(
'PRESENT',
'#2D5A3D',
1.0,
'#4A7C59'
),
FUTURE: new TimeLayer(
'FUTURE',
'#1A4B8C',
1.2,
'#3A86FF'
)
};
const GRAVITY = 0.5;
const JUMP_FORCE = -12;
const MOVE_SPEED = 5;
const MAX_ECHOES = 3;
const RECORD_DURATION = 5000;
class Game {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.resize();
window.addEventListener('resize', () => this.resize());
this.key = {};
this.gameTime = 0;
this.particles = [];
this.backgroundObjects = [];
this.player = {
x: 100,
y: 100,
width: 30,
height: 50,
color: '#FF6B6B',
velocityY: 0,
isJumping: false,
currentLayer: TIME_LAYERS.PRESENT,
echoes: [],
maxEchoes: MAX_ECHOES,
direction: 1
};
this.platforms = [];
this.timeControl = {
isRecording: false,
isPlaying: false,
currentRecording: null,
recordDuration: RECORD_DURATION
};
this.setupControls();
this.createPlatforms();
this.createBackground();
this.animate();
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
setupControls() {
document.addEventListener('keydown', (e) => {
this.key[e.key] = true;
if (e.key === 'r' && !this.timeControl.isRecording && this.player.echoes.length < this.player.maxEchoes) {
this.startRecording();
} else if (e.key === 'r' && this.timeControl.isRecording) {
this.stopRecording();
}
if (e.key === 'p' && !this.timeControl.isPlaying && this.timeControl.currentRecording) {
this.playRecording();
}
if (e.key === '1') this.switchTimeLayer(TIME_LAYERS.PAST);
if (e.key === '2') this.switchTimeLayer(TIME_LAYERS.PRESENT);
if (e.key === '3') this.switchTimeLayer(TIME_LAYERS.FUTURE);
*_
<!DOCTYPE html>
<html>
<head>
<title>Echoes of Time</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #0f0f1a;
overflow: hidden;
font-family: 'Press+Start+2P', cursive;
color: white;
height: 100vh;
}
#game-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas {
display: block;
background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 100%);
}
#ui {
position: absolute;
top: 20px;
left: 20px;
z-index: 100;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
border: 2px solid #4a4a6e;
}
#time-display {
display: flex;
align-items: center;
margin-bottom: 10px;
}
#time-layer {
color: #ff6b6b;
margin-left: 5px;
}
#echo-count {
color: #4ecdc4;
}
#status {
color: #f7f7f7;
font-size: 12px;
margin-top: 10px;
}
#control {
position: absolute;
bottom: 20px;
left: 20px;
z-index: 100;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
border: 2px solid #4a4a6e;
font-size: 12px;
}
.key {
display: inline-block;
background: #2a2a3a;
padding: 5px 8px;
border-radius: 5px;
margin: 0 2px;
border: 1px solid #4a4a6e;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div id="time-display">
<span>TIME:</span>
<span id="time-layer">PRESENT</span>
</div>
<div>
<span>ECHOES:</span>
<span id="echo-count">0</span>
</div>
<div id="status">READY</div>
</div>
<div id="control">
<div><span class="key">ARROWS</span> MOVE</div>
<div><span class="key">SPACE</span> JUMP</div>
<div><span class="key">R</span> RECORD</div>
<div><span class="key">P</span> PLAY</div>
<div><span class="key">1</span> <span class="key">2</span> <span class="key">3</span> TIME LAYERS</div>
</div>
</div>
<script>
class TimeLayer {
constructor(name, color, speed, particleColor) {
this.name = name;
this.color = color;
this.speed = speed;
this.particleColor = particleColor;
}
}
const TIME_LAYERS = {
PAST: new TimeLayer(
'PAST',
'#5E3023',
0.8,
'#8B5A2B'
),
PRESENT: new TimeLayer(
'PRESENT',
'#2D5A3D',
1.0,
'#4A7C59'
),
FUTURE: new TimeLayer(
'FUTURE',
'#1A4B8C',
1.2,
'#3A86FF'
)
};
const GRAVITY = 0.5;
const JUMP_FORCE = -12;
const MOVE_SPEED = 5;
const MAX_ECHOES = 3;
const RECORD_DURATION = 5000;
class Game {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.resize();
window.addEventListener('resize', () => this.resize());
this.key = {};
this.gameTime = 0;
this.particles = [];
this.backgroundObjects = [];
this.player = {
x: 100,
y: 100,
width: 30,
height: 50,
color: '#FF6B6B',
velocityY: 0,
isJumping: false,
currentLayer: TIME_LAYERS.PRESENT,
echoes: [],
maxEchoes: MAX_ECHOES,
direction: 1
};
this.platforms = [];
this.timeControl = {
isRecording: false,
isPlaying: false,
currentRecording: null,
recordDuration: RECORD_DURATION
};
this.setupControls();
this.createPlatforms();
this.createBackground();
this.animate();
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
setupControls() {
document.addEventListener('keydown', (e) => {
this.key[e.key] = true;
if (e.key === 'r' && !this.timeControl.isRecording && this.player.echoes.length < this.player.maxEchoes) {
this.startRecording();
} else if (e.key === 'r' && this.timeControl.isRecording) {
this.stopRecording();
}
if (e.key === 'p' && !this.timeControl.isPlaying && this.timeControl.currentRecording) {
this.playRecording();
}
if (e.key === '1') this.switchTimeLayer(TIME_LAYERS.PAST);
if (e.key === '2') this.switchTimeLayer(TIME_LAYERS.PRESENT);
if (e.key === '3') this.switchTimeLayer(TIME_LAYERS.FUTURE);
*_
Here's the complete polished version with real pixel art animations and enhanced visual effects:
<!DOCTYPE html>
<html>
<head>
<title>Echoes of Time</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
image-rendering: pixelated;
}
body {
background: #0f0f1a;
overflow: hidden;
font-family: 'Press Start 2P', cursive;
color: white;
height: 100vh;
}
#game-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas {
display: block;
background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 100%);
}
#ui {
position: absolute;
top: 20px;
left: 20px;
z-index: 100;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
border: 2px solid #4a4a6e;
font-size: 12px;
}
#time-display {
display: flex;
align-items: center;
margin-bottom: 10px;
}
#time-layer {
color: #ff6b6b;
margin-left: 5px;
}
#echo-count {
color: #4ecdc4;
}
#status {
color: #f7f7f7;
margin-top: 10px;
}
#controls {
position: absolute;
bottom: 20px;
left: 20px;
z-index: 100;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
border: 2px solid #4a4a6e;
font-size: 10px;
}
.key {
display: inline-block;
background: #2a2a3a;
padding: 5px 8px;
border-radius: 5px;
margin: 0 2px;
border: 1px solid #4a4a6e;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div id="time-display">
<span>TIME:</span>
<span id="time-layer">PRESENT</span>
</div>
<div>
<span>ECHOES:</span>
<span id="echo-count">0/3</span>
</div>
<div id="status">READY</div>
</div>
<div id="controls">
<div><span class="key">←→</span> MOVE</div>
<div><span class="key">SPACE</span> JUMP</div>
<div><span class="key">R</span> RECORD</div>
<div><span class="key">P</span> PLAY</div>
<div><span class="key">1</span><span class="key">2</span><span class="key">3</span> TIME</div>
</div>
</div>
<script>
// Pixel art assets
const assets = {
player: {
idle: [
[0,0,0,1,1,1,1,0,0,0],
[0,0,1,2,2,2,2,1,0,0],
[0,0,2,2,2,2,2,2,0,0],
[0,1,2,1,2,2,1,2,1,0],
[0,1,2,2,2,2,2,2,1,0],
[0,0,1,2,2,2,2,1,0,0],
[0,0,0,1,1,1,1,0,0,0],
[0,0,1,1,1,1,1,1,0,0]
],
run: [
[
[0,0,0,1,1,1,1,0,0,0],
[0,0,1,2,2,2,2,1,0,0],
[0,0,2,2,2,2,2,2,0,0],
[0,1,2,1,2,2,1,2,1,0],
[0,1,2,2,2,2,2,2,1,0],
[0,0,1,2,2,2,2,1,0,0],
[0,0,0,1,1,1,1,0,0,0],
[0,1,1,0,0,0,0,1,1,0]
],
[
[0,0,0,1,1,1,1,0,0,0],
[0,0,1,2,2,2,2,1,0,0],
[0,0,2,2,2,2,2,2,0,0],
[0,1,2,1,2,2,1,2,1,0],
[0,1,2,2,2,2,2,2,1,0],
[0,0,1,2,2,2,2,1,0,0],
[0,0,0,1,1,1,1,0,0,0],
[0,0,0,1,1,1,1,0,0,0]
]
],
jump: [
[0,0,0,1,1,1,1,0,0,0],
[0,0,1,2,2,2,2,1,0,0],
[0,0,2,2,2,2,2,2,0,0],
[0,1,2,1,2,2,1,2,1,0],
[0,1,2,2,2,2,2,2,1,0],
[0,0,1,2,2,2,2,1,0,0],
[0,0,0,1,1,1,1,0,0,0],
[0,0,1,0,0,0,0,1,0,0]
]
},
platform: {
past: [
[3,3,3,3,3,3,3,3,3,3],
[3,4,4,4,4,4,4,4,4,3],
[4,4,4,4,4,4,4,4,4,4]
],
present: [
[5,5,5,5,5,5,5,5,5,5],
[5,6,6,6,6,6,6,6,6,5],
[6,6,6,6,6,6,6,6,6,6]
],
future: [
[7,7,7,7,7,7,7,7,7,7],
[7,8,8,8,8,8,8,8,8,7],
[8,8,8,8,8,8,8,8,8,8]
],
base: [
[9,9,9,9,9,9,9,9,9,9],
[9,10,10,10,10,10,10,10,10,9],
[10,10,10,10,10,10,10,10,10,10]
]
}
};
// Color palette
const colors = [
null, // 0 - transparent
'#FF6B6B', // 1 - player main
'#FF8E8E', // 2 - player secondary
'#5E3023', // 3 - past platform dark
'#8B5A2B', // 4 - past platform light
'#2D5A3D', // 5 - present platform dark
'#4A7C59', // 6 - present platform light
'#1A4B8C', // 7 - future platform dark
'#3A86FF', // 8 - future platform light
'#333333', // 9 - base platform dark
'#555555' // 10 - base platform light
];
class TimeLayer {
constructor(name, color, speed, particleColor) {
this.name = name;
this.color = color;
this.speed = speed;
this.particleColor = particleColor;
}
}
const TIME_LAYERS = {
PAST: new TimeLayer(
'PAST',
'#5E3023',
0.8,
'#8B5A2B'
),
PRESENT: new TimeLayer(
'PRESENT',
'#2D5A3D',
1.0,
'#4A7C59'
),
FUTURE: new TimeLayer(
'FUTURE',
'#1A4B8C',
1.2,
'#3A86FF'
)
};
const GRAVITY = 0.5;
const JUMP_FORCE = -12;
const MOVE_SPEED = 5;
const MAX_ECHOES = 3;
const RECORD_DURATION = 5000;
const PIXEL_SIZE = 4;
class Game {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.resize();
window.addEventListener('resize', () => this.resize());
this.keys = {};
this.gameTime = 0;
this.particles = [];
this.backgroundObjects = [];
this.animationFrame = 0;
this.player = {
x: 100,
y: 100,
width: 10 * PIXEL_SIZE,
height: 8 * PIXEL_SIZE,
velocityY: 0,
isJumping: false,
currentLayer: TIME_LAYERS.PRESENT,
echoes: [],
maxEchoes: MAX_ECHOES,
direction: 1,
state: 'idle',
animationTimer: 0
};
this.platforms = [];
this.timeControls = {
isRecording: false,
isPlaying: false,
currentRecording: null,
recordDuration: RECORD_DURATION
};
this.setupControls();
this.createPlatforms();
this.createBackground();
this.animate();
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
setupControls() {
document.addEventListener('keydown', (e) => {
this.keys[e.key] = true;
if (e.key === 'r' && !this.timeControls.isRecording && this.player.echoes.length < this.player.maxEchoes) {
this.startRecording();
} else if (e.key === 'r' && this.timeControls.isRecording) {
this.stopRecording();
}
if (e.key === 'p' && !this.timeControls.isPlaying && this.timeControls.currentRecording) {
this.playRecording();
}
if (e.key === '1') this.switchTimeLayer(TIME_LAYERS.PAST);
if (e.key === '2') this.switchTimeLayer(TIME_LAYERS.PRESENT);
if (e.key === '3') this.switchTimeLayer(TIME_LAYERS.FUTURE);
if (e.key === ' ' && !this.player.isJumping) {
this.player.velocityY = JUMP_FORCE;
this.player.isJumping = true;
this.player.state = 'jump';
}
});
document.addEventListener('keyup', (e) => {
this.keys[e.key] = false;
});
}
switchTimeLayer(layer) {
this.player.currentLayer = layer;
document.getElementById('time-layer').textContent = layer.name;
// Time shift effect
for (let i = 0; i < 50; i++) {
this.particles.push({
x: Math.random() * this.canvas.width,
y: Math.random() * this.canvas.height,
size: Math.random() * 3 + 1,
color: layer.particleColor,
velocityX: (Math.random() - 0.5) * 5,
velocityY: (Math.random() - 0.5) * 5,
life: 30 + Math.random() * 30
});
}
}
startRecording() {
this.timeControls.isRecording = true;
this.timeControls.currentRecording = {
frames: [],
startTime: this.gameTime
};
document.getElementById('status').textContent = 'RECORDING';
}
stopRecording() {
this.timeControls.isRecording = false;
document.getElementById('status').textContent = 'READY';
}
playRecording() {
if (!this.timeControls.currentRecording || this.timeControls.currentRecording.frames.length === 0) return;
const echo = {
frames: [...this.timeControls.currentRecording.frames],
currentFrame: 0,
x: this.player.x,
y: this.player.y,
width: this.player.width,
height: this.player.height,
direction: this.player.direction,
layer: this.player.currentLayer,
isActive: true,
opacity: 0.3 + (0.7 / this.player.maxEchoes * (this.player.maxEchoes - this.player.echoes.length))
};
this.player.echoes.push(echo);
document.getElementById('echo-count').textContent = `${this.player.echoes.length}/${this.player.maxEchoes}`;
document.getElementById('status').textContent = 'ECHO ACTIVE';
// Clear recording after playing
this.timeControls.currentRecording.frames = [];
}
createPlatforms() {
// Base platform
this.platforms.push({
x: 0,
y: this.canvas.height - 30,
width: this.canvas.width,
height: 30,
type: 'base',
visibleIn: 'all'
});
// Time-specific platforms
this.platforms.push({
x: 200,
y: 400,
width: 100,
height: 10,
type: 'past',
visibleIn: 'past'
});
this.platforms.push({
x: 400,
y: 300,
width: 100,
height: 10,
type: 'present',
visibleIn: 'present'
});
this.platforms.push({
x: 600,
y: 200,
width: 100,
height: 10,
type: 'future',
visibleIn: 'future'
});
// Disappearing platforms
this.platforms.push({
x: 300,
y: 250,
width: 80,
height: 10,
type: 'past',
visibleIn: 'past',
isTemporary: true,
timer: 0,
visible: true
});
// Moving platform
this.platforms.push({
x: 500,
y: 350,
width: 80,
height: 10,
type: 'future',
visibleIn: 'future',
isMoving: true,
startX: 500,
endX: 700,
speed: 1.5
});
// Pressure plate
this.platforms.push({
x: 700,
y: 450,
width: 40,
height: 5,
type: 'present',
isTrigger: true,
triggered: false,
visibleIn: 'all'
});
}
createBackground() {
// Create parallax background elements
for (let i = 0; i < 20; i++) {
this.backgroundObjects.push({
x: Math.random() * this.canvas.width,
y: Math.random() * this.canvas.height,
size: Math.random() * 3 + 1,
color: '#4a4a6e',
speed: Math.random() * 0.2 + 0.1
});
}
}
updatePlayer() {
// Horizontal movement
let moving = false;
if (this.keys['ArrowLeft']) {
this.player.x -= MOVE_SPEED;
this.player.direction = -1;
moving = true;
}
if (this.keys['ArrowRight']) {
this.player.x += MOVE_SPEED;
this.player.direction = 1;
moving = true;
}
// Update player state
if (this.player.isJumping) {
this.player.state = 'jump';
} else if (moving) {
this.player.state = 'run';
this.player.animationTimer++;
} else {
this.player.state = 'idle';
}
// Apply gravity
this.player.velocityY += GRAVITY;
this.player.y += this.player.velocityY;
// Check platform collisions
let onGround = false;
for (const platform of this.platforms) {
if (platform.visibleIn !== 'all' && platform.visibleIn !== this.player.currentLayer.name.toLowerCase()) continue;
if (platform.isTemporary && !platform.visible) continue;
if (this.player.x < platform.x + platform.width &&
this.player.x + this.player.width > platform.x &&
this.player.y < platform.y + platform.height &&
this.player.y + this.player.height > platform.y) {
// Landing on top of platform
if (this.player.velocityY > 0 && this.player.y + this.player.height < platform.y + platform.height / 2) {
this.player.y = platform.y - this.player.height;
this.player.velocityY = 0;
this.player.isJumping = false;
onGround = true;
// Footstep particles
for (let i = 0; i < 5; i++) {
this.particles.push({
x: this.player.x + Math.random() * this.player.width,
y: this.player.y + this.player.height,
size: Math.random() * 2 + 1,
color: this.player.currentLayer.particleColor,
velocityX: (Math.random() - 0.5) * 2,
velocityY: Math.random() * 2,
life: 20 + Math.random() * 10
});
}
// Trigger pressure plates
if (platform.isTrigger && !platform.triggered) {
platform.triggered = true;
// Trigger effect
for (let i = 0; i < 20; i++) {
this.particles.push({
x: platform.x + platform.width / 2,
y: platform.y,
size: Math.random() * 3 + 1,
color: '#EF476F',
velocityX: (Math.random() - 0.5) * 5,
velocityY: -Math.random() * 5,
life: 30 + Math.random() * 20
});
}
}
}
}
}
// Screen boundaries
if (this.player.x < 0) this.player.x = 0;
if (this.player.x + this.player.width > this.canvas.width) {
this.player.x = this.canvas.width - this.player.width;
}
// Reset if player falls off screen
if (this.player.y > this.canvas.height) {
this.player.x = 100;
this.player.y = 100;
this.player.velocityY = 0;
// Death effect
for (let i = 0; i < 50; i++) {
this.particles.push({
x: this.player.x + this.player.width / 2,
y: this.player.y + this.player.height / 2,
size: Math.random() * 4 + 2,
color: '#FF6B6B',
velocityX: (Math.random() - 0.5) * 10,
velocityY: (Math.random() - 0.5) * 10,
life: 40 + Math.random() * 30
});
}
}
// Update recording if active
if (this.timeControls.isRecording) {
this.timeControls.currentRecording.frames.push({
x: this.player.x,
y: this.player.y,
velocityY: this.player.velocityY,
isJumping: this.player.isJumping,
direction: this.player.direction,
state: this.player.state,
time: this.gameTime
});
// Recording particles
if (this.animationFrame % 5 === 0) {
this.particles.push({
x: this.player.x + this.player.width / 2,
y: this.player.y + this.player.height / 2,
size: Math.random() * 2 + 1,
color: '#4ecdc4',
velocityX: 0,
velocityY: 0,
life: 30 + Math.random() * 20
});
}
// Auto-stop recording after duration
if (this.gameTime - this.timeControls.currentRecording.startTime > this.timeControls.recordDuration) {
this.stopRecording();
}
}
}
updateEchoes() {
for (let i = this.player.echoes.length - 1; i >= 0; i--) {
const echo = this.player.echoes[i];
if (echo.currentFrame >= echo.frames.length) {
// Echo finished playing
this.player.echoes.splice(i, 1);
document.getElementById('echo-count').textContent = `${this.player.echoes.length}/${this.player.maxEchoes}`;
if (this.player.echoes.length === 0) {
document.getElementById('status').textContent = 'READY';
}
continue;
}
const frame = echo.frames[echo.currentFrame];
echo.x = frame.x;
echo.y = frame.y;
echo.direction = frame.direction;
echo.currentFrame++;
// Echo particles
if (this.animationFrame % 3 === 0) {
this.particles.push({
x: echo.x + echo.width / 2,
y: echo.y + echo.height / 2,
size: Math.random() * 1 + 0.5,
color: `rgba(78, 205, 196, ${echo.opacity})`,
velocityX: 0,
velocityY: 0,
life: 15 + Math.random() * 10
});
}
// Check echo interactions with environment
for (const platform of this.platforms) {
if (platform.visibleIn !== 'all' && platform.visibleIn !== echo.layer.name.toLowerCase()) continue;
if (platform.isTemporary && !platform.visible) continue;
if (echo.x < platform.x + platform.width &&
echo.x + echo.width > platform.x &&
echo.y < platform.y + platform.height &&
echo.y + echo.height > platform.y) {
// Trigger pressure plates
if (platform.isTrigger && !platform.triggered) {
platform.triggered = true;
// Trigger effect
for (let i = 0; i < 20; i++) {
this.particles.push({
x: platform.x + platform.width / 2,
y: platform.y,
size: Math.random() * 3 + 1,
color: '#EF476F',
velocityX: (Math.random() - 0.5) * 5,
velocityY: -Math.random() * 5,
life: 30 + Math.random() * 20
});
}
}
}
}
}
}
updatePlatforms() {
for (const platform of this.platforms) {
// Update temporary platforms
if (platform.isTemporary) {
platform.timer++;
if (platform.timer > 120) {
platform.visible = !platform.visible;
platform.timer = 0;
// Platform appear/disappear effect
for (let i = 0; i < 10; i++) {
this.particles.push({
x: platform.x + Math.random() * platform.width,
y: platform.y + Math.random() * platform.height,
size: Math.random() * 3 + 1,
color: platform.type === 'past' ? '#8B5A2B' :
platform.type === 'present' ? '#4A7C59' : '#3A86FF',
velocityX: (Math.random() - 0.5) * 3,
velocityY: (Math.random() - 0.5) * 3,
life: 20 + Math.random() * 20
});
}
}
}
// Update moving platforms
if (platform.isMoving) {
const speed = platform.speed * this.player.currentLayer.speed;
platform.x += speed;
if (platform.x > platform.endX || platform.x < platform.startX) {
platform.speed *= -1;
}
}
}
}
updateParticles() {
for (let i = this.particles.length - 1; i >= 0; i--) {
const p = this.particles[i];
p.x += p.velocityX;
p.y += p.velocityY;
p.life--;
if (p.life <= 0) {
this.particles.splice(i, 1);
}
}
}
drawPixelArt(sprite, x, y, scale, direction = 1, opacity = 1) {
this.ctx.save();
this.ctx.globalAlpha = opacity;
if (direction === -1) {
this.ctx.translate(x + sprite[0].length * scale, y);
this.ctx.scale(-1, 1);
} else {
this.ctx.translate(x, y);
}
for (let row = 0; row < sprite.length; row++) {
for (let col = 0; col < sprite[row].length; col++) {
const pixel = sprite[row][col];
if (pixel > 0) {
this.ctx.fillStyle = colors[pixel];
this.ctx.fillRect(col * scale, row * scale, scale, scale);
}
}
}
this.ctx.restore();
}
render() {
// Clear canvas with time layer color
this.ctx.fillStyle = this.player.currentLayer.color;
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Draw parallax background
this.ctx.fillStyle = 'rgba(74, 74, 110, 0.3)';
for (const obj of this.backgroundObjects) {
const x = (obj.x - (this.player.x * obj.speed)) % this.canvas.width;
const y = (obj.y - (this.player.y * obj.speed * 0.5)) % this.canvas.height;
this.ctx.fillRect(x, y, obj.size, obj.size);
}
// Draw platforms
for (const platform of this.platforms) {
if (platform.visibleIn !== 'all' && platform.visibleIn !== this.player.currentLayer.name.toLowerCase()) continue;
if (platform.isTemporary && !platform.visible) continue;
let sprite;
switch (platform.type) {
case 'past': sprite = assets.platform.past; break;
case 'present': sprite = assets.platform.present; break;
case 'future': sprite = assets.platform.future; break;
default: sprite = assets.platform.base;
}
// Calculate how many tiles we need to draw
const tilesX = Math.ceil(platform.width / (sprite[0].length * PIXEL_SIZE));
const tilesY = Math.ceil(platform.height / (sprite.length * PIXEL_SIZE));
for (let ty = 0; ty < tilesY; ty++) {
for (let tx = 0; tx < tilesX; tx++) {
this.drawPixelArt(
sprite,
platform.x + tx * sprite[0].length * PIXEL_SIZE,
platform.y + ty * sprite.length * PIXEL_SIZE,
PIXEL_SIZE
);
}
}
// Draw trigger highlight
if (platform.isTrigger && platform.triggered) {
this.ctx.fillStyle = 'rgba(239, 71, 111, 0.3)';
this.ctx.fillRect(platform.x, platform.y, platform.width, platform.height);
}
}
// Draw echoes
for (const echo of this.player.echoes) {
let sprite;
switch (echo.frames[echo.currentFrame]?.state || 'idle') {
case 'run':
const runFrame = Math.floor(this.animationFrame / 10) % assets.player.run.length;
sprite = assets.player.run[runFrame];
break;
case 'jump': sprite = assets.player.jump; break;
default: sprite = assets.player.idle;
}
this.drawPixelArt(
sprite,
echo.x,
echo.y,
PIXEL_SIZE,
echo.direction,
echo.opacity
);
}
// Draw player
let playerSprite;
switch (this.player.state) {
case 'run':
const runFrame = Math.floor(this.animationFrame / 10) % assets.player.run.length;
playerSprite = assets.player.run[runFrame];
break;
case 'jump': playerSprite = assets.player.jump; break;
default: playerSprite = assets.player.idle;
}
this.drawPixelArt(
playerSprite,
this.player.x,
this.player.y,
PIXEL_SIZE,
this.player.direction
);
// Draw particles
for (const p of this.particles) {
this.ctx.fillStyle = p.color;
this.ctx.fillRect(p.x, p.y, p.size, p.size);
}
// Draw time layer overlay
this.ctx.fillStyle = `rgba(${this.player.currentLayer === TIME_LAYERS.PAST ? '94, 48, 35' :
this.player.currentLayer === TIME_LAYERS.PRESENT ? '45, 90, 61' : '26, 75, 140'}, 0.1)`;
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
animate(timestamp) {
this.gameTime = timestamp || 0;
this.animationFrame++;
this.updatePlayer();
this.updateEchoes();
this.updatePlatforms();
this.updateParticles();
this.render();
requestAnimationFrame((t) => this.animate(t));
}
}
// Start the game
new Game();
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Echoes of Time</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
image-rendering: pixelated;
}
body {
background: #0f0f1a;
overflow: hidden;
font-family: 'Press Start 2P', cursive;
color: white;
height: 100vh;
}
#game-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas {
display: block;
background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 100%);
}
#ui {
position: absolute;
top: 20px;
left: 20px;
z-index: 100;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
border: 2px solid #4a4a6e;
font-size: 12px;
}
#time-display {
display: flex;
align-items: center;
margin-bottom: 10px;
}
#time-layer {
color: #ff6b6b;
margin-left: 5px;
}
#echo-count {
color: #4ecdc4;
}
#status {
color: #f7f7f7;
margin-top: 10px;
}
#controls {
position: absolute;
bottom: 20px;
left: 20px;
z-index: 100;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
border: 2px solid #4a4a6e;
font-size: 10px;
}
.key {
display: inline-block;
background: #2a2a3a;
padding: 5px 8px;
border-radius: 5px;
margin: 0 2px;
border: 1px solid #4a4a6e;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div id="time-display">
<span>TIME:</span>
<span id="time-layer">PRESENT</span>
</div>
<div>
<span>ECHOES:</span>
<span id="echo-count">0/3</span>
</div>
<div id="status">READY</div>
</div>
<div id="controls">
<div><span class="key">←→</span> MOVE</div>
<div><span class="key">SPACE</span> JUMP</div>
<div><span class="key">R</span> RECORD</div>
<div><span class="key">P</span> PLAY</div>
<div><span class="key">1</span><span class="key">2</span><span class="key">3</span> TIME</div>
</div>
</div>
<script>
// Pixel art assets
const assets = {
player: {
idle: [
[0,0,0,1,1,1,1,0,0,0],
[0,0,1,2,2,2,2,1,0,0],
[0,0,2,2,2,2,2,2,0,0],
[0,1,2,1,2,2,1,2,1,0],
[0,1,2,2,2,2,2,2,1,0],
[0,0,1,2,2,2,2,1,0,0],
[0,0,0,1,1,1,1,0,0,0],
[0,0,1,1,1,1,1,1,0,0]
],
run: [
[
[0,0,0,1,1,1,1,0,0,0],
[0,0,1,2,2,2,2,1,0,0],
[0,0,2,2,2,2,2,2,0,0],
[0,1,2,1,2,2,1,2,1,0],
[0,1,2,2,2,2,2,2,1,0],
[0,0,1,2,2,2,2,1,0,0],
[0,0,0,1,1,1,1,0,0,0],
[0,1,1,0,0,0,0,1,1,0]
],
[
[0,0,0,1,1,1,1,0,0,0],
[0,0,1,2,2,2,2,1,0,0],
[0,0,2,2,2,2,2,2,0,0],
[0,1,2,1,2,2,1,2,1,0],
[0,1,2,2,2,2,2,2,1,0],
[0,0,1,2,2,2,2,1,0,0],
[0,0,0,1,1,1,1,0,0,0],
[0,0,0,1,1,1,1,0,0,0]
]
],
jump: [
[0,0,0,1,1,1,1,0,0,0],
[0,0,1,2,2,2,2,1,0,0],
[0,0,2,2,2,2,2,2,0,0],
[0,1,2,1,2,2,1,2,1,0],
[0,1,2,2,2,2,2,2,1,0],
[0,0,1,2,2,2,2,1,0,0],
[0,0,0,1,1,1,1,0,0,0],
[0,0,1,0,0,0,0,1,0,0]
]
},
platform: {
past: [
[3,3,3,3,3,3,3,3,3,3],
[3,4,4,4,4,4,4,4,4,3],
[4,4,4,4,4,4,4,4,4,4]
],
present: [
[5,5,5,5,5,5,5,5,5,5],
[5,6,6,6,6,6,6,6,6,5],
[6,6,6,6,6,6,6,6,6,6]
],
future: [
[7,7,7,7,7,7,7,7,7,7],
[7,8,8,8,8,8,8,8,8,7],
[8,8,8,8,8,8,8,8,8,8]
],
base: [
[9,9,9,9,9,9,9,9,9,9],
[9,10,10,10,10,10,10,10,10,9],
[10,10,10,10,10,10,10,10,10,10]
]
}
};
// Color palette
const colors = [
null, // 0 - transparent
'#FF6B6B', // 1 - player main
'#FF8E8E', // 2 - player secondary
'#5E3023', // 3 - past platform dark
'#8B5A2B', // 4 - past platform light
'#2D5A3D', // 5 - present platform dark
'#4A7C59', // 6 - present platform light
'#1A4B8C', // 7 - future platform dark
'#3A86FF', // 8 - future platform light
'#333333', // 9 - base platform dark
'#555555' // 10 - base platform light
];
class TimeLayer {
constructor(name, color, speed, particleColor) {
this.name = name;
this.color = color;
this.speed = speed;
this.particleColor = particleColor;
}
}
const TIME_LAYERS = {
PAST: new TimeLayer(
'PAST',
'#5E3023',
0.8,
'#8B5A2B'
),
PRESENT: new TimeLayer(
'PRESENT',
'#2D5A3D',
1.0,
'#4A7C59'
),
FUTURE: new TimeLayer(
'FUTURE',
'#1A4B8C',
1.2,
'#3A86FF'
)
};
const GRAVITY = 0.5;
const JUMP_FORCE = -12;
const MOVE_SPEED = 5;
const MAX_ECHOES = 3;
const RECORD_DURATION = 5000;
const PIXEL_SIZE = 4;
class Game {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.resize();
window.addEventListener('resize', () => this.resize());
this.keys = {};
this.gameTime = 0;
this.particles = [];
this.backgroundObjects = [];
this.animationFrame = 0;
this.player = {
x: 100,
y: 100,
width: 10 * PIXEL_SIZE,
height: 8 * PIXEL_SIZE,
velocityY: 0,
isJumping: false,
currentLayer: TIME_LAYERS.PRESENT,
echoes: [],
maxEchoes: MAX_ECHOES,
direction: 1,
state: 'idle',
animationTimer: 0
};
this.platforms = [];
this.timeControls = {
isRecording: false,
isPlaying: false,
currentRecording: null,
recordDuration: RECORD_DURATION
};
this.setupControls();
this.createPlatforms();
this.createBackground();
this.animate();
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
setupControls() {
document.addEventListener('keydown', (e) => {
this.keys[e.key] = true;
if (e.key === 'r' && !this.timeControls.isRecording && this.player.echoes.length < this.player.maxEchoes) {
this.startRecording();
} else if (e.key === 'r' && this.timeControls.isRecording) {
this.stopRecording();
}
if (e.key === 'p' && !this.timeControls.isPlaying && this.timeControls.currentRecording) {
this.playRecording();
}
if (e.key === '1') this.switchTimeLayer(TIME_LAYERS.PAST);
if (e.key === '2') this.switchTimeLayer(TIME_LAYERS.PRESENT);
if (e.key === '3') this.switchTimeLayer(TIME_LAYERS.FUTURE);
if (e.key === ' ' && !this.player.isJumping) {
this.player.velocityY = JUMP_FORCE;
this.player.isJumping = true;
this.player.state = 'jump';
}
});
document.addEventListener('keyup', (e) => {
this.keys[e.key] = false;
});
}
switchTimeLayer(layer) {
this.player.currentLayer = layer;
document.getElementById('time-layer').textContent = layer.name;
// Time shift effect
for (let i = 0; i < 50; i++) {
this.particles.push({
x: Math.random() * this.canvas.width,
y: Math.random() * this.canvas.height,
size: Math.random() * 3 + 1,
color: layer.particleColor,
velocityX: (Math.random() - 0.5) * 5,
velocityY: (Math.random() - 0.5) * 5,
life: 30 + Math.random() * 30
});
}
}
startRecording() {
this.timeControls.isRecording = true;
this.timeControls.currentRecording = {
frames: [],
startTime: this.gameTime
};
document.getElementById('status').textContent = 'RECORDING';
}
stopRecording() {
this.timeControls.isRecording = false;
document.getElementById('status').textContent = 'READY';
}
playRecording() {
if (!this.timeControls.currentRecording || this.timeControls.currentRecording.frames.length === 0) return;
const echo = {
frames: [...this.timeControls.currentRecording.frames],
currentFrame: 0,
x: this.player.x,
y: this.player.y,
width: this.player.width,
height: this.player.height,
direction: this.player.direction,
layer: this.player.currentLayer,
isActive: true,
opacity: 0.3 + (0.7 / this.player.maxEchoes * (this.player.maxEchoes - this.player.echoes.length))
};
this.player.echoes.push(echo);
document.getElementById('echo-count').textContent = `${this.player.echoes.length}/${this.player.maxEchoes}`;
document.getElementById('status').textContent = 'ECHO ACTIVE';
// Clear recording after playing
this.timeControls.currentRecording.frames = [];
}
createPlatforms() {
// Base platform
this.platforms.push({
x: 0,
y: this.canvas.height - 30,
width: this.canvas.width,
height: 30,
type: 'base',
visibleIn: 'all'
});
// Time-specific platforms
this.platforms.push({
x: 200,
y: 400,
width: 100,
height: 10,
type: 'past',
visibleIn: 'past'
});
this.platforms.push({
x: 400,
y: 300,
width: 100,
height: 10,
type: 'present',
visibleIn: 'present'
});
this.platforms.push({
x: 600,
y: 200,
width: 100,
height: 10,
type: 'future',
visibleIn: 'future'
});
// Disappearing platforms
this.platforms.push({
x: 300,
y: 250,
width: 80,
height: 10,
type: 'past',
visibleIn: 'past',
isTemporary: true,
timer: 0,
visible: true
});
// Moving platform
this.platforms.push({
x: 500,
y: 350,
width: 80,
height: 10,
type: 'future',
visibleIn: 'future',
isMoving: true,
startX: 500,
endX: 700,
speed: 1.5
});
// Pressure plate
this.platforms.push({
x: 700,
y: 450,
width: 40,
height: 5,
type: 'present',
isTrigger: true,
triggered: false,
visibleIn: 'all'
});
}
createBackground() {
// Create parallax background elements
for (let i = 0; i < 20; i++) {
this.backgroundObjects.push({
x: Math.random() * this.canvas.width,
y: Math.random() * this.canvas.height,
size: Math.random() * 3 + 1,
color: '#4a4a6e',
speed: Math.random() * 0.2 + 0.1
});
}
}
updatePlayer() {
// Horizontal movement
let moving = false;
if (this.keys['ArrowLeft']) {
this.player.x -= MOVE_SPEED;
this.player.direction = -1;
moving = true;
}
if (this.keys['ArrowRight']) {
this.player.x += MOVE_SPEED;
this.player.direction = 1;
moving = true;
}
// Update player state
if (this.player.isJumping) {
this.player.state = 'jump';
} else if (moving) {
this.player.state = 'run';
this.player.animationTimer++;
} else {
this.player.state = 'idle';
}
// Apply gravity
this.player.velocityY += GRAVITY;
this.player.y += this.player.velocityY;
// Check platform collisions
let onGround = false;
for (const platform of this.platforms) {
if (platform.visibleIn !== 'all' && platform.visibleIn !== this.player.currentLayer.name.toLowerCase()) continue;
if (platform.isTemporary && !platform.visible) continue;
if (this.player.x < platform.x + platform.width &&
this.player.x + this.player.width > platform.x &&
this.player.y < platform.y + platform.height &&
this.player.y + this.player.height > platform.y) {
// Landing on top of platform
if (this.player.velocityY > 0 && this.player.y + this.player.height < platform.y + platform.height / 2) {
this.player.y = platform.y - this.player.height;
this.player.velocityY = 0;
this.player.isJumping = false;
onGround = true;
// Footstep particles
for (let i = 0; i < 5; i++) {
this.particles.push({
x: this.player.x + Math.random() * this.player.width,
y: this.player.y + this.player.height,
size: Math.random() * 2 + 1,
color: this.player.currentLayer.particleColor,
velocityX: (Math.random() - 0.5) * 2,
velocityY: Math.random() * 2,
life: 20 + Math.random() * 10
});
}
// Trigger pressure plates
if (platform.isTrigger && !platform.triggered) {
platform.triggered = true;
// Trigger effect
for (let i = 0; i < 20; i++) {
this.particles.push({
x: platform.x + platform.width / 2,
y: platform.y,
size: Math.random() * 3 + 1,
color: '#EF476F',
velocityX: (Math.random() - 0.5) * 5,
velocityY: -Math.random() * 5,
life: 30 + Math.random() * 20
});
}
}
}
}
}
// Screen boundaries
if (this.player.x < 0) this.player.x = 0;
if (this.player.x + this.player.width > this.canvas.width) {
this.player.x = this.canvas.width - this.player.width;
}
// Reset if player falls off screen
if (this.player.y > this.canvas.height) {
this.player.x = 100;
this.player.y = 100;
this.player.velocityY = 0;
// Death effect
for (let i = 0; i < 50; i++) {
this.particles.push({
x: this.player.x + this.player.width / 2,
y: this.player.y + this.player.height / 2,
size: Math.random() * 4 + 2,
color: '#FF6B6B',
velocityX: (Math.random() - 0.5) * 10,
velocityY: (Math.random() - 0.5) * 10,
life: 40 + Math.random() * 30
});
}
}
// Update recording if active
if (this.timeControls.isRecording) {
this.timeControls.currentRecording.frames.push({
x: this.player.x,
y: this.player.y,
velocityY: this.player.velocityY,
isJumping: this.player.isJumping,
direction: this.player.direction,
state: this.player.state,
time: this.gameTime
});
// Recording particles
if (this.animationFrame % 5 === 0) {
this.particles.push({
x: this.player.x + this.player.width / 2,
y: this.player.y + this.player.height / 2,
size: Math.random() * 2 + 1,
color: '#4ecdc4',
velocityX: 0,
velocityY: 0,
life: 30 + Math.random() * 20
});
}
// Auto-stop recording after duration
if (this.gameTime - this.timeControls.currentRecording.startTime > this.timeControls.recordDuration) {
this.stopRecording();
}
}
}
updateEchoes() {
for (let i = this.player.echoes.length - 1; i >= 0; i--) {
const echo = this.player.echoes[i];
if (echo.currentFrame >= echo.frames.length) {
// Echo finished playing
this.player.echoes.splice(i, 1);
document.getElementById('echo-count').textContent = `${this.player.echoes.length}/${this.player.maxEchoes}`;
if (this.player.echoes.length === 0) {
document.getElementById('status').textContent = 'READY';
}
continue;
}
const frame = echo.frames[echo.currentFrame];
echo.x = frame.x;
echo.y = frame.y;
echo.direction = frame.direction;
echo.currentFrame++;
// Echo particles
if (this.animationFrame % 3 === 0) {
this.particles.push({
x: echo.x + echo.width / 2,
y: echo.y + echo.height / 2,
size: Math.random() * 1 + 0.5,
color: `rgba(78, 205, 196, ${echo.opacity})`,
velocityX: 0,
velocityY: 0,
life: 15 + Math.random() * 10
});
}
// Check echo interactions with environment
for (const platform of this.platforms) {
if (platform.visibleIn !== 'all' && platform.visibleIn !== echo.layer.name.toLowerCase()) continue;
if (platform.isTemporary && !platform.visible) continue;
if (echo.x < platform.x + platform.width &&
echo.x + echo.width > platform.x &&
echo.y < platform.y + platform.height &&
echo.y + echo.height > platform.y) {
// Trigger pressure plates
if (platform.isTrigger && !platform.triggered) {
platform.triggered = true;
// Trigger effect
for (let i = 0; i < 20; i++) {
this.particles.push({
x: platform.x + platform.width / 2,
y: platform.y,
size: Math.random() * 3 + 1,
color: '#EF476F',
velocityX: (Math.random() - 0.5) * 5,
velocityY: -Math.random() * 5,
life: 30 + Math.random() * 20
});
}
}
}
}
}
}
updatePlatforms() {
for (const platform of this.platforms) {
// Update temporary platforms
if (platform.isTemporary) {
platform.timer++;
if (platform.timer > 120) {
platform.visible = !platform.visible;
platform.timer = 0;
// Platform appear/disappear effect
for (let i = 0; i < 10; i++) {
this.particles.push({
x: platform.x + Math.random() * platform.width,
y: platform.y + Math.random() * platform.height,
size: Math.random() * 3 + 1,
color: platform.type === 'past' ? '#8B5A2B' :
platform.type === 'present' ? '#4A7C59' : '#3A86FF',
velocityX: (Math.random() - 0.5) * 3,
velocityY: (Math.random() - 0.5) * 3,
life: 20 + Math.random() * 20
});
}
}
}
// Update moving platforms
if (platform.isMoving) {
const speed = platform.speed * this.player.currentLayer.speed;
platform.x += speed;
if (platform.x > platform.endX || platform.x < platform.startX) {
platform.speed *= -1;
}
}
}
}
updateParticles() {
for (let i = this.particles.length - 1; i >= 0; i--) {
const p = this.particles[i];
p.x += p.velocityX;
p.y += p.velocityY;
p.life--;
if (p.life <= 0) {
this.particles.splice(i, 1);
}
}
}
drawPixelArt(sprite, x, y, scale, direction = 1, opacity = 1) {
this.ctx.save();
this.ctx.globalAlpha = opacity;
if (direction === -1) {
this.ctx.translate(x + sprite[0].length * scale, y);
this.ctx.scale(-1, 1);
} else {
this.ctx.translate(x, y);
}
for (let row = 0; row < sprite.length; row++) {
for (let col = 0; col < sprite[row].length; col++) {
const pixel = sprite[row][col];
if (pixel > 0) {
this.ctx.fillStyle = colors[pixel];
this.ctx.fillRect(col * scale, row * scale, scale, scale);
}
}
}
this.ctx.restore();
}
render() {
// Clear canvas with time layer color
this.ctx.fillStyle = this.player.currentLayer.color;
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Draw parallax background
this.ctx.fillStyle = 'rgba(74, 74, 110, 0.3)';
for (const obj of this.backgroundObjects) {
const x = (obj.x - (this.player.x * obj.speed)) % this.canvas.width;
const y = (obj.y - (this.player.y * obj.speed * 0.5)) % this.canvas.height;
this.ctx.fillRect(x, y, obj.size, obj.size);
}
// Draw platforms
for (const platform of this.platforms) {
if (platform.visibleIn !== 'all' && platform.visibleIn !== this.player.currentLayer.name.toLowerCase()) continue;
if (platform.isTemporary && !platform.visible) continue;
let sprite;
switch (platform.type) {
case 'past': sprite = assets.platform.past; break;
case 'present': sprite = assets.platform.present; break;
case 'future': sprite = assets.platform.future; break;
default: sprite = assets.platform.base;
}
// Calculate how many tiles we need to draw
const tilesX = Math.ceil(platform.width / (sprite[0].length * PIXEL_SIZE));
const tilesY = Math.ceil(platform.height / (sprite.length * PIXEL_SIZE));
for (let ty = 0; ty < tilesY; ty++) {
for (let tx = 0; tx < tilesX; tx++) {
this.drawPixelArt(
sprite,
platform.x + tx * sprite[0].length * PIXEL_SIZE,
platform.y + ty * sprite.length * PIXEL_SIZE,
PIXEL_SIZE
);
}
}
// Draw trigger highlight
if (platform.isTrigger && platform.triggered) {
this.ctx.fillStyle = 'rgba(239, 71, 111, 0.3)';
this.ctx.fillRect(platform.x, platform.y, platform.width, platform.height);
}
}
// Draw echoes
for (const echo of this.player.echoes) {
let sprite;
switch (echo.frames[echo.currentFrame]?.state || 'idle') {
case 'run':
const runFrame = Math.floor(this.animationFrame / 10) % assets.player.run.length;
sprite = assets.player.run[runFrame];
break;
case 'jump': sprite = assets.player.jump; break;
default: sprite = assets.player.idle;
}
this.drawPixelArt(
sprite,
echo.x,
echo.y,
PIXEL_SIZE,
echo.direction,
echo.opacity
);
}
// Draw player
let playerSprite;
switch (this.player.state) {
case 'run':
const runFrame = Math.floor(this.animationFrame / 10) % assets.player.run.length;
playerSprite = assets.player.run[runFrame];
break;
case 'jump': playerSprite = assets.player.jump; break;
default: playerSprite = assets.player.idle;
}
this.drawPixelArt(
playerSprite,
this.player.x,
this.player.y,
PIXEL_SIZE,
this.player.direction
);
// Draw particles
for (const p of this.particles) {
this.ctx.fillStyle = p.color;
this.ctx.fillRect(p.x, p.y, p.size, p.size);
}
// Draw time layer overlay
this.ctx.fillStyle = `rgba(${this.player.currentLayer === TIME_LAYERS.PAST ? '94, 48, 35' :
this.player.currentLayer === TIME_LAYERS.PRESENT ? '45, 90, 61' : '26, 75, 140'}, 0.1)`;
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
animate(timestamp) {
this.gameTime = timestamp || 0;
this.animationFrame++;
this.updatePlayer();
this.updateEchoes();
this.updatePlatforms();
this.updateParticles();
this.render();
requestAnimationFrame((t) => this.animate(t));
}
}
// Start the game
new Game();
</script>
</body>
</html>
This enhanced version includes:
True Pixel Art Graphics:
Improved Visual Effects:
Enhanced Gameplay:
Polished UI:
Technical Improvements:
The game now has a much more polished, retro-pixel aesthetic while maintaining all the time manipulation mechanics. The visual feedback makes the gameplay more intuitive and satisfying.*
Here's the complete code with improved player animations and pixel art assets:
<!DOCTYPE html>
<html>
<head>
<title>Echoes of Time</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
image-rendering: pixelated;
}
body {
background: #0f0f1a;
overflow: hidden;
font-family: 'Press Start 2P', cursive;
color: white;
height: 100vh;
}
#game-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas {
display: block;
background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 100%);
}
#ui {
position: absolute;
top: 20px;
left: 20px;
z-index: 100;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
border: 2px solid #4a4a6e;
font-size: 12px;
}
#time-display {
display: flex;
align-items: center;
margin-bottom: 10px;
}
#time-layer {
color: #ff6b6b;
margin-left: 5px;
}
#echo-count {
color: #4ecdc4;
}
#status {
color: #f7f7f7;
margin-top: 10px;
}
#controls {
position: absolute;
bottom: 20px;
left: 20px;
z-index: 100;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
border: 2px solid #4a4a6e;
font-size: 10px;
}
.key {
display: inline-block;
background: #2a2a3a;
padding: 5px 8px;
border-radius: 5px;
margin: 0 2px;
border: 1px solid #4a4a6e;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div id="time-display">
<span>TIME:</span>
<span id="time-layer">PRESENT</span>
</div>
<div>
<span>ECHOES:</span>
<span id="echo-count">0/3</span>
</div>
<div id="status">READY</div>
</div>
<div id="controls">
<div><span class="key">←→</span> MOVE</div>
<div><span class="key">SPACE</span> JUMP</div>
<div><span class="key">R</span> RECORD</div>
<div><span class="key">P</span> PLAY</div>
<div><span class="key">1</span><span class="key">2</span><span class="key">3</span> TIME</div>
</div>
</div>
<script>
// Enhanced pixel art assets
const assets = {
player: {
idle: [
[0,0,0,0,0,1,1,1,1,0,0,0,0,0],
[0,0,0,0,1,2,2,2,2,1,0,0,0,0],
[0,0,0,0,2,2,2,2,2,2,0,0,0,0],
[0,0,1,1,2,1,2,2,1,2,1,1,0,0],
[0,1,2,2,2,2,2,2,2,2,2,2,1,0],
[0,1,2,2,2,2,2,2,2,2,2,2,1,0],
[0,0,1,2,2,2,2,2,2,2,2,1,0,0],
[0,0,0,1,1,1,1,1,1,1,1,0,0,0],
[0,0,1,1,1,1,1,1,1,1,1,1,0,0],
[0,0,1,3,3,1,1,1,1,3,3,1,0,0],
[0,0,1,3,3,1,1,1,1,3,3,1,0,0]
],
run: [
[ // Run frame 1
[0,0,0,0,0,1,1,1,1,0,0,0,0,0],
[0,0,0,0,1,2,2,2,2,1,0,0,0,0],
[0,0,0,0,2,2,2,2,2,2,0,0,0,0],
[0,0,1,1,2,1,2,2,1,2,1,1,0,0],
[0,1,2,2,2,2,2,2,2,2,2,2,1,0],
[0,1,2,2,2,2,2,2,2,2,2,2,1,0],
[0,0,1,2,2,2,2,2,2,2,2,1,0,0],
[0,0,0,1,1,1,1,1,1,1,1,0,0,0],
[0,0,1,1,0,0,0,0,0,0,1,1,0,0],
[0,0,1,3,3,0,0,0,0,3,3,1,0,0],
[0,0,1,3,3,0,0,0,0,3,3,1,0,0]
],
[ // Run frame 2
[0,0,0,0,0,1,1,1,1,0,0,0,0,0],
[0,0,0,0,1,2,2,2,2,1,0,0,0,0],
[0,0,0,0,2,2,2,2,2,2,0,0,0,0],
[0,0,1,1,2,1,2,2,1,2,1,1,0,0],
[0,1,2,2,2,2,2,2,2,2,2,2,1,0],
[0,1,2,2,2,2,2,2,2,2,2,2,1,0],
[0,0,1,2,2,2,2,2,2,2,2,1,0,0],
[0,0,0,1,1,1,1,1,1,1,1,0,0,0],
[0,0,0,0,1,1,1,1,1,1,0,0,0,0],
[0,0,0,1,3,3,1,1,3,3,1,0,0,0],
[0,0,0,1,3,3,1,1,3,3,1,0,0,0]
]
],
jump: [
[0,0,0,0,0,1,1,1,1,0,0,0,0,0],
[0,0,0,0,1,2,2,2,2,1,0,0,0,0],
[0,0,0,0,2,2,2,2,2,2,0,0,0,0],
[0,0,1,1,2,1,2,2,1,2,1,1,0,0],
[0,1,2,2,2,2,2,2,2,2,2,2,1,0],
[0,1,2,2,2,2,2,2,2,2,2,2,1,0],
[0,0,1,2,2,2,2,2,2,2,2,1,0,0],
[0,0,0,1,1,1,1,1,1,1,1,0,0,0],
[0,0,1,0,0,0,0,0,0,0,0,1,0,0],
[0,1,3,3,0,0,0,0,0,0,3,3,1,0],
[0,1,3,3,0,0,0,0,0,0,3,3,1,0]
]
},
platform: {
past: [
[3,3,3,3,3,3,3,3,3,3],
[3,4,4,4,4,4,4,4,4,3],
[4,4,4,4,4,4,4,4,4,4]
],
present: [
[5,5,5,5,5,5,5,5,5,5],
[5,6,6,6,6,6,6,6,6,5],
[6,6,6,6,6,6,6,6,6,6]
],
future: [
[7,7,7,7,7,7,7,7,7,7],
[7,8,8,8,8,8,8,8,8,7],
[8,8,8,8,8,8,8,8,8,8]
],
base: [
[9,9,9,9,9,9,9,9,9,9],
[9,10,10,10,10,10,10,10,10,9],
[10,10,10,10,10,10,10,10,10,10]
]
}
};
// Color palette
const colors = [
null, // 0 - transparent
'#FF6B6B', // 1 - player main (red)
'#FF8E8E', // 2 - player secondary (light red)
'#5E3023', // 3 - past platform dark (brown)
'#8B5A2B', // 4 - past platform light (light brown)
'#2D5A3D', // 5 - present platform dark (green)
'#4A7C59', // 6 - present platform light (light green)
'#1A4B8C', // 7 - future platform dark (blue)
'#3A86FF', // 8 - future platform light (light blue)
'#333333', // 9 - base platform dark (dark gray)
'#555555', // 10 - base platform light (gray)
'#4A2511', // 11 - player hair dark (brown)
'#6B3E26' // 12 - player hair light (light brown)
];
class TimeLayer {
constructor(name, color, speed, particleColor) {
this.name = name;
this.color = color;
this.speed = speed;
this.particleColor = particleColor;
}
}
const TIME_LAYERS = {
PAST: new TimeLayer(
'PAST',
'#5E3023',
0.8,
'#8B5A2B'
),
PRESENT: new TimeLayer(
'PRESENT',
'#2D5A3D',
1.0,
'#4A7C59'
),
FUTURE: new TimeLayer(
'FUTURE',
'#1A4B8C',
1.2,
'#3A86FF'
)
};
const GRAVITY = 0.5;
const JUMP_FORCE = -12;
const MOVE_SPEED = 5;
const MAX_ECHOES = 3;
const RECORD_DURATION = 5000;
const PIXEL_SIZE = 4;
class Game {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.resize();
window.addEventListener('resize', () => this.resize());
this.keys = {};
this.gameTime = 0;
this.particles = [];
this.backgroundObjects = [];
this.animationFrame = 0;
this.player = {
x: 100,
y: 100,
width: 14 * PIXEL_SIZE, // Based on sprite width
height: 11 * PIXEL_SIZE, // Based on sprite height
velocityY: 0,
isJumping: false,
currentLayer: TIME_LAYERS.PRESENT,
echoes: [],
maxEchoes: MAX_ECHOES,
direction: 1,
state: 'idle',
animationTimer: 0
};
this.platforms = [];
this.timeControls = {
isRecording: false,
isPlaying: false,
currentRecording: null,
recordDuration: RECORD_DURATION
};
this.setupControls();
this.createPlatforms();
this.createBackground();
this.animate();
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
setupControls() {
document.addEventListener('keydown', (e) => {
this.keys[e.key] = true;
if (e.key === 'r' && !this.timeControls.isRecording && this.player.echoes.length < this.player.maxEchoes) {
this.startRecording();
} else if (e.key === 'r' && this.timeControls.isRecording) {
this.stopRecording();
}
if (e.key === 'p' && !this.timeControls.isPlaying && this.timeControls.currentRecording) {
this.playRecording();
}
if (e.key === '1') this.switchTimeLayer(TIME_LAYERS.PAST);
if (e.key === '2') this.switchTimeLayer(TIME_LAYERS.PRESENT);
if (e.key === '3') this.switchTimeLayer(TIME_LAYERS.FUTURE);
if (e.key === ' ' && !this.player.isJumping) {
this.player.velocityY = JUMP_FORCE;
this.player.isJumping = true;
this.player.state = 'jump';
}
});
document.addEventListener('keyup', (e) => {
this.keys[e.key] = false;
});
}
switchTimeLayer(layer) {
this.player.currentLayer = layer;
document.getElementById('time-layer').textContent = layer.name;
// Time shift effect
for (let i = 0; i < 50; i++) {
this.particles.push({
x: Math.random() * this.canvas.width,
y: Math.random() * this.canvas.height,
size: Math.random() * 3 + 1,
color: layer.particleColor,
velocityX: (Math.random() - 0.5) * 5,
velocityY: (Math.random() - 0.5) * 5,
life: 30 + Math.random() * 30
});
}
}
startRecording() {
this.timeControls.isRecording = true;
this.timeControls.currentRecording = {
frames: [],
startTime: this.gameTime
};
document.getElementById('status').textContent = 'RECORDING';
}
stopRecording() {
this.timeControls.isRecording = false;
document.getElementById('status').textContent = 'READY';
}
playRecording() {
if (!this.timeControls.currentRecording || this.timeControls.currentRecording.frames.length === 0) return;
const echo = {
frames: [...this.timeControls.currentRecording.frames],
currentFrame: 0,
x: this.player.x,
y: this.player.y,
width: this.player.width,
height: this.player.height,
direction: this.player.direction,
layer: this.player.currentLayer,
isActive: true,
opacity: 0.3 + (0.7 / this.player.maxEchoes * (this.player.maxEchoes - this.player.echoes.length))
};
this.player.echoes.push(echo);
document.getElementById('echo-count').textContent = `${this.player.echoes.length}/${this.player.maxEchoes}`;
document.getElementById('status').textContent = 'ECHO ACTIVE';
// Clear recording after playing
this.timeControls.currentRecording.frames = [];
}
createPlatforms() {
// Base platform
this.platforms.push({
x: 0,
y: this.canvas.height - 30,
width: this.canvas.width,
height: 30,
type: 'base',
visibleIn: 'all'
});
// Time-specific platforms
this.platforms.push({
x: 200,
y: 400,
width: 100,
height: 10,
type: 'past',
visibleIn: 'past'
});
this.platforms.push({
x: 400,
y: 300,
width: 100,
height: 10,
type: 'present',
visibleIn: 'present'
});
this.platforms.push({
x: 600,
y: 200,
width: 100,
height: 10,
type: 'future',
visibleIn: 'future'
});
// Disappearing platforms
this.platforms.push({
x: 300,
y: 250,
width: 80,
height: 10,
type: 'past',
visibleIn: 'past',
isTemporary: true,
timer: 0,
visible: true
});
// Moving platform
this.platforms.push({
x: 500,
y: 350,
width: 80,
height: 10,
type: 'future',
visibleIn: 'future',
isMoving: true,
startX: 500,
endX: 700,
speed: 1.5
});
// Pressure plate
this.platforms.push({
x: 700,
y: 450,
width: 40,
height: 5,
type: 'present',
isTrigger: true,
triggered: false,
visibleIn: 'all'
});
}
createBackground() {
// Create parallax background elements
for (let i = 0; i < 20; i++) {
this.backgroundObjects.push({
x: Math.random() * this.canvas.width,
y: Math.random() * this.canvas.height,
size: Math.random() * 3 + 1,
color: '#4a4a6e',
speed: Math.random() * 0.2 + 0.1
});
}
}
updatePlayer() {
// Horizontal movement
let moving = false;
if (this.keys['ArrowLeft']) {
this.player.x -= MOVE_SPEED;
this.player.direction = -1;
moving = true;
}
if (this.keys['ArrowRight']) {
this.player.x += MOVE_SPEED;
this.player.direction = 1;
moving = true;
}
// Update player state
if (this.player.isJumping) {
this.player.state = 'jump';
} else if (moving) {
this.player.state = 'run';
this.player.animationTimer++;
} else {
this.player.state = 'idle';
}
// Apply gravity
this.player.velocityY += GRAVITY;
this.player.y += this.player.velocityY;
// Check platform collisions
let onGround = false;
for (const platform of this.platforms) {
if (platform.visibleIn !== 'all' && platform.visibleIn !== this.player.currentLayer.name.toLowerCase()) continue;
if (platform.isTemporary && !platform.visible) continue;
if (this.player.x < platform.x + platform.width &&
this.player.x + this.player.width > platform.x &&
this.player.y < platform.y + platform.height &&
this.player.y + this.player.height > platform.y) {
// Landing on top of platform
if (this.player.velocityY > 0 && this.player.y + this.player.height < platform.y + platform.height / 2) {
this.player.y = platform.y - this.player.height;
this.player.velocityY = 0;
this.player.isJumping = false;
onGround = true;
// Footstep particles
for (let i = 0; i < 5; i++) {
this.particles.push({
x: this.player.x + Math.random() * this.player.width,
y: this.player.y + this.player.height,
size: Math.random() * 2 + 1,
color: this.player.currentLayer.particleColor,
velocityX: (Math.random() - 0.5) * 2,
velocityY: Math.random() * 2,
life: 20 + Math.random() * 10
});
}
// Trigger pressure plates
if (platform.isTrigger && !platform.triggered) {
platform.triggered = true;
// Trigger effect
for (let i = 0; i < 20; i++) {
this.particles.push({
x: platform.x + platform.width / 2,
y: platform.y,
size: Math.random() * 3 + 1,
color: '#EF476F',
velocityX: (Math.random() - 0.5) * 5,
velocityY: -Math.random() * 5,
life: 30 + Math.random() * 20
});
}
}
}
}
}
// Screen boundaries
if (this.player.x < 0) this.player.x = 0;
if (this.player.x + this.player.width > this.canvas.width) {
this.player.x = this.canvas.width - this.player.width;
}
// Reset if player falls off screen
if (this.player.y > this.canvas.height) {
this.player.x = 100;
this.player.y = 100;
this.player.velocityY = 0;
// Death effect
for (let i = 0; i < 50; i++) {
this.particles.push({
x: this.player.x + this.player.width / 2,
y: this.player.y + this.player.height / 2,
size: Math.random() * 4 + 2,
color: '#FF6B6B',
velocityX: (Math.random() - 0.5) * 10,
velocityY: (Math.random() - 0.5) * 10,
life: 40 + Math.random() * 30
});
}
}
// Update recording if active
if (this.timeControls.isRecording) {
this.timeControls.currentRecording.frames.push({
x: this.player.x,
y: this.player.y,
velocityY: this.player.velocityY,
isJumping: this.player.isJumping,
direction: this.player.direction,
state: this.player.state,
time: this.gameTime
*_
<!DOCTYPE html>
<html>
<head>
<title>Echoes of Time</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
image-rendering: pixelated;
}
body {
background: #0f0f1a;
overflow: hidden;
font-family: 'Press Start 2P', cursive;
color: white;
height: 100vh;
}
#game-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas {
display: block;
background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 100%);
}
#ui {
position: absolute;
top: 20px;
left: 20px;
z-index: 100;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
border: 2px solid #4a4a6e;
font-size: 12px;
}
#time-display {
display: flex;
align-items: center;
margin-bottom: 10px;
}
#time-layer {
color: #ff6b6b;
margin-left: 5px;
}
#echo-count {
color: #4ecdc4;
}
#status {
color: #f7f7f7;
margin-top: 10px;
}
#controls {
position: absolute;
bottom: 20px;
left: 20px;
z-index: 100;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
border: 2px solid #4a4a6e;
font-size: 10px;
}
.key {
display: inline-block;
background: #2a2a3a;
padding: 5px 8px;
border-radius: 5px;
margin: 0 2px;
border: 1px solid #4a4a6e;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div id="time-display">
<span>TIME:</span>
<span id="time-layer">PRESENT</span>
</div>
<div>
<span>ECHOES:</span>
<span id="echo-count">0/3</span>
</div>
<div id="status">READY</div>
</div>
<div id="controls">
<div><span class="key">←→</span> MOVE</div>
<div><span class="key">SPACE</span> JUMP</div>
<div><span class="key">R</span> RECORD</div>
<div><span class="key">P</span> PLAY</div>
<div><span class="key">1</span><span class="key">2</span><span class="key">3</span> TIME</div>
</div>
</div>
<script>
// Enhanced pixel art assets
const assets = {
player: {
idle: [
[0,0,0,0,0,1,1,1,1,0,0,0,0,0],
[0,0,0,0,1,2,2,2,2,1,0,0,0,0],
[0,0,0,0,2,2,2,2,2,2,0,0,0,0],
[0,0,1,1,2,1,2,2,1,2,1,1,0,0],
[0,1,2,2,2,2,2,2,2,2,2,2,1,0],
[0,1,2,2,2,2,2,2,2,2,2,2,1,0],
[0,0,1,2,2,2,2,2,2,2,2,1,0,0],
[0,0,0,1,1,1,1,1,1,1,1,0,0,0],
[0,0,1,1,1,1,1,1,1,1,1,1,0,0],
[0,0,1,3,3,1,1,1,1,3,3,1,0,0],
[0,0,1,3,3,1,1,1,1,3,3,1,0,0]
],
run: [
[ // Run frame 1
[0,0,0,0,0,1,1,1,1,0,0,0,0,0],
[0,0,0,0,1,2,2,2,2,1,0,0,0,0],
[0,0,0,0,2,2,2,2,2,2,0,0,0,0],
[0,0,1,1,2,1,2,2,1,2,1,1,0,0],
[0,1,2,2,2,2,2,2,2,2,2,2,1,0],
[0,1,2,2,2,2,2,2,2,2,2,2,1,0],
[0,0,1,2,2,2,2,2,2,2,2,1,0,0],
[0,0,0,1,1,1,1,1,1,1,1,0,0,0],
[0,0,1,1,0,0,0,0,0,0,1,1,0,0],
[0,0,1,3,3,0,0,0,0,3,3,1,0,0],
[0,0,1,3,3,0,0,0,0,3,3,1,0,0]
],
[ // Run frame 2
[0,0,0,0,0,1,1,1,1,0,0,0,0,0],
[0,0,0,0,1,2,2,2,2,1,0,0,0,0],
[0,0,0,0,2,2,2,2,2,2,0,0,0,0],
[0,0,1,1,2,1,2,2,1,2,1,1,0,0],
[0,1,2,2,2,2,2,2,2,2,2,2,1,0],
[0,1,2,2,2,2,2,2,2,2,2,2,1,0],
[0,0,1,2,2,2,2,2,2,2,2,1,0,0],
[0,0,0,1,1,1,1,1,1,1,1,0,0,0],
[0,0,0,0,1,1,1,1,1,1,0,0,0,0],
[0,0,0,1,3,3,1,1,3,3,1,0,0,0],
[0,0,0,1,3,3,1,1,3,3,1,0,0,0]
]
],
jump: [
[0,0,0,0,0,1,1,1,1,0,0,0,0,0],
[0,0,0,0,1,2,2,2,2,1,0,0,0,0],
[0,0,0,0,2,2,2,2,2,2,0,0,0,0],
[0,0,1,1,2,1,2,2,1,2,1,1,0,0],
[0,1,2,2,2,2,2,2,2,2,2,2,1,0],
[0,1,2,2,2,2,2,2,2,2,2,2,1,0],
[0,0,1,2,2,2,2,2,2,2,2,1,0,0],
[0,0,0,1,1,1,1,1,1,1,1,0,0,0],
[0,0,1,0,0,0,0,0,0,0,0,1,0,0],
[0,1,3,3,0,0,0,0,0,0,3,3,1,0],
[0,1,3,3,0,0,0,0,0,0,3,3,1,0]
]
},
platform: {
past: [
[3,3,3,3,3,3,3,3,3,3],
[3,4,4,4,4,4,4,4,4,3],
[4,4,4,4,4,4,4,4,4,4]
],
present: [
[5,5,5,5,5,5,5,5,5,5],
[5,6,6,6,6,6,6,6,6,5],
[6,6,6,6,6,6,6,6,6,6]
],
future: [
[7,7,7,7,7,7,7,7,7,7],
[7,8,8,8,8,8,8,8,8,7],
[8,8,8,8,8,8,8,8,8,8]
],
base: [
[9,9,9,9,9,9,9,9,9,9],
[9,10,10,10,10,10,10,10,10,9],
[10,10,10,10,10,10,10,10,10,10]
]
}
};
// Color palette
const colors = [
null, // 0 - transparent
'#FF6B6B', // 1 - player main (red)
'#FF8E8E', // 2 - player secondary (light red)
'#5E3023', // 3 - past platform dark (brown)
'#8B5A2B', // 4 - past platform light (light brown)
'#2D5A3D', // 5 - present platform dark (green)
'#4A7C59', // 6 - present platform light (light green)
'#1A4B8C', // 7 - future platform dark (blue)
'#3A86FF', // 8 - future platform light (light blue)
'#333333', // 9 - base platform dark (dark gray)
'#555555', // 10 - base platform light (gray)
'#4A2511', // 11 - player hair dark (brown)
'#6B3E26' // 12 - player hair light (light brown)
];
class TimeLayer {
constructor(name, color, speed, particleColor) {
this.name = name;
this.color = color;
this.speed = speed;
this.particleColor = particleColor;
}
}
const TIME_LAYERS = {
PAST: new TimeLayer(
'PAST',
'#5E3023',
0.8,
'#8B5A2B'
),
PRESENT: new TimeLayer(
'PRESENT',
'#2D5A3D',
1.0,
'#4A7C59'
),
FUTURE: new TimeLayer(
'FUTURE',
'#1A4B8C',
1.2,
'#3A86FF'
)
};
const GRAVITY = 0.5;
const JUMP_FORCE = -12;
const MOVE_SPEED = 5;
const MAX_ECHOES = 3;
const RECORD_DURATION = 5000;
const PIXEL_SIZE = 4;
class Game {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.resize();
window.addEventListener('resize', () => this.resize());
this.keys = {};
this.gameTime = 0;
this.particles = [];
this.backgroundObjects = [];
this.animationFrame = 0;
this.player = {
x: 100,
y: 100,
width: 14 * PIXEL_SIZE, // Based on sprite width
height: 11 * PIXEL_SIZE, // Based on sprite height
velocityY: 0,
isJumping: false,
currentLayer: TIME_LAYERS.PRESENT,
echoes: [],
maxEchoes: MAX_ECHOES,
direction: 1,
state: 'idle',
animationTimer: 0
};
this.platforms = [];
this.timeControls = {
isRecording: false,
isPlaying: false,
currentRecording: null,
recordDuration: RECORD_DURATION
};
this.setupControls();
this.createPlatforms();
this.createBackground();
this.animate();
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
setupControls() {
document.addEventListener('keydown', (e) => {
this.keys[e.key] = true;
if (e.key === 'r' && !this.timeControls.isRecording && this.player.echoes.length < this.player.maxEchoes) {
this.startRecording();
} else if (e.key === 'r' && this.timeControls.isRecording) {
this.stopRecording();
}
if (e.key === 'p' && !this.timeControls.isPlaying && this.timeControls.currentRecording) {
this.playRecording();
}
if (e.key === '1') this.switchTimeLayer(TIME_LAYERS.PAST);
if (e.key === '2') this.switchTimeLayer(TIME_LAYERS.PRESENT);
if (e.key === '3') this.switchTimeLayer(TIME_LAYERS.FUTURE);
if (e.key === ' ' && !this.player.isJumping) {
this.player.velocityY = JUMP_FORCE;
this.player.isJumping = true;
this.player.state = 'jump';
}
});
document.addEventListener('keyup', (e) => {
this.keys[e.key] = false;
});
}
switchTimeLayer(layer) {
this.player.currentLayer = layer;
document.getElementById('time-layer').textContent = layer.name;
// Time shift effect
for (let i = 0; i < 50; i++) {
this.particles.push({
x: Math.random() * this.canvas.width,
y: Math.random() * this.canvas.height,
size: Math.random() * 3 + 1,
color: layer.particleColor,
velocityX: (Math.random() - 0.5) * 5,
velocityY: (Math.random() - 0.5) * 5,
life: 30 + Math.random() * 30
});
}
}
startRecording() {
this.timeControls.isRecording = true;
this.timeControls.currentRecording = {
frames: [],
startTime: this.gameTime
};
document.getElementById('status').textContent = 'RECORDING';
}
stopRecording() {
this.timeControls.isRecording = false;
document.getElementById('status').textContent = 'READY';
}
playRecording() {
if (!this.timeControls.currentRecording || this.timeControls.currentRecording.frames.length === 0) return;
const echo = {
frames: [...this.timeControls.currentRecording.frames],
currentFrame: 0,
x: this.player.x,
y: this.player.y,
width: this.player.width,
height: this.player.height,
direction: this.player.direction,
layer: this.player.currentLayer,
isActive: true,
opacity: 0.3 + (0.7 / this.player.maxEchoes * (this.player.maxEchoes - this.player.echoes.length))
};
this.player.echoes.push(echo);
document.getElementById('echo-count').textContent = `${this.player.echoes.length}/${this.player.maxEchoes}`;
document.getElementById('status').textContent = 'ECHO ACTIVE';
// Clear recording after playing
this.timeControls.currentRecording.frames = [];
}
createPlatforms() {
// Base platform
this.platforms.push({
x: 0,
y: this.canvas.height - 30,
width: this.canvas.width,
height: 30,
type: 'base',
visibleIn: 'all'
});
// Time-specific platforms
this.platforms.push({
x: 200,
y: 400,
width: 100,
height: 10,
type: 'past',
visibleIn: 'past'
});
this.platforms.push({
x: 400,
y: 300,
width: 100,
height: 10,
type: 'present',
visibleIn: 'present'
});
this.platforms.push({
x: 600,
y: 200,
width: 100,
height: 10,
type: 'future',
visibleIn: 'future'
});
// Disappearing platforms
this.platforms.push({
x: 300,
y: 250,
width: 80,
height: 10,
type: 'past',
visibleIn: 'past',
isTemporary: true,
timer: 0,
visible: true
});
// Moving platform
this.platforms.push({
x: 500,
y: 350,
width: 80,
height: 10,
type: 'future',
visibleIn: 'future',
isMoving: true,
startX: 500,
endX: 700,
speed: 1.5
});
// Pressure plate
this.platforms.push({
x: 700,
y: 450,
width: 40,
height: 5,
type: 'present',
isTrigger: true,
triggered: false,
visibleIn: 'all'
});
}
createBackground() {
// Create parallax background elements
for (let i = 0; i < 20; i++) {
this.backgroundObjects.push({
x: Math.random() * this.canvas.width,
y: Math.random() * this.canvas.height,
size: Math.random() * 3 + 1,
color: '#4a4a6e',
speed: Math.random() * 0.2 + 0.1
});
}
}
updatePlayer() {
// Horizontal movement
let moving = false;
if (this.keys['ArrowLeft']) {
this.player.x -= MOVE_SPEED;
this.player.direction = -1;
moving = true;
}
if (this.keys['ArrowRight']) {
this.player.x += MOVE_SPEED;
this.player.direction = 1;
moving = true;
}
// Update player state
if (this.player.isJumping) {
this.player.state = 'jump';
} else if (moving) {
this.player.state = 'run';
this.player.animationTimer++;
} else {
this.player.state = 'idle';
}
// Apply gravity
this.player.velocityY += GRAVITY;
this.player.y += this.player.velocityY;
// Check platform collisions
let onGround = false;
for (const platform of this.platforms) {
if (platform.visibleIn !== 'all' && platform.visibleIn !== this.player.currentLayer.name.toLowerCase()) continue;
if (platform.isTemporary && !platform.visible) continue;
if (this.player.x < platform.x + platform.width &&
this.player.x + this.player.width > platform.x &&
this.player.y < platform.y + platform.height &&
this.player.y + this.player.height > platform.y) {
// Landing on top of platform
if (this.player.velocityY > 0 && this.player.y + this.player.height < platform.y + platform.height / 2) {
this.player.y = platform.y - this.player.height;
this.player.velocityY = 0;
this.player.isJumping = false;
onGround = true;
// Footstep particles
for (let i = 0; i < 5; i++) {
this.particles.push({
x: this.player.x + Math.random() * this.player.width,
y: this.player.y + this.player.height,
size: Math.random() * 2 + 1,
color: this.player.currentLayer.particleColor,
velocityX: (Math.random() - 0.5) * 2,
velocityY: Math.random() * 2,
life: 20 + Math.random() * 10
});
}
// Trigger pressure plates
if (platform.isTrigger && !platform.triggered) {
platform.triggered = true;
// Trigger effect
for (let i = 0; i < 20; i++) {
this.particles.push({
x: platform.x + platform.width / 2,
y: platform.y,
size: Math.random() * 3 + 1,
color: '#EF476F',
velocityX: (Math.random() - 0.5) * 5,
velocityY: -Math.random() * 5,
life: 30 + Math.random() * 20
});
}
}
}
}
}
// Screen boundaries
if (this.player.x < 0) this.player.x = 0;
if (this.player.x + this.player.width > this.canvas.width) {
this.player.x = this.canvas.width - this.player.width;
}
// Reset if player falls off screen
if (this.player.y > this.canvas.height) {
this.player.x = 100;
this.player.y = 100;
this.player.velocityY = 0;
// Death effect
for (let i = 0; i < 50; i++) {
this.particles.push({
x: this.player.x + this.player.width / 2,
y: this.player.y + this.player.height / 2,
size: Math.random() * 4 + 2,
color: '#FF6B6B',
velocityX: (Math.random() - 0.5) * 10,
velocityY: (Math.random() - 0.5) * 10,
life: 40 + Math.random() * 30
});
}
}
// Update recording if active
if (this.timeControls.isRecording) {
this.timeControls.currentRecording.frames.push({
x: this.player.x,
y: this.player.y,
velocityY: this.player.velocityY,
isJumping: this.player.isJumping,
direction: this.player.direction,
state: this.player.state,
time: this.gameTime
*_