User prompt
they has to be on platform only not outstage
User prompt
Put characters on the platform. Seperate them
User prompt
add background assets
Code edit (1 edits merged)
Please save this source code
User prompt
Gravity Duel: Sword & Bow
Initial prompt
Game Overview: Include 2 playable characters for now. Add a Custom Game Mode where players can play against each other or against a computer-controlled bot. Add a Computer AI opponent (medium difficulty). Map: Use a simple horizontal rectangular platform as the map. No holes or pits — the map should be a flat surface (no platforms or gaps). Movement & Controls: W: Jump (allow up to 3 jumps in midair) A / D: Move left / right S: Crouch (optional) J: Light attack (can be directional) K: Heavy attack (can be used on ground or in midair) L: Dodge / Dash H: Throw weapon Wall Climbing: Characters should be able to climb walls by repeatedly jumping while next to a wall. Weapons: Include 2 weapons: Sword and Bow Place weapon pickup icons on the map. When a player touches an icon, they pick up that weapon and the icon disappears. Players can only have one weapon at a time. Weapons can be thrown using the H key and will affect the enemy on hit. Attacks: Light Attacks (J): Should vary by direction: W + J → Up attack A + J → Left attack D + J → Right attack S + J → Downward attack Heavy Attacks (K): Can be used both on ground and midair. In midair, heavy attacks act as recovery attacks (knock enemies upward on hit). Gravity Cancel System: If player presses L while in air, a dash occurs and enables a Gravity Cancel. After performing a Gravity Cancel (L), pressing J or K allows performing light or heavy attacks while airborne as if grounded. Physics: Medium gravity Wall jumps for vertical mobility No edge clinging Flat terrain only (no ledges or falling) Start with just 2 characters, a single flat map, and the two weapons (sword and bow). More features can be added later.
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Arrow class (projectile)
var Arrow = Container.expand(function () {
var self = Container.call(this);
var arrowGfx = self.attachAsset('arrow', {
anchorX: 0.1,
anchorY: 0.5
});
self.vx = 0;
self.vy = 0;
self.owner = null;
self.lifetime = 60;
self.update = function () {
self.x += self.vx;
self.y += self.vy;
self.lifetime--;
if (self.lifetime <= 0 || self.x < -100 || self.x > 2148 || self.y < -100 || self.y > 3000) {
self.destroy();
}
};
return self;
});
// Player class
var Player = Container.expand(function () {
var self = Container.call(this);
self.isAI = false;
self.hp = 100;
self.maxHp = 100;
self.stocks = 3;
self.facing = 1; // 1 = right, -1 = left
self.vx = 0;
self.vy = 0;
self.grounded = false;
self.jumps = 3;
self.maxJumps = 3;
self.wallCling = false;
self.gravityCancel = false;
self.gravityCancelTimer = 0;
self.dashTimer = 0;
self.dashDir = 0;
self.dashCooldown = 0;
self.attackCooldown = 0;
self.hitstun = 0;
self.invuln = 0;
self.weapon = null;
self.input = {
left: false,
right: false,
up: false,
down: false,
jump: false,
attackLight: false,
attackHeavy: false,
dash: false,
throwWeapon: false,
gravityCancel: false
};
self.lastInput = {};
self.charType = 1; // 1 or 2
self.name = '';
self.spawnX = 0;
self.spawnY = 0;
self.dead = false;
// Graphics
var charGfx = self.attachAsset(self.charType === 1 ? 'char1' : 'char2', {
anchorX: 0.5,
anchorY: 0.5
});
self.hpBar = new Text2('100', {
size: 60,
fill: "#fff"
});
self.hpBar.anchor.set(0.5, 0.5);
self.addChild(self.hpBar);
self.setCharType = function (type) {
self.charType = type;
charGfx.destroy();
var newGfx = self.attachAsset(type === 1 ? 'char1' : 'char2', {
anchorX: 0.5,
anchorY: 0.5
});
};
self.respawn = function () {
self.x = self.spawnX;
self.y = self.spawnY;
self.vx = 0;
self.vy = 0;
self.hp = self.maxHp;
self.jumps = self.maxJumps;
self.grounded = false;
self.wallCling = false;
self.gravityCancel = false;
self.gravityCancelTimer = 0;
self.dashTimer = 0;
self.dashDir = 0;
self.dashCooldown = 0;
self.attackCooldown = 0;
self.hitstun = 0;
self.invuln = 60;
self.dead = false;
};
self.takeDamage = function (amount, knockbackX, knockbackY) {
if (self.invuln > 0) return;
self.hp -= amount;
self.vx += knockbackX;
self.vy += knockbackY;
self.hitstun = 18 + Math.floor(amount * 0.7);
self.invuln = 18;
// Flash effect
LK.effects.flashObject(self, 0xffe066, 200);
// Show hit effect
var fx = LK.getAsset('hitfx', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y
});
game.addChild(fx);
tween(fx, {
alpha: 0
}, {
duration: 400,
onFinish: function onFinish() {
fx.destroy();
}
});
if (self.hp <= 0) {
self.stocks--;
self.dead = true;
self.hp = 0;
self.vx = 0;
self.vy = 0;
self.x = -9999;
self.y = -9999;
if (self.stocks > 0) {
LK.setTimeout(function () {
self.respawn();
}, 1200);
}
}
};
self.pickupWeapon = function (weapon) {
if (self.weapon) return;
weapon.pickup(self);
self.weapon = weapon;
};
self.dropWeapon = function () {
if (!self.weapon) return;
self.weapon.drop();
self.weapon = null;
};
self.throwWeapon = function () {
if (!self.weapon) return;
var throwVX = self.facing * 24 + self.vx * 0.5;
var throwVY = -8 + self.vy * 0.2;
self.weapon.throwWeapon(throwVX, throwVY);
self.weapon = null;
};
self.attack = function (type) {
if (!self.weapon) return;
if (self.weapon.cooldown > 0) return;
self.weapon.attack(type, self.facing);
};
self.gravityCancelStart = function () {
if (self.gravityCancel || self.grounded) return;
self.gravityCancel = true;
self.gravityCancelTimer = 24;
self.vx *= 0.7;
self.vy *= 0.7;
};
self.gravityCancelEnd = function () {
self.gravityCancel = false;
self.gravityCancelTimer = 0;
};
self.update = function () {
// Update HP bar
self.hpBar.setText(self.hp + '');
self.hpBar.x = 0;
self.hpBar.y = -140;
if (self.dead) return;
// Invuln
if (self.invuln > 0) self.invuln--;
// Hitstun
if (self.hitstun > 0) {
self.hitstun--;
self.x += self.vx;
self.y += self.vy;
self.vx *= 0.85;
self.vy *= 0.85;
return;
}
// Dashing
if (self.dashTimer > 0) {
self.dashTimer--;
self.x += self.dashDir * 32;
if (self.dashTimer === 0) {
self.dashCooldown = 24;
}
} else {
// Movement
if (self.input.left) {
self.vx -= 2.2;
self.facing = -1;
}
if (self.input.right) {
self.vx += 2.2;
self.facing = 1;
}
if (!self.input.left && !self.input.right) {
self.vx *= 0.8;
}
if (self.input.jump && !self.lastInput.jump) {
if (self.grounded || self.jumps > 0 || self.wallCling) {
self.vy = -32;
if (!self.grounded) self.jumps--;
if (self.wallCling) {
self.vx = -self.facing * 18;
self.wallCling = false;
}
}
}
if (self.input.dash && !self.lastInput.dash && self.dashCooldown === 0) {
self.dashTimer = 8;
self.dashDir = self.facing;
}
if (self.input.gravityCancel && !self.lastInput.gravityCancel) {
self.gravityCancelStart();
}
if (!self.input.gravityCancel && self.gravityCancel) {
self.gravityCancelEnd();
}
if (self.input.attackLight && !self.lastInput.attackLight) {
self.attack('light');
}
if (self.input.attackHeavy && !self.lastInput.attackHeavy) {
self.attack('heavy');
}
if (self.input.throwWeapon && !self.lastInput.throwWeapon) {
self.throwWeapon();
}
}
// Gravity
if (!self.grounded && !self.gravityCancel) {
self.vy += 2.2;
if (self.vy > 40) self.vy = 40;
}
if (self.gravityCancel) {
self.gravityCancelTimer--;
if (self.gravityCancelTimer <= 0) self.gravityCancelEnd();
}
// Wall climbing
if (!self.grounded && !self.gravityCancel) {
// Left wall
if (self.x < platform.x - platform.width / 2 + 100 && self.vy > 0) {
self.wallCling = true;
self.jumps = self.maxJumps;
self.vy *= 0.7;
}
// Right wall
if (self.x > platform.x + platform.width / 2 - 100 && self.vy > 0) {
self.wallCling = true;
self.jumps = self.maxJumps;
self.vy *= 0.7;
}
} else {
self.wallCling = false;
}
// Apply velocity
self.x += self.vx;
self.y += self.vy;
// Friction
if (self.grounded) self.vx *= 0.7;else self.vx *= 0.98;
// Clamp position
if (self.x < 100) self.x = 100;
if (self.x > 1948) self.x = 1948;
// Platform collision
if (self.y + 110 > platform.y - platform.height / 2 && self.y < platform.y + platform.height / 2) {
if (self.y < platform.y) {
self.y = platform.y - 110;
self.vy = 0;
self.grounded = true;
self.jumps = self.maxJumps;
}
} else {
self.grounded = false;
}
// Out of bounds
if (self.y > 3000) {
self.stocks--;
self.dead = true;
self.hp = 0;
self.x = -9999;
self.y = -9999;
if (self.stocks > 0) {
LK.setTimeout(function () {
self.respawn();
}, 1200);
}
}
// Update last input
for (var k in self.input) {
self.lastInput[k] = self.input[k];
}
};
return self;
});
// Weapon base class
var Weapon = Container.expand(function () {
var self = Container.call(this);
self.owner = null; // Player who owns this weapon, or null if on ground
self.isHeld = false;
self.isThrown = false;
self.throwVX = 0;
self.throwVY = 0;
self.type = 'none'; // 'sword' or 'bow'
self.damage = 0;
self.dir = 1; // 1 = right, -1 = left
self.cooldown = 0; // frames until can attack again
self.attackType = 'light'; // 'light' or 'heavy'
self.attackTimer = 0; // frames left in attack animation
self.attackDir = 1; // 1 = right, -1 = left
self.attackActive = false;
self.hitbox = null; // Rectangle for attack
self.arrow = null; // For bow, the arrow object
self.init = function () {};
self.update = function () {};
self.attack = function (type, dir) {};
self.throwWeapon = function (vx, vy) {};
self.pickup = function (player) {};
self.drop = function () {};
self.destroyWeapon = function () {
self.destroy();
};
return self;
});
// Sword class
var Sword = Weapon.expand(function () {
var self = Weapon.call(this);
var swordGfx = self.attachAsset('sword', {
anchorX: 0.1,
anchorY: 0.5
});
self.type = 'sword';
self.damage = 8;
self.cooldown = 0;
self.attackType = 'light';
self.attackTimer = 0;
self.attackActive = false;
self.dir = 1;
self.hitbox = null;
self.isThrown = false;
self.throwVX = 0;
self.throwVY = 0;
self.isHeld = false;
self.owner = null;
self.attack = function (type, dir) {
if (self.cooldown > 0) return;
self.attackType = type;
self.attackDir = dir;
self.attackTimer = type === 'light' ? 12 : 22;
self.attackActive = true;
self.cooldown = type === 'light' ? 18 : 32;
};
self.update = function () {
if (self.isHeld && self.owner) {
// Follow owner's hand
self.x = self.owner.x + self.owner.facing * 80;
self.y = self.owner.y + 40;
self.scaleX = self.owner.facing;
self.scaleY = 1;
self.rotation = self.attackActive ? self.attackDir * 0.5 * (self.attackType === 'light' ? 1 : 1.5) : 0;
}
if (self.attackActive) {
self.attackTimer--;
if (self.attackTimer <= 0) {
self.attackActive = false;
self.rotation = 0;
}
}
if (self.cooldown > 0) self.cooldown--;
// Thrown logic
if (self.isThrown) {
self.x += self.throwVX;
self.y += self.throwVY;
self.throwVY += 1.2; // gravity
self.rotation += 0.25 * self.throwVX / 10;
// Out of bounds
if (self.y > 3000 || self.x < -200 || self.x > 2248) {
self.destroyWeapon();
}
}
};
self.throwWeapon = function (vx, vy) {
self.isHeld = false;
self.owner = null;
self.isThrown = true;
self.throwVX = vx;
self.throwVY = vy;
};
self.pickup = function (player) {
self.isHeld = true;
self.owner = player;
self.isThrown = false;
};
self.drop = function () {
self.isHeld = false;
self.owner = null;
self.isThrown = false;
};
return self;
});
// Bow class
var Bow = Weapon.expand(function () {
var self = Weapon.call(this);
var bowGfx = self.attachAsset('bow', {
anchorX: 0.1,
anchorY: 0.5
});
self.type = 'bow';
self.damage = 6;
self.cooldown = 0;
self.attackType = 'light';
self.attackTimer = 0;
self.attackActive = false;
self.dir = 1;
self.arrow = null;
self.isThrown = false;
self.throwVX = 0;
self.throwVY = 0;
self.isHeld = false;
self.owner = null;
self.attack = function (type, dir) {
if (self.cooldown > 0) return;
self.attackType = type;
self.attackDir = dir;
self.attackTimer = type === 'light' ? 18 : 30;
self.attackActive = true;
self.cooldown = type === 'light' ? 28 : 44;
// Fire arrow at peak of draw
if (self.arrow === null) {
var arrow = new Arrow();
arrow.x = self.x + self.attackDir * 80;
arrow.y = self.y;
arrow.vx = self.attackDir * (type === 'light' ? 32 : 48);
arrow.vy = 0;
arrow.owner = self.owner;
self.arrow = arrow;
game.addChild(arrow);
arrows.push(arrow);
}
};
self.update = function () {
if (self.isHeld && self.owner) {
self.x = self.owner.x + self.owner.facing * 80;
self.y = self.owner.y + 40;
self.scaleX = self.owner.facing;
self.scaleY = 1;
self.rotation = self.attackActive ? self.attackDir * 0.2 : 0;
}
if (self.attackActive) {
self.attackTimer--;
if (self.attackTimer <= 0) {
self.attackActive = false;
self.rotation = 0;
self.arrow = null;
}
}
if (self.cooldown > 0) self.cooldown--;
// Thrown logic
if (self.isThrown) {
self.x += self.throwVX;
self.y += self.throwVY;
self.throwVY += 1.2; // gravity
self.rotation += 0.25 * self.throwVX / 10;
// Out of bounds
if (self.y > 3000 || self.x < -200 || self.x > 2248) {
self.destroyWeapon();
}
}
};
self.throwWeapon = function (vx, vy) {
self.isHeld = false;
self.owner = null;
self.isThrown = true;
self.throwVX = vx;
self.throwVY = vy;
};
self.pickup = function (player) {
self.isHeld = true;
self.owner = player;
self.isThrown = false;
};
self.drop = function () {
self.isHeld = false;
self.owner = null;
self.isThrown = false;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a1a
});
/****
* Game Code
****/
// background image asset
// Add background image behind all gameplay elements
var bg = LK.getAsset('bg', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2
});
game.addChild(bg);
// Hit effect
// Platform
// Weapons
// Characters
// Platform
var platform = LK.getAsset('platform', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2000
});
game.addChild(platform);
// Players
var player1 = new Player();
player1.setCharType(1);
player1.name = "Player 1";
// Calculate platform bounds for safe spawn
var platformLeft = platform.x - platform.width / 2 + 100;
var platformRight = platform.x + platform.width / 2 - 100;
var platformY = platform.y - 110;
// Place player1 on the left side of the platform, clamped to platform bounds
player1.spawnX = Math.max(platformLeft, platform.x - platform.width / 4);
player1.spawnY = platformY;
player1.x = player1.spawnX;
player1.y = player1.spawnY;
player1.facing = 1;
player1.isAI = false;
player1.stocks = 3;
game.addChild(player1);
var player2 = new Player();
player2.setCharType(2);
player2.name = "Player 2";
// Place player2 on the right side of the platform, clamped to platform bounds
player2.spawnX = Math.min(platformRight, platform.x + platform.width / 4);
player2.spawnY = platformY;
player2.x = player2.spawnX;
player2.y = player2.spawnY;
player2.facing = -1;
player2.isAI = true; // Set to false for local 2P
player2.stocks = 3;
game.addChild(player2);
// Weapons
var weapons = [];
var arrows = [];
// Spawn initial weapons
function spawnWeapon(type, x, y) {
var w;
if (type === 'sword') w = new Sword();else w = new Bow();
w.x = x;
w.y = y;
weapons.push(w);
game.addChild(w);
return w;
}
var sword1 = spawnWeapon('sword', 2048 / 2 - 200, platform.y - 100);
var bow1 = spawnWeapon('bow', 2048 / 2 + 200, platform.y - 100);
// GUI
var p1StockTxt = new Text2('P1: 3', {
size: 80,
fill: 0x3A7BD5
});
p1StockTxt.anchor.set(0, 0);
LK.gui.top.addChild(p1StockTxt);
p1StockTxt.x = 120;
p1StockTxt.y = 20;
var p2StockTxt = new Text2('P2: 3', {
size: 80,
fill: 0xD53A3A
});
p2StockTxt.anchor.set(1, 0);
LK.gui.top.addChild(p2StockTxt);
p2StockTxt.x = LK.gui.top.width - 120;
p2StockTxt.y = 20;
// Touch controls (mobile)
var touchState = {
left: false,
right: false,
up: false,
down: false,
jump: false,
attackLight: false,
attackHeavy: false,
dash: false,
throwWeapon: false,
gravityCancel: false
};
var dragNode = null;
// Touch area for movement (left half)
var moveArea = {
x: 0,
y: 300,
width: 1024,
height: 2432
};
// Touch area for actions (right half)
var actionArea = {
x: 1024,
y: 300,
width: 1024,
height: 2432
};
// Helper: get touch area
function getTouchArea(x, y) {
if (x < moveArea.x + moveArea.width && y > moveArea.y) return 'move';
if (x > actionArea.x && y > actionArea.y) return 'action';
return null;
}
// Touch input
game.down = function (x, y, obj) {
if (x < 100 && y < 100) return; // Don't allow in top left
var area = getTouchArea(x, y);
if (area === 'move') {
dragNode = 'move';
if (y < platform.y) {
touchState.jump = true;
} else if (x < moveArea.x + moveArea.width / 2) {
touchState.left = true;
} else {
touchState.right = true;
}
} else if (area === 'action') {
dragNode = 'action';
if (y < platform.y - 200) {
touchState.attackHeavy = true;
} else if (y < platform.y) {
touchState.attackLight = true;
} else if (x > 2048 - 200) {
touchState.throwWeapon = true;
} else if (x > 2048 - 400) {
touchState.dash = true;
} else {
touchState.gravityCancel = true;
}
}
updatePlayerInput();
};
game.move = function (x, y, obj) {
if (!dragNode) return;
var area = getTouchArea(x, y);
// Reset all
for (var k in touchState) touchState[k] = false;
if (area === 'move') {
if (y < platform.y) {
touchState.jump = true;
} else if (x < moveArea.x + moveArea.width / 2) {
touchState.left = true;
} else {
touchState.right = true;
}
} else if (area === 'action') {
if (y < platform.y - 200) {
touchState.attackHeavy = true;
} else if (y < platform.y) {
touchState.attackLight = true;
} else if (x > 2048 - 200) {
touchState.throwWeapon = true;
} else if (x > 2048 - 400) {
touchState.dash = true;
} else {
touchState.gravityCancel = true;
}
}
updatePlayerInput();
};
game.up = function (x, y, obj) {
dragNode = null;
for (var k in touchState) touchState[k] = false;
updatePlayerInput();
};
// Update player1 input from touchState
function updatePlayerInput() {
for (var k in player1.input) {
player1.input[k] = !!touchState[k];
}
}
// AI logic for player2
function aiUpdate() {
if (!player2.isAI || player2.dead) return;
// Simple AI: move toward player1, jump if close, attack if in range
var dx = player1.x - player2.x;
var dy = player1.y - player2.y;
for (var k in player2.input) player2.input[k] = false;
if (Math.abs(dx) > 80) {
player2.input.left = dx < 0;
player2.input.right = dx > 0;
}
if (Math.abs(dx) < 220 && Math.abs(dy) < 120 && player2.weapon && player2.weapon.cooldown === 0) {
player2.input.attackLight = true;
}
if (Math.abs(dx) < 120 && Math.abs(dy) > 80 && player2.jumps > 0) {
player2.input.jump = true;
}
if (!player2.weapon) {
// Try to pick up weapon
for (var i = 0; i < weapons.length; ++i) {
var w = weapons[i];
if (!w.isHeld && !w.isThrown && Math.abs(w.x - player2.x) < 100 && Math.abs(w.y - player2.y) < 120) {
player2.pickupWeapon(w);
break;
}
}
}
}
// Main update
game.update = function () {
// Update players
player1.update();
player2.update();
aiUpdate();
// Update weapons
for (var i = weapons.length - 1; i >= 0; --i) {
var w = weapons[i];
w.update();
// Pickup logic
if (!w.isHeld && !w.isThrown) {
// Player1
if (!player1.weapon && !player1.dead && Math.abs(w.x - player1.x) < 100 && Math.abs(w.y - player1.y) < 120) {
player1.pickupWeapon(w);
}
// Player2
if (!player2.weapon && !player2.dead && Math.abs(w.x - player2.x) < 100 && Math.abs(w.y - player2.y) < 120) {
player2.pickupWeapon(w);
}
}
// Remove destroyed
if (w.destroyed) {
weapons.splice(i, 1);
}
}
// Update arrows
for (var i = arrows.length - 1; i >= 0; --i) {
var a = arrows[i];
a.update();
// Hit player1
if (!player1.dead && a.owner !== player1 && Math.abs(a.x - player1.x) < 100 && Math.abs(a.y - player1.y) < 80) {
player1.takeDamage(12, a.vx * 0.7, -8);
a.destroy();
arrows.splice(i, 1);
continue;
}
// Hit player2
if (!player2.dead && a.owner !== player2 && Math.abs(a.x - player2.x) < 100 && Math.abs(a.y - player2.y) < 80) {
player2.takeDamage(12, a.vx * 0.7, -8);
a.destroy();
arrows.splice(i, 1);
continue;
}
// Remove destroyed
if (a.destroyed) {
arrows.splice(i, 1);
}
}
// Sword attack hit detection
for (var i = 0; i < weapons.length; ++i) {
var w = weapons[i];
if (w.type === 'sword' && w.attackActive && w.owner) {
var target = w.owner === player1 ? player2 : player1;
if (!target.dead && Math.abs(w.x + w.attackDir * 80 - target.x) < 120 && Math.abs(w.y - target.y) < 120) {
target.takeDamage(w.attackType === 'light' ? 10 : 18, w.attackDir * (w.attackType === 'light' ? 18 : 28), -12);
w.attackActive = false;
}
}
}
// Bow melee (if needed)
for (var i = 0; i < weapons.length; ++i) {
var w = weapons[i];
if (w.type === 'bow' && w.attackActive && w.owner) {
var target = w.owner === player1 ? player2 : player1;
if (!target.dead && Math.abs(w.x + w.attackDir * 80 - target.x) < 120 && Math.abs(w.y - target.y) < 120) {
target.takeDamage(w.attackType === 'light' ? 8 : 14, w.attackDir * (w.attackType === 'light' ? 12 : 22), -8);
w.attackActive = false;
}
}
}
// Thrown weapon hit detection
for (var i = 0; i < weapons.length; ++i) {
var w = weapons[i];
if (w.isThrown) {
// Hit player1
if (!player1.dead && Math.abs(w.x - player1.x) < 100 && Math.abs(w.y - player1.y) < 100 && w.owner !== player1) {
player1.takeDamage(16, w.throwVX * 0.7, -12);
w.isThrown = false;
}
// Hit player2
if (!player2.dead && Math.abs(w.x - player2.x) < 100 && Math.abs(w.y - player2.y) < 100 && w.owner !== player2) {
player2.takeDamage(16, w.throwVX * 0.7, -12);
w.isThrown = false;
}
}
}
// Update GUI
p1StockTxt.setText('P1: ' + player1.stocks);
p2StockTxt.setText('P2: ' + player2.stocks);
// Win/Lose
if (player1.stocks <= 0 && !player1.dead) {
LK.showGameOver();
}
if (player2.stocks <= 0 && !player2.dead) {
LK.showYouWin();
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Arrow class (projectile)
var Arrow = Container.expand(function () {
var self = Container.call(this);
var arrowGfx = self.attachAsset('arrow', {
anchorX: 0.1,
anchorY: 0.5
});
self.vx = 0;
self.vy = 0;
self.owner = null;
self.lifetime = 60;
self.update = function () {
self.x += self.vx;
self.y += self.vy;
self.lifetime--;
if (self.lifetime <= 0 || self.x < -100 || self.x > 2148 || self.y < -100 || self.y > 3000) {
self.destroy();
}
};
return self;
});
// Player class
var Player = Container.expand(function () {
var self = Container.call(this);
self.isAI = false;
self.hp = 100;
self.maxHp = 100;
self.stocks = 3;
self.facing = 1; // 1 = right, -1 = left
self.vx = 0;
self.vy = 0;
self.grounded = false;
self.jumps = 3;
self.maxJumps = 3;
self.wallCling = false;
self.gravityCancel = false;
self.gravityCancelTimer = 0;
self.dashTimer = 0;
self.dashDir = 0;
self.dashCooldown = 0;
self.attackCooldown = 0;
self.hitstun = 0;
self.invuln = 0;
self.weapon = null;
self.input = {
left: false,
right: false,
up: false,
down: false,
jump: false,
attackLight: false,
attackHeavy: false,
dash: false,
throwWeapon: false,
gravityCancel: false
};
self.lastInput = {};
self.charType = 1; // 1 or 2
self.name = '';
self.spawnX = 0;
self.spawnY = 0;
self.dead = false;
// Graphics
var charGfx = self.attachAsset(self.charType === 1 ? 'char1' : 'char2', {
anchorX: 0.5,
anchorY: 0.5
});
self.hpBar = new Text2('100', {
size: 60,
fill: "#fff"
});
self.hpBar.anchor.set(0.5, 0.5);
self.addChild(self.hpBar);
self.setCharType = function (type) {
self.charType = type;
charGfx.destroy();
var newGfx = self.attachAsset(type === 1 ? 'char1' : 'char2', {
anchorX: 0.5,
anchorY: 0.5
});
};
self.respawn = function () {
self.x = self.spawnX;
self.y = self.spawnY;
self.vx = 0;
self.vy = 0;
self.hp = self.maxHp;
self.jumps = self.maxJumps;
self.grounded = false;
self.wallCling = false;
self.gravityCancel = false;
self.gravityCancelTimer = 0;
self.dashTimer = 0;
self.dashDir = 0;
self.dashCooldown = 0;
self.attackCooldown = 0;
self.hitstun = 0;
self.invuln = 60;
self.dead = false;
};
self.takeDamage = function (amount, knockbackX, knockbackY) {
if (self.invuln > 0) return;
self.hp -= amount;
self.vx += knockbackX;
self.vy += knockbackY;
self.hitstun = 18 + Math.floor(amount * 0.7);
self.invuln = 18;
// Flash effect
LK.effects.flashObject(self, 0xffe066, 200);
// Show hit effect
var fx = LK.getAsset('hitfx', {
anchorX: 0.5,
anchorY: 0.5,
x: self.x,
y: self.y
});
game.addChild(fx);
tween(fx, {
alpha: 0
}, {
duration: 400,
onFinish: function onFinish() {
fx.destroy();
}
});
if (self.hp <= 0) {
self.stocks--;
self.dead = true;
self.hp = 0;
self.vx = 0;
self.vy = 0;
self.x = -9999;
self.y = -9999;
if (self.stocks > 0) {
LK.setTimeout(function () {
self.respawn();
}, 1200);
}
}
};
self.pickupWeapon = function (weapon) {
if (self.weapon) return;
weapon.pickup(self);
self.weapon = weapon;
};
self.dropWeapon = function () {
if (!self.weapon) return;
self.weapon.drop();
self.weapon = null;
};
self.throwWeapon = function () {
if (!self.weapon) return;
var throwVX = self.facing * 24 + self.vx * 0.5;
var throwVY = -8 + self.vy * 0.2;
self.weapon.throwWeapon(throwVX, throwVY);
self.weapon = null;
};
self.attack = function (type) {
if (!self.weapon) return;
if (self.weapon.cooldown > 0) return;
self.weapon.attack(type, self.facing);
};
self.gravityCancelStart = function () {
if (self.gravityCancel || self.grounded) return;
self.gravityCancel = true;
self.gravityCancelTimer = 24;
self.vx *= 0.7;
self.vy *= 0.7;
};
self.gravityCancelEnd = function () {
self.gravityCancel = false;
self.gravityCancelTimer = 0;
};
self.update = function () {
// Update HP bar
self.hpBar.setText(self.hp + '');
self.hpBar.x = 0;
self.hpBar.y = -140;
if (self.dead) return;
// Invuln
if (self.invuln > 0) self.invuln--;
// Hitstun
if (self.hitstun > 0) {
self.hitstun--;
self.x += self.vx;
self.y += self.vy;
self.vx *= 0.85;
self.vy *= 0.85;
return;
}
// Dashing
if (self.dashTimer > 0) {
self.dashTimer--;
self.x += self.dashDir * 32;
if (self.dashTimer === 0) {
self.dashCooldown = 24;
}
} else {
// Movement
if (self.input.left) {
self.vx -= 2.2;
self.facing = -1;
}
if (self.input.right) {
self.vx += 2.2;
self.facing = 1;
}
if (!self.input.left && !self.input.right) {
self.vx *= 0.8;
}
if (self.input.jump && !self.lastInput.jump) {
if (self.grounded || self.jumps > 0 || self.wallCling) {
self.vy = -32;
if (!self.grounded) self.jumps--;
if (self.wallCling) {
self.vx = -self.facing * 18;
self.wallCling = false;
}
}
}
if (self.input.dash && !self.lastInput.dash && self.dashCooldown === 0) {
self.dashTimer = 8;
self.dashDir = self.facing;
}
if (self.input.gravityCancel && !self.lastInput.gravityCancel) {
self.gravityCancelStart();
}
if (!self.input.gravityCancel && self.gravityCancel) {
self.gravityCancelEnd();
}
if (self.input.attackLight && !self.lastInput.attackLight) {
self.attack('light');
}
if (self.input.attackHeavy && !self.lastInput.attackHeavy) {
self.attack('heavy');
}
if (self.input.throwWeapon && !self.lastInput.throwWeapon) {
self.throwWeapon();
}
}
// Gravity
if (!self.grounded && !self.gravityCancel) {
self.vy += 2.2;
if (self.vy > 40) self.vy = 40;
}
if (self.gravityCancel) {
self.gravityCancelTimer--;
if (self.gravityCancelTimer <= 0) self.gravityCancelEnd();
}
// Wall climbing
if (!self.grounded && !self.gravityCancel) {
// Left wall
if (self.x < platform.x - platform.width / 2 + 100 && self.vy > 0) {
self.wallCling = true;
self.jumps = self.maxJumps;
self.vy *= 0.7;
}
// Right wall
if (self.x > platform.x + platform.width / 2 - 100 && self.vy > 0) {
self.wallCling = true;
self.jumps = self.maxJumps;
self.vy *= 0.7;
}
} else {
self.wallCling = false;
}
// Apply velocity
self.x += self.vx;
self.y += self.vy;
// Friction
if (self.grounded) self.vx *= 0.7;else self.vx *= 0.98;
// Clamp position
if (self.x < 100) self.x = 100;
if (self.x > 1948) self.x = 1948;
// Platform collision
if (self.y + 110 > platform.y - platform.height / 2 && self.y < platform.y + platform.height / 2) {
if (self.y < platform.y) {
self.y = platform.y - 110;
self.vy = 0;
self.grounded = true;
self.jumps = self.maxJumps;
}
} else {
self.grounded = false;
}
// Out of bounds
if (self.y > 3000) {
self.stocks--;
self.dead = true;
self.hp = 0;
self.x = -9999;
self.y = -9999;
if (self.stocks > 0) {
LK.setTimeout(function () {
self.respawn();
}, 1200);
}
}
// Update last input
for (var k in self.input) {
self.lastInput[k] = self.input[k];
}
};
return self;
});
// Weapon base class
var Weapon = Container.expand(function () {
var self = Container.call(this);
self.owner = null; // Player who owns this weapon, or null if on ground
self.isHeld = false;
self.isThrown = false;
self.throwVX = 0;
self.throwVY = 0;
self.type = 'none'; // 'sword' or 'bow'
self.damage = 0;
self.dir = 1; // 1 = right, -1 = left
self.cooldown = 0; // frames until can attack again
self.attackType = 'light'; // 'light' or 'heavy'
self.attackTimer = 0; // frames left in attack animation
self.attackDir = 1; // 1 = right, -1 = left
self.attackActive = false;
self.hitbox = null; // Rectangle for attack
self.arrow = null; // For bow, the arrow object
self.init = function () {};
self.update = function () {};
self.attack = function (type, dir) {};
self.throwWeapon = function (vx, vy) {};
self.pickup = function (player) {};
self.drop = function () {};
self.destroyWeapon = function () {
self.destroy();
};
return self;
});
// Sword class
var Sword = Weapon.expand(function () {
var self = Weapon.call(this);
var swordGfx = self.attachAsset('sword', {
anchorX: 0.1,
anchorY: 0.5
});
self.type = 'sword';
self.damage = 8;
self.cooldown = 0;
self.attackType = 'light';
self.attackTimer = 0;
self.attackActive = false;
self.dir = 1;
self.hitbox = null;
self.isThrown = false;
self.throwVX = 0;
self.throwVY = 0;
self.isHeld = false;
self.owner = null;
self.attack = function (type, dir) {
if (self.cooldown > 0) return;
self.attackType = type;
self.attackDir = dir;
self.attackTimer = type === 'light' ? 12 : 22;
self.attackActive = true;
self.cooldown = type === 'light' ? 18 : 32;
};
self.update = function () {
if (self.isHeld && self.owner) {
// Follow owner's hand
self.x = self.owner.x + self.owner.facing * 80;
self.y = self.owner.y + 40;
self.scaleX = self.owner.facing;
self.scaleY = 1;
self.rotation = self.attackActive ? self.attackDir * 0.5 * (self.attackType === 'light' ? 1 : 1.5) : 0;
}
if (self.attackActive) {
self.attackTimer--;
if (self.attackTimer <= 0) {
self.attackActive = false;
self.rotation = 0;
}
}
if (self.cooldown > 0) self.cooldown--;
// Thrown logic
if (self.isThrown) {
self.x += self.throwVX;
self.y += self.throwVY;
self.throwVY += 1.2; // gravity
self.rotation += 0.25 * self.throwVX / 10;
// Out of bounds
if (self.y > 3000 || self.x < -200 || self.x > 2248) {
self.destroyWeapon();
}
}
};
self.throwWeapon = function (vx, vy) {
self.isHeld = false;
self.owner = null;
self.isThrown = true;
self.throwVX = vx;
self.throwVY = vy;
};
self.pickup = function (player) {
self.isHeld = true;
self.owner = player;
self.isThrown = false;
};
self.drop = function () {
self.isHeld = false;
self.owner = null;
self.isThrown = false;
};
return self;
});
// Bow class
var Bow = Weapon.expand(function () {
var self = Weapon.call(this);
var bowGfx = self.attachAsset('bow', {
anchorX: 0.1,
anchorY: 0.5
});
self.type = 'bow';
self.damage = 6;
self.cooldown = 0;
self.attackType = 'light';
self.attackTimer = 0;
self.attackActive = false;
self.dir = 1;
self.arrow = null;
self.isThrown = false;
self.throwVX = 0;
self.throwVY = 0;
self.isHeld = false;
self.owner = null;
self.attack = function (type, dir) {
if (self.cooldown > 0) return;
self.attackType = type;
self.attackDir = dir;
self.attackTimer = type === 'light' ? 18 : 30;
self.attackActive = true;
self.cooldown = type === 'light' ? 28 : 44;
// Fire arrow at peak of draw
if (self.arrow === null) {
var arrow = new Arrow();
arrow.x = self.x + self.attackDir * 80;
arrow.y = self.y;
arrow.vx = self.attackDir * (type === 'light' ? 32 : 48);
arrow.vy = 0;
arrow.owner = self.owner;
self.arrow = arrow;
game.addChild(arrow);
arrows.push(arrow);
}
};
self.update = function () {
if (self.isHeld && self.owner) {
self.x = self.owner.x + self.owner.facing * 80;
self.y = self.owner.y + 40;
self.scaleX = self.owner.facing;
self.scaleY = 1;
self.rotation = self.attackActive ? self.attackDir * 0.2 : 0;
}
if (self.attackActive) {
self.attackTimer--;
if (self.attackTimer <= 0) {
self.attackActive = false;
self.rotation = 0;
self.arrow = null;
}
}
if (self.cooldown > 0) self.cooldown--;
// Thrown logic
if (self.isThrown) {
self.x += self.throwVX;
self.y += self.throwVY;
self.throwVY += 1.2; // gravity
self.rotation += 0.25 * self.throwVX / 10;
// Out of bounds
if (self.y > 3000 || self.x < -200 || self.x > 2248) {
self.destroyWeapon();
}
}
};
self.throwWeapon = function (vx, vy) {
self.isHeld = false;
self.owner = null;
self.isThrown = true;
self.throwVX = vx;
self.throwVY = vy;
};
self.pickup = function (player) {
self.isHeld = true;
self.owner = player;
self.isThrown = false;
};
self.drop = function () {
self.isHeld = false;
self.owner = null;
self.isThrown = false;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a1a
});
/****
* Game Code
****/
// background image asset
// Add background image behind all gameplay elements
var bg = LK.getAsset('bg', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2
});
game.addChild(bg);
// Hit effect
// Platform
// Weapons
// Characters
// Platform
var platform = LK.getAsset('platform', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2000
});
game.addChild(platform);
// Players
var player1 = new Player();
player1.setCharType(1);
player1.name = "Player 1";
// Calculate platform bounds for safe spawn
var platformLeft = platform.x - platform.width / 2 + 100;
var platformRight = platform.x + platform.width / 2 - 100;
var platformY = platform.y - 110;
// Place player1 on the left side of the platform, clamped to platform bounds
player1.spawnX = Math.max(platformLeft, platform.x - platform.width / 4);
player1.spawnY = platformY;
player1.x = player1.spawnX;
player1.y = player1.spawnY;
player1.facing = 1;
player1.isAI = false;
player1.stocks = 3;
game.addChild(player1);
var player2 = new Player();
player2.setCharType(2);
player2.name = "Player 2";
// Place player2 on the right side of the platform, clamped to platform bounds
player2.spawnX = Math.min(platformRight, platform.x + platform.width / 4);
player2.spawnY = platformY;
player2.x = player2.spawnX;
player2.y = player2.spawnY;
player2.facing = -1;
player2.isAI = true; // Set to false for local 2P
player2.stocks = 3;
game.addChild(player2);
// Weapons
var weapons = [];
var arrows = [];
// Spawn initial weapons
function spawnWeapon(type, x, y) {
var w;
if (type === 'sword') w = new Sword();else w = new Bow();
w.x = x;
w.y = y;
weapons.push(w);
game.addChild(w);
return w;
}
var sword1 = spawnWeapon('sword', 2048 / 2 - 200, platform.y - 100);
var bow1 = spawnWeapon('bow', 2048 / 2 + 200, platform.y - 100);
// GUI
var p1StockTxt = new Text2('P1: 3', {
size: 80,
fill: 0x3A7BD5
});
p1StockTxt.anchor.set(0, 0);
LK.gui.top.addChild(p1StockTxt);
p1StockTxt.x = 120;
p1StockTxt.y = 20;
var p2StockTxt = new Text2('P2: 3', {
size: 80,
fill: 0xD53A3A
});
p2StockTxt.anchor.set(1, 0);
LK.gui.top.addChild(p2StockTxt);
p2StockTxt.x = LK.gui.top.width - 120;
p2StockTxt.y = 20;
// Touch controls (mobile)
var touchState = {
left: false,
right: false,
up: false,
down: false,
jump: false,
attackLight: false,
attackHeavy: false,
dash: false,
throwWeapon: false,
gravityCancel: false
};
var dragNode = null;
// Touch area for movement (left half)
var moveArea = {
x: 0,
y: 300,
width: 1024,
height: 2432
};
// Touch area for actions (right half)
var actionArea = {
x: 1024,
y: 300,
width: 1024,
height: 2432
};
// Helper: get touch area
function getTouchArea(x, y) {
if (x < moveArea.x + moveArea.width && y > moveArea.y) return 'move';
if (x > actionArea.x && y > actionArea.y) return 'action';
return null;
}
// Touch input
game.down = function (x, y, obj) {
if (x < 100 && y < 100) return; // Don't allow in top left
var area = getTouchArea(x, y);
if (area === 'move') {
dragNode = 'move';
if (y < platform.y) {
touchState.jump = true;
} else if (x < moveArea.x + moveArea.width / 2) {
touchState.left = true;
} else {
touchState.right = true;
}
} else if (area === 'action') {
dragNode = 'action';
if (y < platform.y - 200) {
touchState.attackHeavy = true;
} else if (y < platform.y) {
touchState.attackLight = true;
} else if (x > 2048 - 200) {
touchState.throwWeapon = true;
} else if (x > 2048 - 400) {
touchState.dash = true;
} else {
touchState.gravityCancel = true;
}
}
updatePlayerInput();
};
game.move = function (x, y, obj) {
if (!dragNode) return;
var area = getTouchArea(x, y);
// Reset all
for (var k in touchState) touchState[k] = false;
if (area === 'move') {
if (y < platform.y) {
touchState.jump = true;
} else if (x < moveArea.x + moveArea.width / 2) {
touchState.left = true;
} else {
touchState.right = true;
}
} else if (area === 'action') {
if (y < platform.y - 200) {
touchState.attackHeavy = true;
} else if (y < platform.y) {
touchState.attackLight = true;
} else if (x > 2048 - 200) {
touchState.throwWeapon = true;
} else if (x > 2048 - 400) {
touchState.dash = true;
} else {
touchState.gravityCancel = true;
}
}
updatePlayerInput();
};
game.up = function (x, y, obj) {
dragNode = null;
for (var k in touchState) touchState[k] = false;
updatePlayerInput();
};
// Update player1 input from touchState
function updatePlayerInput() {
for (var k in player1.input) {
player1.input[k] = !!touchState[k];
}
}
// AI logic for player2
function aiUpdate() {
if (!player2.isAI || player2.dead) return;
// Simple AI: move toward player1, jump if close, attack if in range
var dx = player1.x - player2.x;
var dy = player1.y - player2.y;
for (var k in player2.input) player2.input[k] = false;
if (Math.abs(dx) > 80) {
player2.input.left = dx < 0;
player2.input.right = dx > 0;
}
if (Math.abs(dx) < 220 && Math.abs(dy) < 120 && player2.weapon && player2.weapon.cooldown === 0) {
player2.input.attackLight = true;
}
if (Math.abs(dx) < 120 && Math.abs(dy) > 80 && player2.jumps > 0) {
player2.input.jump = true;
}
if (!player2.weapon) {
// Try to pick up weapon
for (var i = 0; i < weapons.length; ++i) {
var w = weapons[i];
if (!w.isHeld && !w.isThrown && Math.abs(w.x - player2.x) < 100 && Math.abs(w.y - player2.y) < 120) {
player2.pickupWeapon(w);
break;
}
}
}
}
// Main update
game.update = function () {
// Update players
player1.update();
player2.update();
aiUpdate();
// Update weapons
for (var i = weapons.length - 1; i >= 0; --i) {
var w = weapons[i];
w.update();
// Pickup logic
if (!w.isHeld && !w.isThrown) {
// Player1
if (!player1.weapon && !player1.dead && Math.abs(w.x - player1.x) < 100 && Math.abs(w.y - player1.y) < 120) {
player1.pickupWeapon(w);
}
// Player2
if (!player2.weapon && !player2.dead && Math.abs(w.x - player2.x) < 100 && Math.abs(w.y - player2.y) < 120) {
player2.pickupWeapon(w);
}
}
// Remove destroyed
if (w.destroyed) {
weapons.splice(i, 1);
}
}
// Update arrows
for (var i = arrows.length - 1; i >= 0; --i) {
var a = arrows[i];
a.update();
// Hit player1
if (!player1.dead && a.owner !== player1 && Math.abs(a.x - player1.x) < 100 && Math.abs(a.y - player1.y) < 80) {
player1.takeDamage(12, a.vx * 0.7, -8);
a.destroy();
arrows.splice(i, 1);
continue;
}
// Hit player2
if (!player2.dead && a.owner !== player2 && Math.abs(a.x - player2.x) < 100 && Math.abs(a.y - player2.y) < 80) {
player2.takeDamage(12, a.vx * 0.7, -8);
a.destroy();
arrows.splice(i, 1);
continue;
}
// Remove destroyed
if (a.destroyed) {
arrows.splice(i, 1);
}
}
// Sword attack hit detection
for (var i = 0; i < weapons.length; ++i) {
var w = weapons[i];
if (w.type === 'sword' && w.attackActive && w.owner) {
var target = w.owner === player1 ? player2 : player1;
if (!target.dead && Math.abs(w.x + w.attackDir * 80 - target.x) < 120 && Math.abs(w.y - target.y) < 120) {
target.takeDamage(w.attackType === 'light' ? 10 : 18, w.attackDir * (w.attackType === 'light' ? 18 : 28), -12);
w.attackActive = false;
}
}
}
// Bow melee (if needed)
for (var i = 0; i < weapons.length; ++i) {
var w = weapons[i];
if (w.type === 'bow' && w.attackActive && w.owner) {
var target = w.owner === player1 ? player2 : player1;
if (!target.dead && Math.abs(w.x + w.attackDir * 80 - target.x) < 120 && Math.abs(w.y - target.y) < 120) {
target.takeDamage(w.attackType === 'light' ? 8 : 14, w.attackDir * (w.attackType === 'light' ? 12 : 22), -8);
w.attackActive = false;
}
}
}
// Thrown weapon hit detection
for (var i = 0; i < weapons.length; ++i) {
var w = weapons[i];
if (w.isThrown) {
// Hit player1
if (!player1.dead && Math.abs(w.x - player1.x) < 100 && Math.abs(w.y - player1.y) < 100 && w.owner !== player1) {
player1.takeDamage(16, w.throwVX * 0.7, -12);
w.isThrown = false;
}
// Hit player2
if (!player2.dead && Math.abs(w.x - player2.x) < 100 && Math.abs(w.y - player2.y) < 100 && w.owner !== player2) {
player2.takeDamage(16, w.throwVX * 0.7, -12);
w.isThrown = false;
}
}
}
// Update GUI
p1StockTxt.setText('P1: ' + player1.stocks);
p2StockTxt.setText('P2: ' + player2.stocks);
// Win/Lose
if (player1.stocks <= 0 && !player1.dead) {
LK.showGameOver();
}
if (player2.stocks <= 0 && !player2.dead) {
LK.showYouWin();
}
};