Cursed conquest aka JJK game

This is a Jujutsu Kaisen-inspired 1v1 fighting game where two players battle in an arena. Players control characters that can move, jump, attack, and use special abilities (Domain Expansion) to defeat their opponent before their health runs out.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make gravity stronger
  2. Boost attack damage
  3. Start players closer together
  4. Give players more health
  5. Make jumping higher
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a fully playable 1v1 fighting game inspired by Jujutsu Kaisen with two characters battling in an interactive arena. The visuals feature health bars, special attack animations, and domain expansion effects powered by p5.js drawing functions, collision detection, and real-time physics. It combines game state management, keyboard input handling, and sprite-like character rendering to create a complete interactive experience.

The code is organized into a setup() function that initializes the canvas and game state, a draw() function that updates physics and renders each frame, and separate class definitions for Player characters and special effects. By studying it, you'll learn how professional game sketches structure player input, collision detection between multiple objects, health systems, and how to layer UI elements (health bars, text) on top of animated game graphics.

⚙️ How It Works

  1. When you click 'ENTER THE VEIL' and select a stage, initArena() creates the canvas, sets the background color, initializes two Player objects (one for each fighter), and displays the HUD with health bars
  2. The draw() function runs 60 times per second, clearing the background and updating both players' physics (gravity, velocity, collision with ground) every frame
  3. Keyboard input (WASD for P1, Arrow Keys for P2) controls movement and jumping; pressing specific keys triggers attacks that deal damage and knockback
  4. When players collide, hit detection checks if one player is attacking and reduces the opponent's health; visual feedback (knockback, hit flashes) plays instantly
  5. Special abilities (Domain Expansion) drain a 'Nurture' meter and trigger full-screen visual effects; the first player to reach 0 HP loses and the winner is displayed on a win screen

🎓 Concepts You'll Learn

Game state managementPhysics simulation (gravity, velocity, collisions)Input handling (keyboard controls)Collision detectionHealth and resource systemsFrame-based animation2D character renderingUI and HUD display

📝 Code Breakdown

setup()

setup() runs once when the game starts. It initializes the canvas size and frame rate. The actual game objects (players) are created later when initArena() is called.

function setup() {
  createCanvas(960, 600);
  frameRate(60);
}
Line-by-line explanation (2 lines)
createCanvas(960, 600);
Creates a 960x600 pixel canvas where the game arena will be drawn
frameRate(60);
Sets the animation to run at 60 frames per second, standard for smooth game animation

draw()

draw() is the heartbeat of the game. It runs 60 times per second and orchestrates everything: physics, rendering, input, collision, and victory conditions. The order matters—update positions before checking collisions, and check victory after all damage is applied.

