Certainly! Let's create a game called "Servant of Evil Goblins" where the player controls a servant tasked with cleaning up dirt left by goblins in a castle. The game has two possible endings:

Peaceful Ending: The servant successfully cleans the castle without reaching maximum rage by avoiding the goblins.
Combat Ending: The servant's rage reaches 100, and he retaliates by defeating the goblins with fireballs.
We'll develop this game using HTML5 Canvas and JavaScript. This approach allows the game to run in a web browser without the need for additional plugins.

Table of Contents
Game Overview
Technologies Used
Game Features
Development Steps
1. Set Up the HTML Structure
2. Style the Game with CSS
3. Game Assets
4. Initialize the Canvas and Game Loop
5. Create Game Objects
Servant (Player)
Goblins (Enemies)
Dirt (Mess to Clean)
Fireballs (Projectiles)
6. Implement Game Mechanics
Movement and Controls
Goblins' Movement
Rage Meter
Collision Detection
Cleaning Mechanism
Combat Mode
7. Game States and Endings
8. Main Game Loop
9. Putting It All Together
Final Notes
Running the Game
Possible Enhancements
Game Overview
The player controls the Servant, who must clean up dirt left behind by 10 Goblins in a castle. The Goblins move around randomly, leaving trails of dirt. When the Servant encounters a Goblin, his Rage Meter increases. If it reaches 100, the Servant enters Combat Mode, shooting fireballs at Goblins until they are all defeated.

Technologies Used
HTML5 Canvas: For rendering the game visuals.
JavaScript: For game logic and interactivity.
CSS: For styling the game elements.
Game Features
Player Movement: Move the servant using keyboard controls (arrow keys or WASD).
Rage Meter: Increases when the servant encounters goblins.
Goblins: Move around the map, leaving dirt trails.
Cleaning Mechanism: Clean dirt by moving over it.
Combat Mode: Activate when rage reaches 100, allowing the servant to fire projectiles at goblins.
Two Endings:
Peaceful: Clean all dirt without rage reaching 100.
Combat: Defeat all goblins after rage reaches 100.
Development Steps
1. Set Up the HTML Structure
Create an index.html file with a canvas element and a container for the rage meter.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Servant of Evil Goblins</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div id="rageMeterContainer">
<div id="rageMeter"></div>
</div>
<script src="game.js"></script>
</body>
</html>
2. Style the Game with CSS
Create a styles.css file to style the game canvas and rage meter.

body {
margin: 0;
overflow: hidden;
background-color: #2c3e50;
}

#gameCanvas {
display: block;
background-color: #34495e;
}

#rageMeterContainer {
position: absolute;
top: 10px;
left: 10px;
width: 200px;
height: 20px;
border: 2px solid #ecf0f1;
}

#rageMeter {
width: 0%;
height: 100%;
background-color: #e74c3c;
}
3. Game Assets
For simplicity, we'll represent the servant, goblins, dirt, and fireballs using basic shapes:

Servant: Blue rectangle.
Goblins: Green rectangles.
Dirt: Brown circles.
Fireballs: Orange circles.
You can replace these with images later if desired.

4. Initialize the Canvas and Game Loop
Create a game.js file to initialize the game.

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

const canvasWidth = canvas.width;
const canvasHeight = canvas.height;

let gameRunning = true;

function gameLoop() {
if (gameRunning) {
update();
draw();
requestAnimationFrame(gameLoop);
}
}
5. Create Game Objects
Servant (Player)

const servant = {
x: canvasWidth / 2,
y: canvasHeight / 2,
width: 20,
height: 20,
speed: 2,
color: 'blue',
rage: 0,
};
Goblins (Enemies)

const goblins = [];
const goblinCount = 10;

for (let i = 0; i < goblinCount; i++) {
goblins.push({
x: Math.random() * canvasWidth,
y: Math.random() * canvasHeight,
width: 20,
height: 20,
speed: 1,
color: 'green',
dirX: Math.random() < 0.5 ? 1 : -1,
dirY: Math.random() < 0.5 ? 1 : -1,
});
}
Dirt (Mess to Clean)
We'll store dirt in an array. Each dirt spot has an x and y.

const dirts = [];
Fireballs (Projectiles)

const fireballs = [];
6. Implement Game Mechanics
Movement and Controls
Add event listeners for keyboard input.

const keys = {};

window.addEventListener('keydown', function(e) {
keys[e.key] = true;
});

window.addEventListener('keyup', function(e) {
keys[e.key] = false;
});

