xxxxxxxxxx
417
// sketch.js - Typographic Runner (Adjusted Difficulty + Character Choice)
// --- Global Game Variables ---
let playerA; // Holds the player object
let obstacles = []; // Array for obstacle objects
let skyline = []; // Array for background elements
let score = 0;
let gameSpeed; // Current speed of the game
let gravity = 0.7;
let jumpPower = -12;
let groundY; // Y-coordinate of the ground
let gameOver = false;
let gameStarted = false;
let playerLetter = 'A'; // Default player character, can be changed
// --- Constants ---
const PLAYER_SIZE = 50;
const OBSTACLE_SIZE = 65;
const PLAYER_WIDTH = PLAYER_SIZE * 0.6;
const PLAYER_HEIGHT = PLAYER_SIZE * 0.8;
const OBSTACLE_WIDTH = OBSTACLE_SIZE * 0.6;
const OBSTACLE_HEIGHT = OBSTACLE_SIZE * 0.8;
const FONT = 'Courier New';
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); // Full alphabet array
// --- Difficulty Settings ---
const INITIAL_GAME_SPEED = 5;
const MAX_GAME_SPEED = 15;
const SPEED_INCREMENT = 0.0015;
const INITIAL_SPAWN_RATE_MIN = 90;
const INITIAL_SPAWN_RATE_MAX = 170;
const MIN_OBSTACLE_DISTANCE_BASE = 200;
const MIN_OBSTACLE_DISTANCE_SCALE = 18;
// ==============================
// --- P5.js Core Functions ---
// ==============================
function setup() {
createCanvas(windowWidth > 800 ? 800 : windowWidth, 400);
groundY = height - 50;
textFont(FONT);
textAlign(CENTER, CENTER);
textSize(20);
// Create initial skyline elements
for (let i = 0; i < 7; i++) {
skyline.push(new SkylineElement(random(width, width * 2)));
}
// Player object is created in restartGame() when game starts/restarts
}
function draw() {
background(240, 240, 240);
// --- Ground ---
fill(50);
noStroke();
rect(0, groundY, width, height - groundY);
// --- Skyline ---
drawSkyline();
// --- Game State Screens ---
if (!gameStarted) {
showStartScreen();
return; // Stop further drawing until game starts
}
if (gameOver) {
showGameOverScreen();
return; // Stop game logic drawing
}
// --- Active Game Logic ---
handleObstacles();
playerA.update();
playerA.show();
updateScore();
displayScore();
// Increase game speed gradually up to the maximum
if (gameSpeed < MAX_GAME_SPEED) {
gameSpeed += SPEED_INCREMENT;
}
gameSpeed = min(gameSpeed, MAX_GAME_SPEED); // Ensure cap
}
// ==============================
// --- Input Handling ---
// ==============================
function keyPressed() {
// --- Character Selection on Start Screen ---
if (!gameStarted) {
// Check if the key pressed is an uppercase letter
if (key.length === 1 && key >= 'A' && key <= 'Z') {
playerLetter = key.toUpperCase(); // Update chosen letter
}
// Check if the key pressed is a lowercase letter
else if (key.length === 1 && key >= 'a' && key <= 'z') {
playerLetter = key.toUpperCase(); // Update chosen letter (convert to uppercase)
}
// Start game with SPACE or ENTER after potentially choosing a letter
else if (key === ' ' || keyCode === ENTER) {
startGame();
}
}
// --- Jumping in Game ---
else if (!gameOver && (key === ' ' || keyCode === UP_ARROW)) {
handleInput();
}
// --- Restarting Game Over ---
else if (gameOver && keyCode === ENTER) {
restartGame();
}
}
function mousePressed() {
// --- Start Game on Click ---
if (!gameStarted) {
startGame();
}
// --- Jumping in Game ---
else if (!gameOver) {
handleInput();
}
// --- Restarting Game Over ---
else { // (gameOver is true)
restartGame();
}
}
// Central input handler for jump action
function handleInput() {
if (playerA) { // Ensure player object exists
playerA.jump();
}
}
// ==============================
// --- Game State Management ---
// ==============================
function startGame() {
gameStarted = true;
restartGame(); // Initialize all game variables for a fresh start
}
function showStartScreen() {
fill(0);
textSize(40);
text("TYPO RUNNER", width / 2, height / 2 - 60); // Moved up slightly
textSize(18);
text(`Press A-Z to choose your character`, width / 2, height / 2 - 10);
textSize(22);
fill(0, 100, 200); // Highlight selected character
text(`Selected: ${playerLetter}`, width / 2, height / 2 + 20);
fill(0);
textSize(18);
text("Press SPACE or Click to Start & Jump", width / 2, height / 2 + 60);
}
function showGameOverScreen() {
fill(100, 100, 100, 180);
rect(0, 0, width, height);
fill(20);
textSize(50);
text("GAME OVER", width / 2, height / 2 - 40);
textSize(25);
text(`Score: ${floor(score)}`, width / 2, height / 2 + 10);
textSize(18);
text("Press ENTER or Click to Restart", width / 2, height / 2 + 50);
}
// Resets game variables to start a new round
function restartGame() {
obstacles = [];
score = 0;
gameSpeed = INITIAL_GAME_SPEED;
// Create the player with the currently selected letter
playerA = new Player(playerLetter);
gameOver = false;
gameStarted = true; // Ensure state is active
frameCount = 0; // Reset frameCount for consistent initial spawn timing
loop(); // Ensure draw loop is running
}
// ==============================
// --- Game Elements Update & Display ---
// ==============================
function updateScore() {
score += (gameSpeed / INITIAL_GAME_SPEED) * 0.1;
}
function displayScore() {
fill(0);
textSize(20);
textAlign(RIGHT, TOP);
text(`Score: ${floor(score)}`, width - 20, 20);
textAlign(CENTER, CENTER); // Reset default
}
function handleObstacles() {
// --- Spawning Logic ---
let spawnCheckInterval = floor(random(INITIAL_SPAWN_RATE_MIN, INITIAL_SPAWN_RATE_MAX) / (gameSpeed / INITIAL_GAME_SPEED));
spawnCheckInterval = max(20, spawnCheckInterval); // Prevent interval from being too low
if (frameCount > 40 && frameCount % spawnCheckInterval === 0) { // Small delay at game start
// --- Minimum Distance Check ---
let minDistance = MIN_OBSTACLE_DISTANCE_BASE + gameSpeed * MIN_OBSTACLE_DISTANCE_SCALE;
let safeToSpawn = true;
if (obstacles.length > 0) {
let lastObstacle = obstacles[obstacles.length - 1];
let spawnX = width + OBSTACLE_WIDTH / 2;
if (spawnX - lastObstacle.x < minDistance) {
safeToSpawn = false;
}
}
if (safeToSpawn) {
obstacles.push(new Obstacle()); // Spawn new obstacle
}
}
// --- Update, Show, Check Collision, Remove ---
for (let i = obstacles.length - 1; i >= 0; i--) {
obstacles[i].update();
obstacles[i].show();
if (playerA && playerA.hits(obstacles[i])) {
gameOver = true;
}
if (obstacles[i].isOffscreen()) {
obstacles.splice(i, 1); // Remove obstacles that leave the screen
}
}
}
function drawSkyline() {
if (!gameStarted && skyline.length === 0) return;
for (let i = skyline.length - 1; i >= 0; i--) {
let currentSpeed = (typeof gameSpeed !== 'undefined') ? gameSpeed : 1;
skyline[i].update(currentSpeed);
skyline[i].show();
if (skyline[i].isOffscreen()) {
skyline[i].x = width + random(50, 200);
skyline[i].resetAppearance();
}
}
// Add new elements if needed during gameplay
if (gameStarted && skyline.length < 7 && frameCount % 50 === 0) {
skyline.push(new SkylineElement(width + random(50, 200)));
}
}
// ==============================
// --- Classes ---
// ==============================
// --- Player Class (Chosen Letter) ---
class Player {
constructor(chosenLetter) { // Accepts the chosen letter
this.char = chosenLetter; // Store the character
this.size = PLAYER_SIZE;
this.w = PLAYER_WIDTH;
this.h = PLAYER_HEIGHT;
this.x = 60;
this.baseY = groundY - this.h / 2;
this.y = this.baseY;
this.velocityY = 0;
}
jump() {
if (this.y >= this.baseY - 5) { // Check if near ground
this.velocityY = jumpPower;
}
}
hits(obstacle) {
// AABB collision detection
let playerLeft = this.x - this.w / 2;
let playerRight = this.x + this.w / 2;
let playerTop = this.y - this.h / 2;
let playerBottom = this.y + this.h / 2;
let obsLeft = obstacle.x - obstacle.w / 2;
let obsRight = obstacle.x + obstacle.w / 2;
let obsTop = obstacle.y - obstacle.h / 2;
let obsBottom = obstacle.y + obstacle.h / 2;
return (
playerRight > obsLeft &&
playerLeft < obsRight &&
playerBottom > obsTop &&
playerTop < obsBottom
);
}
update() {
this.velocityY += gravity;
this.y += this.velocityY;
// Prevent falling through ground
if (this.y >= this.baseY) {
this.y = this.baseY;
this.velocityY = 0;
}
}
show() {
fill(0, 100, 200); // Player color
textSize(this.size);
textStyle(BOLD);
// Draw the stored character
text(this.char, this.x, this.y);
textStyle(NORMAL);
}
}
// --- Obstacle Class (Letters excluding Player's) ---
class Obstacle {
constructor(startX = width + OBSTACLE_WIDTH / 2) {
this.size = OBSTACLE_SIZE;
this.w = OBSTACLE_WIDTH;
this.h = OBSTACLE_HEIGHT;
this.x = startX;
this.y = groundY - this.h / 2;
// --- Choose a letter EXCLUDING the player's letter ---
let possibleObstacles = ALPHABET.filter(char => char !== playerLetter.toUpperCase());
if (possibleObstacles.length > 0) {
this.letter = random(possibleObstacles); // Pick from filtered list
} else {
this.letter = 'X'; // Fallback just in case filter fails
}
// --- End of letter choosing ---
}
update() {
this.x -= gameSpeed;
}
isOffscreen() {
return this.x < -this.w / 2;
}
show() {
fill(30); // Obstacle color
textSize(this.size);
textStyle(BOLD);
text(this.letter, this.x, this.y);
textStyle(NORMAL);
}
}
// --- Skyline Element Class (Letters/Words) ---
class SkylineElement {
constructor(startX) {
this.x = startX;
this.speedFactor = random(0.1, 0.4);
this.resetAppearance();
}
resetAppearance() {
this.isWord = random(1) < 0.3;
this.txtSize = random(15, 45);
this.h = this.txtSize;
if (this.isWord) {
this.text = random(['UP', 'BIG', 'TYPO', 'CITY', 'HIGH', 'RUN']);
this.w = (this.text.length * this.txtSize * 0.6);
} else {
// Use any letter for background, including player's potentially
this.text = random(ALPHABET);
this.w = this.txtSize * 0.6;
}
this.y = groundY - random(5, 150) - this.h / 2;
this.color = color(180, 180, 180, 150);
}
update(currentSpeed) {
this.x -= currentSpeed * this.speedFactor;
}
isOffscreen() {
return this.x < -this.w;
}
show() {
fill(this.color);
textSize(this.txtSize);
noStroke();
text(this.text, this.x, this.y);
}
}
// ==============================
// --- Utility Functions ---
// ==============================
// Adjust canvas size on window resize
function windowResized() {
resizeCanvas(windowWidth > 800 ? 800 : windowWidth, 400);
groundY = height - 50;
// Adjust player base Y if game is running
if (playerA) {
playerA.baseY = groundY - playerA.h / 2;
// Prevent player from falling through ground immediately on resize
playerA.y = min(playerA.y, playerA.baseY);
}
}