function draw() {
  if (!gameActive) return;
  background(bgColor);
  
  p1.update();
  p2.update();
  
  p1.display();
  p2.display();
  
  handleInput();
  checkCollisions();
  updateHUD();
  
  if (p1.hp <= 0 || p2.hp <= 0) {
    endGame();
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

conditional Game Active Guard if (!gameActive) return;

Pauses the game loop if gameActive is false, preventing updates during menus

calculation Physics Updates p1.update(); p2.update();

Updates both players' positions, velocities, and applies gravity each frame

calculation Render Characters p1.display(); p2.display();

Draws both characters to the screen at their current positions

calculation Process Input handleInput();

Reads keyboard presses and moves/attacks characters based on player input

calculation Check Collisions checkCollisions();

Tests if attacks are hitting opponents and applies damage/knockback

calculation Update UI updateHUD();

Refreshes health bars and other UI elements on screen

conditional Victory Detection if (p1.hp <= 0 || p2.hp <= 0) { endGame(); }

Ends the game when either player's health reaches zero

if (!gameActive) return;
If the game is not active (in menu), stop running the rest of draw() to pause gameplay
background(bgColor);
Clears the screen each frame with the current stage's background color
p1.update();
Updates player 1's position, applies gravity, handles jumping and falling physics
p2.update();
Updates player 2's position with the same physics calculations
p1.display();
Draws player 1 as a colored rectangle at their current position
p2.display();
Draws player 2 as a colored rectangle at their current position
handleInput();
Reads which keys are pressed and updates each player's direction and actions
checkCollisions();
Tests if player hitboxes overlap and if attacks connect, applying damage if they hit
updateHUD();
Updates the on-screen health bars and resource meters to match current player stats
if (p1.hp <= 0 || p2.hp <= 0) {
Tests if either player has died (health at or below zero)
endGame();
Stops the game and shows the win screen with the victor's name

handleInput()

handleInput() translates raw keyboard events into game actions. It reads p5.js's keyIsPressed and keyCode variables and calls methods on the player objects. This separation keeps input logic clean and reusable—you could swap controllers or add new input types without changing the rest of the game.

function handleInput() {
  const keys = {};
  if (keyIsPressed) {
    if (key === 'w' || key === 'W') keys.w = true;
    if (key === 'a' || key === 'A') keys.a = true;
    if (key === 's' || key === 'S') keys.s = true;
    if (key === 'd' || key === 'D') keys.d = true;
    if (keyCode === 38) keys.up = true;   // arrow up
    if (keyCode === 37) keys.left = true;  // arrow left
    if (keyCode === 40) keys.down = true;  // arrow down
    if (keyCode === 39) keys.right = true; // arrow right
  }
  
  // P1 controls
  if (keys.a) p1.moveLeft();
  if (keys.d) p1.moveRight();
  if (keys.w) p1.jump();
  if (keys.s) p1.attack();
  
  // P2 controls
  if (keys.left) p2.moveLeft();
  if (keys.right) p2.moveRight();
  if (keys.up) p2.jump();
  if (keys.down) p2.attack();
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Key Press Detection if (keyIsPressed) {

Only processes input if a key is currently being pressed

conditional Player 1 Key Mapping if (keys.a) p1.moveLeft();

Maps W/A/S/D keys to player 1's movement and attack actions

conditional Player 2 Key Mapping if (keys.left) p2.moveLeft();

Maps Arrow Keys to player 2's movement and attack actions

const keys = {};
Creates an empty object to store which keys are currently pressed
if (keyIsPressed) {
Only checks for specific keys if ANY key is being pressed (p5.js built-in)
if (key === 'w' || key === 'W') keys.w = true;
Detects if W or w is pressed and marks it in the keys object
if (keyCode === 38) keys.up = true; // arrow up
Uses keyCode (ASCII number) to detect arrow keys, since they aren't regular characters
if (keys.a) p1.moveLeft();
If 'a' was pressed, calls player 1's moveLeft() method to move them left
if (keys.s) p1.attack();
If 's' was pressed, calls player 1's attack() method to perform an attack

checkCollisions()

checkCollisions() is the combat engine. It measures the distance between players and triggers damage when they overlap while attacking. The !p2.isHit flag prevents a single attack from hitting multiple times in one frame—without it, damage would be applied every frame the attacker touched the opponent.

function checkCollisions() {
  const dist = Math.abs(p1.x - p2.x);
  const hitRange = p1.w + p2.w;
  
  if (dist < hitRange) {
    if (p1.isAttacking && !p2.isHit) {
      p2.takeDamage(p1.attackDamage);
      p2.isHit = true;
      p2.vx = -p1.knockbackForce * (p1.direction || 1);
    }
    if (p2.isAttacking && !p1.isHit) {
      p1.takeDamage(p2.attackDamage);
      p1.isHit = true;
      p1.vx = -p2.knockbackForce * (p2.direction || 1);
    }
  }
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

calculation Distance Calculation const dist = Math.abs(p1.x - p2.x);

Calculates the absolute distance between both players' x positions

conditional Hitbox Overlap Check if (dist < hitRange) {

Tests if the distance is small enough for a collision to occur

conditional Player 1 Attacking P2 if (p1.isAttacking && !p2.isHit) {

Checks if player 1 is attacking and player 2 hasn't already been hit this frame

calculation Apply Damage and Knockback p2.takeDamage(p1.attackDamage);

Reduces opponent's health and applies directional knockback force

const dist = Math.abs(p1.x - p2.x);
Uses Math.abs() to get the distance between players, ignoring which is further left or right
const hitRange = p1.w + p2.w;
Sets the collision range as the sum of both players' widths (their bounding boxes touching)
if (dist < hitRange) {
If distance is smaller than the combined width, the players are overlapping
if (p1.isAttacking && !p2.isHit) {
Only deals damage if player 1 is actively attacking AND player 2 hasn't been hit already this frame
p2.takeDamage(p1.attackDamage);
Reduces player 2's HP by the attack damage amount
p2.isHit = true;
Marks player 2 as hit this frame to prevent multiple damage hits from one attack
p2.vx = -p1.knockbackForce * (p1.direction || 1);
Applies horizontal velocity to push player 2 away, in the opposite direction of player 1's facing

updateHUD()

updateHUD() bridges the game logic and HTML UI. It reads the players' numeric stats (hp, nurture) and translates them into visual bar widths on the page. This happens every frame, so the bars update in real-time as damage is taken and abilities are used.

function updateHUD() {
  const p1HPPercent = Math.max(0, (p1.hp / p1.maxHP) * 100);
  const p2HPPercent = Math.max(0, (p2.hp / p2.maxHP) * 100);
  
  document.getElementById('p1-hp').style.width = p1HPPercent + '%';
  document.getElementById('p2-hp').style.width = p2HPPercent + '%';
  document.getElementById('p1-nr').style.width = (p1.nurture / p1.maxNurture) * 100 + '%';
  document.getElementById('p2-nr').style.width = (p2.nurture / p2.maxNurture) * 100 + '%';
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

calculation HP Percentage Calculation const p1HPPercent = Math.max(0, (p1.hp / p1.maxHP) * 100);

Converts current HP to a percentage (0-100) for display, never going below 0

calculation DOM Element Updates document.getElementById('p1-hp').style.width = p1HPPercent + '%';

Updates the HTML health bar width to match the calculated percentage

const p1HPPercent = Math.max(0, (p1.hp / p1.maxHP) * 100);
Divides current HP by max HP, multiplies by 100 to get percentage, and uses Math.max() to ensure it never goes below 0
document.getElementById('p1-hp').style.width = p1HPPercent + '%';
Finds the HTML element with id 'p1-hp' and sets its width CSS to match the HP percentage, making the bar shrink as health decreases
document.getElementById('p1-nr').style.width = (p1.nurture / p1.maxNurture) * 100 + '%';
Does the same calculation for the Nurture (special ability) resource bar

endGame()

endGame() is the victory handler. It stops the game loop, determines the winner, and displays the win screen with appropriate text. The ternary operator (? :) is a compact if/else for assigning values—perfect for picking between two outcomes based on a condition.

function endGame() {
  gameActive = false;
  const winner = p1.hp > 0 ? 'PLAYER 1' : 'PLAYER 2';
  const quote = p1.hp > 0 ? '"I am the strongest."' : '"You were strong."';
  document.getElementById('win-text').textContent = winner + ' HAS PREVAILED';
  document.getElementById('quote').textContent = quote;
  document.getElementById('win-screen').style.display = 'flex';
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

calculation Stop Game Loop gameActive = false;

Sets gameActive to false, which prevents draw() from running game updates

conditional Determine Winner const winner = p1.hp > 0 ? 'PLAYER 1' : 'PLAYER 2';

Uses ternary operator to check which player survived and assign the winner name

calculation Show Win Screen document.getElementById('win-screen').style.display = 'flex';

Makes the win screen HTML element visible by changing its display CSS property

gameActive = false;
Sets the game flag to false, which causes draw() to exit early and stop updating the game
const winner = p1.hp > 0 ? 'PLAYER 1' : 'PLAYER 2';
Uses the ternary operator (? :) to set winner to 'PLAYER 1' if p1 survived, otherwise 'PLAYER 2'
const quote = p1.hp > 0 ? '"I am the strongest."' : '"You were strong."';
Selects a victory quote based on which player won
document.getElementById('win-text').textContent = winner + ' HAS PREVAILED';
Updates the win screen's main text to show which player won
document.getElementById('win-screen').style.display = 'flex';
Makes the win screen visible by setting its CSS display property to 'flex'

initArena(color, name)

initArena() is the game launcher. It receives the selected stage's color and name, sets up the two players with opposite directions (so they face each other), and toggles the visibility of UI elements to hide menus and show the game.

function initArena(color, name) {
  bgColor = color;
  arenaName = name;
  gameActive = true;
  
  p1 = new Player(100, 400, 1, '#ff4444');
  p2 = new Player(860, 400, -1, '#4444ff');
  
  document.getElementById('stage-select').style.display = 'none';
  document.getElementById('menu').style.display = 'none';
  document.getElementById('ui').style.display = 'flex';
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

calculation Arena Configuration bgColor = color;

Stores the background color for the selected stage

calculation Initialize Players p1 = new Player(100, 400, 1, '#ff4444');

Creates player 1 with starting position, direction, and color

calculation Show Game UI document.getElementById('ui').style.display = 'flex';

Makes the HUD visible by changing its CSS display property

bgColor = color;
Stores the background color parameter for this arena
arenaName = name;
Stores the arena name (used for display or future features)
gameActive = true;
Sets the game flag to true, allowing draw() to run game updates
p1 = new Player(100, 400, 1, '#ff4444');
Creates player 1 at x:100, y:400, direction:1 (right), color:red; direction 1 means facing right
p2 = new Player(860, 400, -1, '#4444ff');
Creates player 2 at x:860, y:400, direction:-1 (left), color:blue; starting on the right side facing left
document.getElementById('ui').style.display = 'flex';
Makes the health bars and HUD visible by setting CSS display to 'flex'

showStages()

showStages() is a simple UI controller. It uses document.getElementById() to access HTML elements and toggles their visibility with CSS display properties. This pattern is used throughout the game to swap between screens.

function showStages() {
  document.getElementById('menu').style.display = 'none';
  document.getElementById('stage-select').style.display = 'flex';
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

calculation Hide Main Menu document.getElementById('menu').style.display = 'none';

Hides the main menu by setting display to 'none'

calculation Show Stage Select document.getElementById('stage-select').style.display = 'flex';

Shows the stage select screen by setting display to 'flex'

document.getElementById('menu').style.display = 'none';
Uses DOM selection to find the menu element and hides it by setting display to 'none'
document.getElementById('stage-select').style.display = 'flex';
Finds the stage select element and makes it visible by setting display to 'flex'

class Player()

The Player class is the core of the game. It stores all stats (hp, position, velocity) and methods (movement, attack, damage). The update() method handles physics every frame, while display() renders the player. Each player controls their own data, making the code modular and easy to debug.

class Player {
  constructor(x, y, direction, color) {
    this.x = x;
    this.y = y;
    this.direction = direction;
    this.color = color;
    this.w = 40;
    this.h = 60;
    this.vx = 0;
    this.vy = 0;
    this.hp = 100;
    this.maxHP = 100;
    this.nurture = 0;
    this.maxNurture = 100;
    this.moveSpeed = 5;
    this.jumpPower = -15;
    this.attackDamage = 5;
    this.knockbackForce = 12;
    this.isAttacking = false;
    this.isHit = false;
    this.onGround = false;
  }
  
  update() {
    this.vy += gravity;
    this.x += this.vx;
    this.y += this.vy;
    this.isAttacking = false;
    this.isHit = false;
    
    if (this.y >= height - this.h) {
      this.y = height - this.h;
      this.vy = 0;
      this.onGround = true;
    } else {
      this.onGround = false;
    }
    
    if (this.x < 0) this.x = 0;
    if (this.x > width - this.w) this.x = width - this.w;
  }
  
  display() {
    fill(this.color);
    rect(this.x, this.y, this.w, this.h);
    if (this.isAttacking) {
      stroke('#ffff00');
      strokeWeight(3);
      noFill();
      rect(this.x, this.y, this.w, this.h);
    }
  }
  
  moveLeft() {
    this.vx = -this.moveSpeed;
    this.direction = -1;
  }
  
  moveRight() {
    this.vx = this.moveSpeed;
    this.direction = 1;
  }
  
  jump() {
    if (this.onGround) {
      this.vy = this.jumpPower;
      this.onGround = false;
    }
  }
  
  attack() {
    this.isAttacking = true;
    this.nurture = Math.min(this.maxNurture, this.nurture + 2);
  }
  
  takeDamage(amount) {
    this.hp -= amount;
  }
}
Line-by-line explanation (24 lines)

🔧 Subcomponents:

calculation Constructor (Initialization) constructor(x, y, direction, color) {

Sets up all initial properties when a new Player is created

calculation Physics Update this.vy += gravity;

Applies gravity to vertical velocity each frame

conditional Ground Collision if (this.y >= height - this.h) {

Detects when player hits the ground and stops falling

conditional Canvas Boundaries if (this.x < 0) this.x = 0;

Prevents players from moving off the left or right edges

constructor(x, y, direction, color) {
The constructor method runs when 'new Player()' is called, initializing all properties
this.w = 40;
Sets the player's width to 40 pixels for rendering and collision detection
this.h = 60;
Sets the player's height to 60 pixels
this.hp = 100;
Sets starting health points to 100
this.onGround = false;
Tracks whether the player is touching the ground (needed for jump validation)
this.vy += gravity;
Adds gravity acceleration to vertical velocity each frame, making falling feel natural
this.x += this.vx;
Moves the player horizontally by their velocity
this.y += this.vy;
Moves the player vertically by their velocity (which includes gravity)
this.isAttacking = false;
Resets attacking flag to false each frame so attack only lasts one frame
if (this.y >= height - this.h) {
Checks if player's bottom edge has hit or passed the ground (canvas bottom)
this.y = height - this.h;
Snaps player back to ground level to prevent sinking below
this.vy = 0;
Stops downward velocity so player doesn't keep accelerating into the ground
this.onGround = true;
Marks player as grounded, allowing jumping
if (this.x < 0) this.x = 0;
If player moves left past the edge, snap them back to x=0
fill(this.color);
Sets the fill color to this player's color before drawing
rect(this.x, this.y, this.w, this.h);
Draws the player as a rectangle at their current position with their width and height
if (this.isAttacking) {
If the player is attacking, draws a yellow outline around them to show they're hitting
this.vx = -this.moveSpeed;
Sets horizontal velocity to negative (moving left) when moveLeft() is called
this.direction = -1;
Records that the player is facing left for knockback calculations
if (this.onGround) {
Only allows jumping if the player is touching the ground (prevents double jumping)
this.vy = this.jumpPower;
Sets vertical velocity to a large negative number, creating upward momentum
this.isAttacking = true;
Marks that the player is attacking this frame
this.nurture = Math.min(this.maxNurture, this.nurture + 2);
Increases the Nurture meter by 2 each attack, capping at maxNurture so it doesn't overflow
this.hp -= amount;
Reduces health points by the damage amount received

📦 Key Variables

gameActive boolean

Controls whether the game loop is running - false pauses all game updates (used in menus and after game over)

let gameActive = false;
bgColor string

Stores the current stage's background color as a hex string, selected when entering an arena

let bgColor = '#050508';
arenaName string

Stores the name of the current battle stage (e.g., 'SHIBUYA', 'ZENIN') for display purposes

let arenaName = 'SHIBUYA';
gravity number

The constant gravitational acceleration applied to all players every frame - makes falling realistic

const gravity = 0.5;
p1 object (Player instance)

The first player character object, storing position, health, abilities, and methods

let p1;
p2 object (Player instance)

The second player character object, separate from p1 with independent stats and controls

let p2;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG checkCollisions()

Hit detection only works on the horizontal (x) axis - players can pass through each other vertically without colliding

💡 Add vertical distance checking: const vertDist = Math.abs(p1.y - p2.y); and include it in the hitRange calculation to create a full 2D collision box

PERFORMANCE updateHUD()

DOM queries (document.getElementById) run every frame even when nothing changed, which is slow and unnecessary

💡 Cache the elements as global variables once in initArena(), then reuse them: const hpBar1 = document.getElementById('p1-hp'); then in updateHUD() just set hpBar1.style.width

FEATURE Player class

No animation system - the character is just a static rectangle that doesn't change appearance during jumps, attacks, or knockback

💡 Add visual feedback: change the color or draw a glow when attacking, scale the character during jumps, or add a knockback animation by drawing them at an offset

STYLE handleInput()

The input mapping is hardcoded directly in the function, making it impossible to remap controls or test different key bindings

💡 Create a keyBindings object at the top: const keyBindings = { p1Left: 'a', p1Right: 'd', ... } and reference it instead of hardcoding

BUG Player.update()

Velocity isn't reset after movement, so calling moveLeft() once causes the player to continue sliding left forever until another movement key is pressed

💡 In handleInput(), set vx = 0 at the start of each frame before processing input, or add a method like stop() to reset velocity when no keys are pressed

🔄 Code Flow

Code flow showing setup, draw, handleinput, checkcollisions, updatehud, endgame, initarena, showstages, player

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> game-active-check[Game Active Check] game-active-check -->|true| player-updates[Player Updates] player-updates --> physics-update[Physics Update] physics-update --> ground-collision[Ground Collision] ground-collision --> boundary-check[Boundary Check] boundary-check --> rendering[Render Characters] rendering --> input-handling[Process Input] input-handling --> key-detection[Key Detection] key-detection -->|true| p1-mapping[Player 1 Key Mapping] key-detection -->|true| p2-mapping[Player 2 Key Mapping] input-handling --> collision-check[Check Collisions] collision-check --> distance-calc[Distance Calculation] distance-calc --> hit-range-check[Hitbox Overlap Check] hit-range-check -->|true| p1-hit-p2[Player 1 Attacking P2] p1-hit-p2 --> damage-knockback[Apply Damage and Knockback] damage-knockback --> victory-check[Victory Detection] victory-check -->|true| endgame[endGame] endgame --> game-stop[Stop Game Loop] game-stop --> display-screen[Show Win Screen] rendering --> hud-update[Update UI] hud-update --> hp-calc[HP Percentage Calculation] hp-calc --> dom-update[DOM Element Updates] click setup href "#fn-setup" click draw href "#fn-draw" click game-active-check href "#sub-game-active-check" click player-updates href "#sub-player-updates" click physics-update href "#sub-physics-update" click ground-collision href "#sub-ground-collision" click boundary-check href "#sub-boundary-check" click rendering href "#sub-rendering" click input-handling href "#sub-input-handling" click key-detection href "#sub-key-detection" click p1-mapping href "#sub-p1-mapping" click p2-mapping href "#sub-p2-mapping" click collision-check href "#sub-collision-check" click distance-calc href "#sub-distance-calc" click hit-range-check href "#sub-hit-range-check" click p1-hit-p2 href "#sub-p1-hit-p2" click damage-knockback href "#sub-damage-knockback" click victory-check href "#sub-victory-check" click endgame href "#fn-endgame" click game-stop href "#sub-game-stop" click display-screen href "#sub-display-screen" click hud-update href "#sub-hud-update" click hp-calc href "#sub-hp-calc" click dom-update href "#sub-dom-update"

Preview

Cursed conquest aka JJK game - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Cursed conquest aka JJK game - Code flow showing setup, draw, handleinput, checkcollisions, updatehud, endgame, initarena, showstages, player
Code Flow Diagram