function moveServant() {
if (keys['ArrowUp'] || keys['w']) servant.y -= servant.speed;
if (keys['ArrowDown'] || keys['s']) servant.y += servant.speed;
if (keys['ArrowLeft'] || keys['a']) servant.x -= servant.speed;
if (keys['ArrowRight'] || keys['d']) servant.x += servant.speed;

// Keep servant within boundaries
servant.x = Math.max(0, Math.min(servant.x, canvasWidth - servant.width));
servant.y = Math.max(0, Math.min(servant.y, canvasHeight - servant.height));
}
Goblins' Movement
Goblins move randomly and leave dirt trails.

function moveGoblins() {
goblins.forEach(goblin => {
goblin.x += goblin.speed * goblin.dirX;
goblin.y += goblin.speed * goblin.dirY;

// Change direction on collision with walls
if (goblin.x <= 0 || goblin.x >= canvasWidth - goblin.width)
goblin.dirX *= -1;
if (goblin.y <= 0 || goblin.y >= canvasHeight - goblin.height)
goblin.dirY *= -1;

// Leave dirt
if (Math.random() < 0.05) {
dirts.push({ x: goblin.x + goblin.width / 2, y: goblin.y + goblin.height / 2 });
}
});
}
Rage Meter
Increase rage when the servant meets a goblin.

function checkServantGoblinCollision() {
goblins.forEach(goblin => {
if (isColliding(servant, goblin)) {
servant.rage += 1;
updateRageMeter();
}
});
}

function updateRageMeter() {
const ragePercentage = Math.min(servant.rage, 100);
const rageMeter = document.getElementById('rageMeter');
rageMeter.style.width = '${ragePercentage * 2}px'; // Since container is 200px
}
Collision Detection
Utility function for rectangle collision.

function isColliding(rect1, rect2) {
return rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y;
}
Cleaning Mechanism
Remove dirt when the servant moves over it.

function cleanDirt() {
for (let i = dirts.length - 1; i >= 0; i--) {
const dirt = dirts[i];
const dx = servant.x + servant.width / 2 - dirt.x;
const dy = servant.y + servant.height / 2 - dirt.y;
const distance = Math.sqrt(dx * dx + dy * dy);

if (distance < servant.width / 2) {
dirts.splice(i, 1);
}
}
}
Combat Mode
When rage reaches 100, the servant can shoot fireballs.

let combatMode = false;

function checkCombatMode() {
if (servant.rage >= 100) {
combatMode = true;
}
}

window.addEventListener('keydown', function(e) {
if (combatMode && e.key === ' ') {
fireballs.push({
x: servant.x + servant.width / 2,
y: servant.y + servant.height / 2,
radius: 5,
speed: 5,
dirX: 0,
dirY: 0,
});

// Set direction based on last movement
if (keys['ArrowUp'] || keys['w']) {
fireballs[fireballs.length - 1].dirY = -1;
} else if (keys['ArrowDown'] || keys['s']) {
fireballs[fireballs.length - 1].dirY = 1;
} else if (keys['ArrowLeft'] || keys['a']) {
fireballs[fireballs.length - 1].dirX = -1;
} else if (keys['ArrowRight'] || keys['d']) {
fireballs[fireballs.length - 1].dirX = 1;
} else {
fireballs[fireballs.length - 1].dirY = -1; // Default upwards
}
}
});

function moveFireballs() {
for (let i = fireballs.length - 1; i >= 0; i--) {
const fb = fireballs[i];
fb.x += fb.speed * fb.dirX;
fb.y += fb.speed * fb.dirY;

// Remove fireball if it goes off-screen
if (fb.x < 0 || fb.x > canvasWidth || fb.y < 0 || fb.y > canvasHeight) {
fireballs.splice(i, 1);
continue;
}

// Check collision with goblins
for (let j = goblins.length - 1; j >= 0; j--) {
const goblin = goblins[j];
if (circleRectCollision(fb, goblin)) {
goblins.splice(j, 1);
fireballs.splice(i, 1);
break;
}
}
}
}

function circleRectCollision(circle, rect) {
const distX = Math.abs(circle.x - rect.x - rect.width / 2);
const distY = Math.abs(circle.y - rect.y - rect.height / 2);

if (distX > (rect.width / 2 + circle.radius)) return false;
if (distY > (rect.height / 2 + circle.radius)) return false;

if (distX <= (rect.width / 2)) return true;
if (distY <= (rect.height / 2)) return true;

const dx = distX - rect.width / 2;
const dy = distY - rect.height / 2;
return dx * dx + dy * dy <= (circle.radius * circle.radius);
}
7. Game States and Endings
Add logic to check for game ending conditions.

function checkGameEnd() {
if (!combatMode && dirts.length === 0) {
// Peaceful ending
gameRunning = false;
alert('You have successfully cleaned the castle! Peaceful Ending Achieved.');
} else if (combatMode && goblins.length === 0) {
// Combat ending
gameRunning = false;
alert('You have defeated all the goblins! Combat Ending Achieved.');
}
}
8. Main Game Loop
Implement the update and draw functions.

function update() {
moveServant();
moveGoblins();
moveFireballs();
cleanDirt();
checkServantGoblinCollision();
checkCombatMode();
checkGameEnd();
}

function draw() {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);

// Draw servant
ctx.fillStyle = servant.color;
ctx.fillRect(servant.x, servant.y, servant.width, servant.height);

// Draw goblins
goblins.forEach(goblin => {
ctx.fillStyle = goblin.color;
ctx.fillRect(goblin.x, goblin.y, goblin.width, goblin.height);
});

// Draw dirts
dirts.forEach(dirt => {
ctx.fillStyle = 'brown';
ctx.beginPath();
ctx.arc(dirt.x, dirt.y, 5, 0, Math.PI * 2);
ctx.fill();
});

// Draw fireballs
fireballs.forEach(fireball => {
ctx.fillStyle = 'orange';
ctx.beginPath();
ctx.arc(fireball.x, fireball.y, fireball.radius, 0, Math.PI * 2);
ctx.fill();
});
}
9. Putting It All Together
Here's the complete game.js file:

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

const canvasWidth = canvas.width;
const canvasHeight = canvas.height;

let gameRunning = true;

const servant = {
x: canvasWidth / 2,
y: canvasHeight / 2,
width: 20,
height: 20,
speed: 2,
color: 'blue',
rage: 0,
};

const goblins = [];
const goblinCount = 10;

for (let i = 0; i < goblinCount; i++) {
goblins.push({
x: Math.random() * (canvasWidth - 20),
y: Math.random() * (canvasHeight - 20),
width: 20,
height: 20,
speed: 1.5,
color: 'green',
dirX: Math.random() < 0.5 ? 1 : -1,
dirY: Math.random() < 0.5 ? 1 : -1,
});
}

const dirts = [];
const fireballs = [];
let combatMode = false;

const keys = {};

window.addEventListener('keydown', function(e) {
keys[e.key] = true;

if (combatMode && e.key === ' ') {
fireballs.push({
x: servant.x + servant.width / 2,
y: servant.y + servant.height / 2,
radius: 5,
speed: 5,
dirX: 0,
dirY: 0,
});

// Set direction based on last movement or default to upward
if (keys['ArrowUp'] || keys['w']) {
fireballs[fireballs.length - 1].dirY = -1;
} else if (keys['ArrowDown'] || keys['s']) {
fireballs[fireballs.length - 1].dirY = 1;
} else if (keys['ArrowLeft'] || keys['a']) {
fireballs[fireballs.length - 1].dirX = -1;
} else if (keys['ArrowRight'] || keys['d']) {
fireballs[fireballs.length - 1].dirX = 1;
} else {
fireballs[fireballs.length - 1].dirY = -1;
}
}
});

window.addEventListener('keyup', function(e) {
keys[e.key] = false;
});

function moveServant() {
if (keys['ArrowUp'] || keys['w']) servant.y -= servant.speed;
if (keys['ArrowDown'] || keys['s']) servant.y += servant.speed;
if (keys['ArrowLeft'] || keys['a']) servant.x -= servant.speed;
if (keys['ArrowRight'] || keys['d']) servant.x += servant.speed;

// Keep servant within boundaries
servant.x = Math.max(0, Math.min(servant.x, canvasWidth - servant.width));
servant.y = Math.max(0, Math.min(servant.y, canvasHeight - servant.height));
}

function moveGoblins() {
goblins.forEach(goblin => {
goblin.x += goblin.speed * goblin.dirX;
goblin.y += goblin.speed * goblin.dirY;

// Change direction on collision with walls
if (goblin.x <= 0 || goblin.x >= canvasWidth - goblin.width)
goblin.dirX *= -1;
if (goblin.y <= 0 || goblin.y >= canvasHeight - goblin.height)
goblin.dirY *= -1;

// Leave dirt
if (Math.random() < 0.02) {
dirts.push({ x: goblin.x + goblin.width / 2, y: goblin.y + goblin.height / 2 });
}
});
}

function moveFireballs() {
for (let i = fireballs.length - 1; i >= 0; i--) {
const fb = fireballs[i];
fb.x += fb.speed * fb.dirX;
fb.y += fb.speed * fb.dirY;

// Remove fireball if it goes off-screen
if (fb.x < 0 || fb.x > canvasWidth || fb.y < 0 || fb.y > canvasHeight) {
fireballs.splice(i, 1);
continue;
}

// Check collision with goblins
for (let j = goblins.length - 1; j >= 0; j--) {
const goblin = goblins[j];
if (circleRectCollision(fb, goblin)) {
goblins.splice(j, 1);
fireballs.splice(i, 1);
break;
}
}
}
}

function cleanDirt() {
for (let i = dirts.length - 1; i >= 0; i--) {
const dirt = dirts[i];
const dx = servant.x + servant.width / 2 - dirt.x;
const dy = servant.y + servant.height / 2 - dirt.y;
const distance = Math.sqrt(dx * dx + dy * dy);

if (distance < servant.width / 2) {
dirts.splice(i, 1);
}
}
}

function checkServantGoblinCollision() {
goblins.forEach(goblin => {
if (isColliding(servant, goblin)) {
servant.rage += 1;
updateRageMeter();
}
});
}

function checkCombatMode() {
if (servant.rage >= 100) {
combatMode = true;
}
}

function checkGameEnd() {
if (!combatMode && dirts.length === 0) {
// Peaceful ending
gameRunning = false;
alert('You have successfully cleaned the castle! Peaceful Ending Achieved.');
} else if (combatMode && goblins.length === 0) {
// Combat ending
gameRunning = false;
alert('You have defeated all the goblins! Combat Ending Achieved.');
}
}

function updateRageMeter() {
const ragePercentage = Math.min(servant.rage, 100);
const rageMeter = document.getElementById('rageMeter');
rageMeter.style.width = '${ragePercentage * 2}px';
}

function isColliding(rect1, rect2) {
return rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y;
}

function circleRectCollision(circle, rect) {
const distX = Math.abs(circle.x - rect.x - rect.width / 2);
const distY = Math.abs(circle.y - rect.y - rect.height / 2);

if (distX > (rect.width / 2 + circle.radius)) return false;
if (distY > (rect.height / 2 + circle.radius)) return false;

if (distX <= (rect.width / 2)) return true;
if (distY <= (rect.height / 2)) return true;

const dx = distX - rect.width / 2;
const dy = distY - rect.height / 2;
return dx * dx + dy * dy <= (circle.radius * circle.radius);
}

function update() {
moveServant();
moveGoblins();
moveFireballs();
cleanDirt();
checkServantGoblinCollision();
checkCombatMode();
checkGameEnd();
}

function draw() {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);

// Draw servant
ctx.fillStyle = servant.color;
ctx.fillRect(servant.x, servant.y, servant.width, servant.height);

// Draw goblins
goblins.forEach(goblin => {
ctx.fillStyle = goblin.color;
ctx.fillRect(goblin.x, goblin.y, goblin.width, goblin.height);
});

// Draw dirts
dirts.forEach(dirt => {
ctx.fillStyle = 'brown';
ctx.beginPath();
ctx.arc(dirt.x, dirt.y, 5, 0, Math.PI * 2);
ctx.fill();
});

// Draw fireballs
fireballs.forEach(fireball => {
ctx.fillStyle = 'orange';
ctx.beginPath();
ctx.arc(fireball.x, fireball.y, fireball.radius, 0, Math.PI * 2);
ctx.fill();
});
}

function gameLoop() {
if (gameRunning) {
update();
draw();
requestAnimationFrame(gameLoop);
}
}

gameLoop();
Final Notes
Game Balance: Adjust the speeds, rates, and thresholds to balance the game's difficulty.
Graphics: Replace the basic shapes with images or sprites to enhance visual appeal.
Sound Effects: Add sounds for cleaning, goblin taunts, fireballs, and game endings.
User Interface: Improve the rage meter and add additional HUD elements if needed.
Collision Optimization: For better performance, especially with more objects, consider spatial partitioning techniques.
Running the Game
Create the files: index.html, styles.css, and game.js as shown above.
Open index.html in a modern web browser.
Play the game:
Move the servant using the arrow keys or WASD.
Avoid goblins to keep rage low.
Clean dirt by moving over it.
Engage in combat after rage reaches 100 by pressing spacebar to shoot fireballs.
Achieve one of the endings based on your actions.
Possible Enhancements
Levels: Introduce multiple levels with increasing difficulty.
Power-ups: Add items that reduce rage or enhance cleaning speed.
Goblin Behavior: Make goblins react to the servant or have different movement patterns.
Obstacles: Include obstacles in the castle that the servant and goblins must navigate around.
Mobile Support: Add touch controls for playing on mobile devices.
Storyline: Incorporate a narrative explaining why the servant is cleaning and why goblins mock him.
By following these steps, you've created a simple yet engaging game that meets the requested features. Feel free to expand and customize the game to make it more fun and challenging!

Happy coding! If you need further assistance or have questions about specific parts of the code, feel free to ask.

There is an error. When the game starts, it immediately says "You have successfully cleaned the castle! Peaceful Ending Achieved." Fix it!