SC CODE: // Copyright 2024. Civilware. All rights reserved.
// TELA Decentralized Web Document (TELA-DOC-1)
Function InitializePrivate() Uint64
10 IF init() == 0 THEN GOTO 30
20 RETURN 1
30 STORE("nameHdr", "main.js")
31 STORE("descrHdr", "Main JS file")
32 STORE("iconURLHdr", "")
33 STORE("dURL", "main.js")
34 STORE("docType", "TELA-JS-1")
35 STORE("subDir", "")
36 STORE("fileCheckC", "223f65df5b5f9c2e1b606beb006143be6e4633687942eb2fe76308bd08635867")
37 STORE("fileCheckS", "11dd619ff9df671e0e957904534f2f46ae2890b1ef47af63112ab0b474edd3bf")
100 RETURN 0
End Function
Function init() Uint64
10 IF EXISTS("owner") == 0 THEN GOTO 30
20 RETURN 1
30 STORE("owner", address())
50 STORE("docVersion", "1.0.0")
60 STORE("hash", HEX(TXID()))
70 STORE("likes", 0)
80 STORE("dislikes", 0)
100 RETURN 0
End Function
Function address() String
10 DIM s as String
20 LET s = SIGNER()
30 IF IS_ADDRESS_VALID(s) THEN GOTO 50
40 RETURN "anon"
50 RETURN ADDRESS_STRING(s)
End Function
Function Rate(r Uint64) Uint64
10 DIM addr as String
15 LET addr = address()
16 IF r < 100 && EXISTS(addr) == 0 && addr != "anon" THEN GOTO 30
20 RETURN 1
30 STORE(addr, ""+r+"_"+BLOCK_HEIGHT())
40 IF r < 50 THEN GOTO 70
50 STORE("likes", LOAD("likes")+1)
60 RETURN 0
70 STORE("dislikes", LOAD("dislikes")+1)
100 RETURN 0
End Function
/*
//----------
// Rendering
function render(t) {
// depth sort
const living = [game.player, ...game.enemies.filter(e => !e.dead)];
for (const e of living) e.depth = e.y + e.height;
living.sort((a, b) => a.depth - b.depth);
// reorder DOM groups
const layer = document.getElementById("players");
for (const e of living) {
const g = document.getElementById(e.id) || createSVGGroup(e.id);
layer.appendChild(g); // append moves it to correct depth
}
// now render bones
renderSkeleton(game.player);
for (const enemy of game.enemies) {
renderSkeleton(enemy);
deathAnimation(enemy);
}
renderHats(t)
}
function transformPoint(px, py, ox, oy, rot) {
const cos = Math.cos(rot);
const sin = Math.sin(rot);
return {
x: ox + px * cos - py * sin,
y: oy + px * sin + py * cos
};
}
function mirrorPoly(poly) {
return poly.map(p => ({ x: -p.x, y: p.y }));
}
function drawBone(entity, bone, boneName, parentX, parentY, parentRot) {
const [px, py] = bone.pivot;
const [cx, cy] = bone.childOrigin;
const pivotWorld = transformPoint(px, py, parentX, parentY, parentRot);
let rot = 0
if(entity.facing < 0) {
rot = parentRot - (bone.rotation || 0)
if(boneName === "hat"){
bone.flipX = true;
}
}else {
rot = parentRot + (bone.rotation || 0)
bone.flipX = false;
}
let renderPoly = {}
if (bone.flipX){
renderPoly = mirrorPoly(bone.poly);
}else{
renderPoly = bone.poly;
}
// STORE WORLD TRANSFORM FOR COLLISION DETECTION
bone.worldX = pivotWorld.x;
bone.worldY = pivotWorld.y;
bone.worldRot = rot;
const pts = renderPoly.map(p => {
const wp = transformPoint(p.x, p.y, pivotWorld.x, pivotWorld.y, rot);
return `${wp.x},${wp.y}`;
}).join(" ");
bones.push({
name: boneName,
worldX :pivotWorld.x,
worldY :pivotWorld.y,
z: bone.z,
points: pts,
worldRot: rot,
});
const childWorld = transformPoint(cx, cy, pivotWorld.x, pivotWorld.y, rot);
for (let name in bone.children) {
drawBone(entity, bone.children[name], name, childWorld.x, childWorld.y, rot);
}
}
function applyZMap(bone, boneName) {
if (zMap[boneName] !== undefined) {
bone.z = zMap[boneName];
}
for (const childName in bone.children) {
applyZMap(bone.children[childName], childName);
}
}
let bones = [];
function renderSkeleton(entity) {
const g = document.getElementById(entity.id);
g.innerHTML = "";
const p = entity;
const torso = p.skeleton.torso;
if (p.facing > 0) {
zMap.hat = 7;
zMap.head = 6; // front
zMap.torso = 5; // back
} else {
zMap.hat = 5;
zMap.head = 6; // front
zMap.torso = 7; // back
}
// Apply z-map BEFORE collecting bones
applyZMap(torso, "torso");
drawBone(entity, torso, "torso", p.x, p.y + p.skeletonOffsetY, 0);
// Sort by z (facing-aware)
if (p.facing > 0) {
bones.sort((a, b) => a.z - b.z);
} else {
bones.sort((a, b) => b.z - a.z);
}
// Render
for (let i = 0; i < bones.length; i++) {
const poly = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
poly.setAttribute("points", bones[i].points);
poly.setAttribute("fill", getBoneFill(entity,bones[i]));
poly.setAttribute("stroke", "grey");
poly.setAttribute("stroke-width", ".25");
g.appendChild(poly);
}
bones = [];
}
function getBoneFill(entity,bone){
if(entity.id ==="player"){
if(bone.name === "torso"){
return renderGradient(bone,"player_torso_grad")
}
if(bone.name === "upperArmL" || bone.name === "upperArmR"||bone.name === "lowerArmL" || bone.name === "lowerArmR"){
return "#ccf";
}
if(bone.name === "head"){
return "#fff";
}
return "#54f"
}else if(entity.type !== "boss"){
if(bone.name === "torso"){
return renderGradient(bone,`${entity.id}_torso_grad`);
}
if(bone.name === "upperArmL" || bone.name === "upperArmR"||bone.name === "lowerArmL" || bone.name === "lowerArmR"){
return "#d2c59a";
}
if(bone.name === "head"){
return "#fff";
}
return "#b8a878"
}else{
if(bone.name === "head"){
return "#fff";
}
return "#000"
}
}
function renderGradient(bone,gradId) {
const grad = document.getElementById(gradId);
const deg = bone.worldRot * (180 / Math.PI);
grad.setAttribute("gradientUnits", "userSpaceOnUse");
grad.setAttribute("x1", bone.worldX);
grad.setAttribute("y1", bone.worldY - 40);
grad.setAttribute("x2", bone.worldX);
grad.setAttribute("y2", bone.worldY + 40);
grad.setAttribute("gradientTransform", `rotate(${deg}, ${bone.worldX}, ${bone.worldY})`);
return "url(#"+gradId+")"
}
function renderHats(t){
for (const h of game.hats) {
// physics
h.x += h.vx;
h.y += h.vy;
h.vy += game.gravity * t * 3;
h.rotation += h.rotationSpeed * 2;
h.life -= t;
if (h.life <= 0) {
h.g.remove();
delete h
continue;
}
// update transform
h.g.setAttribute("transform",
`translate(${h.x},${h.y}) rotate(${h.rotation})`
);
// update polygon points (local coords)
h.node.setAttribute(
"points",
h.poly.map(p => `${p.x},${p.y}`).join(" ")
);
}
}
//physics
function applySeparation() {
const all = [game.player, ...game.enemies];
for (let i = 0; i < all.length; i++) {
for (let j = i + 1; j < all.length; j++) {
const a = all[i];
const b = all[j];
const dx = b.x - a.x;
const dy = b.y - a.y;
const dist = Math.hypot(dx, dy);
const minDist = (a.radius || 18) + (b.radius || 18);
if (dist < minDist && dist > 0.001) {
const overlap = minDist - dist;
const nx = dx / dist;
const ny = dy / dist;
const push = overlap * 0.5;
a.x -= nx * push;
a.y -= ny * push;
b.x += nx * push;
b.y += ny * push;
}
}
}
}
function updatePhysics(p, dt) {
const tolerance = 30;
p.onGround = false;
// 1. Save last position
const oldX = p.x;
const headY = p.y - p.height * .5;
const feetY = p.y + p.height * .5;
for (let plat of game.platforms) {
for (let seg of plat.segments) {
if (seg.isWall) continue;
const dx = seg.x2 - seg.x1;
const dy = seg.y2 - seg.y1;
const angle = seg.angle;
// horizontal + vertical bounding box check
if (
p.x >= seg.x1 && p.x <= seg.x2 &&
seg.maxY >= feetY - tolerance &&
seg.minY <= feetY + tolerance
) {
// slope interpolation
const t = (p.x - seg.x1) / (seg.x2 - seg.x1);
const platY = seg.y1 + (seg.y2 - seg.y1) * t;
const t2 = (feetY - seg.y1) / (seg.y2 - seg.y1);
const platXfeet = seg.x1 + (seg.x2 - seg.x1) * t2;
if (feetY < platY) continue;
// TOO STEEP wall + slide
if (Math.abs(angle) > maxClimbAngle ) {
let sliding = false;
if (p.x-5 <= platXfeet && dy < 0) {
sliding = true;
} else if(p.x+5 >= platXfeet && dy > 0){
sliding = true;
}
if(sliding){
p.vx = 0;
const slideSpeed = Math.abs(Math.sin(angle)) * 0.5;
p.vx += Math.sign(angle) * slideSpeed;
}
}
// NORMAL LANDING
if (p.vy >= 0 && p.y < platY) {
if (feetY >= platY - tolerance) {
p.y = platY - p.height * .5;
p.vy = 0;
p.inAir = false;
p.onGround = true;
}
}
}
}
}
// gravity
p.vy += game.gravity * dt;
const maxFallSpeed = 1.2;
if (p.vy > maxFallSpeed) p.vy = maxFallSpeed;
// friction
if (!p.inAir && p.moveInput === 0) {
p.vx *= 0.8;
if (Math.abs(p.vx) < 0.01) p.vx = 0;
}
}
function updateWallPhysics(p, dt) {
// 1. Save last position
const oldX = p.x;
const kneeY = p.y + p.height * .25;
const feetY = p.y + p.height * .5;
for (let plat of game.platforms) {
for (let seg of plat.segments) {
if (seg.isWall) {
if ( feetY -1 <= seg.minY || kneeY+1 >= seg.maxY) continue;// headY+1 >= seg.maxY
const left = p.x - p.width/2;
const right = p.x + p.width/2;
// hit from left
if (right > seg.x1 && p.x <= seg.x1 && (seg.oneWay === "left" || !seg.oneWay) ) {
p.x = oldX; // rollback X
p.vx = -(right - seg.x1) * .1;
break;
}
// hit from right
if (left < seg.x1 && p.x >= seg.x1 && (seg.oneWay === "right" || !seg.oneWay)) {
p.x = oldX; // rollback X
p.vx = (seg.x1 - left) * .1;
break;
}
}
}
}
p.x += p.vx * dt;
p.y += p.vy * dt;
}
function distance(a,b){
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.hypot(dx, dy);
}
function updateExit(exitDoor){
if(!exitDoor)return;
if (exitDoor.closed && !exitDoor.locked) {
// treat door as a wall segment
if (distance(game.player, exitDoor) < 100) {
openDoor(exitDoor);
}
return;
}else if(!exitDoor.locked && !exitDoor.closed && (game.player.verticalInput === 1 || game.player.wantJump) && distance(game.player, exitDoor) < 100){
loadLevel(++currentLevel)
game.score +500
respawnPlayer()
}
if(exitDoor.node === null)return;
if (Math.abs(exitDoor.targetScaleX - exitDoor.scaleX) < 0.1) {
return
}
const dtSec = dt * 0.001;
exitDoor.scaleX += (exitDoor.targetScaleX - exitDoor.scaleX) * dtSec * exitDoor.speed;
exitDoor.node.setAttribute(
"transform",
` scale(${exitDoor.scaleX}, 1)`
);
}
function updateDoors(dt) {
const doors = game.doors;
updateExit(game.doors.find(d => d.type === "exit"));
}
function openDoor(door) {
door.scaleX = 1;
door.closed = false;
}
function closeDoor() {
door.targetScaleX = 0.05;
door.closed = true;
}
function updateHud(player) {
const hpText = document.getElementById("hpText");
const scoreText = document.getElementById("scoreText");
const timeText = document.getElementById("timeText");
hpText.textContent = player.hitPoints;
hpText.setAttribute("fill", getHealthColor(player.hitPoints, player.maxPoints));
scoreText.textContent = `Score: ${game.score}`;
timeText.textContent = `Time: ${formatTime(game.time)}`;
}
function getHealthColor(hp, maxHp) {
const pct = hp / maxHp;
if (pct > 0.7) return "#00ff00"; // green
if (pct > 0.4) return "#ffff00"; // yellow
if (pct > 0.2) return "#ff8800"; // orange
return "#ff0000"; // red
}
function formatTime(t) {
const m = Math.floor(t / 60);
const s = Math.floor(t % 60);
return `${m}:${s.toString().padStart(2, "0")}`;
}
function showWinTally(game) {
const modal = document.getElementById("modal");
const time = formatTime(game.time);
const kills = game.totalKills;
const deaths = game.deaths;
const score = game.score;
document.getElementById("modalText").innerHTML = `
<text x="50%" y="20%" text-anchor="middle" font-size="42" fill="white">ENEMIES DEFEATED</text>
<text x="50%" y="35%" text-anchor="middle" font-size="28" fill="white">Time: ${time}</text>
<text x="50%" y="42%" text-anchor="middle" font-size="28" fill="white">Kills: ${kills}</text>
<text x="50%" y="49%" text-anchor="middle" font-size="28" fill="white">Deaths: ${deaths}</text>
<text x="50%" y="56%" text-anchor="middle" font-size="28" fill="white">Score: ${score}</text>
<text x="50%" y="63%" text-anchor="middle" font-size="28" fill="white">Press Start or Escape to Restart</text>
`;
modal.style.opacity = 1;
}
function menu(startPressed,move){
// PAUSE
if (startPressed && !prevStartPressed) {
if (game.state === STATE.PAUSED){
game.state = STATE.PLAYING;
document.getElementById("modal").style.opacity = 0;
}else if(game.state === STATE.PLAYING){
game.state = STATE.PAUSED;
document.getElementById("modal").style.opacity = 1;
}
}
if(game.state === STATE.PLAYING)return;
// STATS / RESART
if(game.state === STATE.WIN && startPressed && !prevStartPressed){
resetStats();
currentLevel = 1;
loadLevel(currentLevel);
document.getElementById("modal").style.opacity = 0;
game.state = STATE.PLAYING;
}else if(game.state === STATE.WIN) return;
// VOLUME
if(move === -1)volume -= 0.02;
if(move === 1)volume += 0.02;
volume = Math.max(0, Math.min(1, volume));
document.getElementById("modalText").innerHTML = `
<text x="50%" y="30%" text-anchor="middle" font-size="42" fill="white">PAUSED</text>
<text x="50%" y="40%" text-anchor="middle" font-size="28" fill="white">VOLUME: ${(volume * 100)|0}%</text>
<text x="50%" y="45%" text-anchor="middle" font-size="28" fill="white">CONTROLS: </text>
<text x="50%" y="52%" text-anchor="middle" font-size="24" fill="white">Move Left "a", Move Right "d", Jump "Spacebar" </text>
<text x="50%" y="57%" text-anchor="middle" font-size="24" fill="white">Crouch/Attack Low "s", Attack High "w"</text>
<text x="50%" y="62%" text-anchor="middle" font-size="24" fill="white">Punch "l", Kick ";" </text>
`;
}
function updatePlayerAudio(){
if((game.player.wantKick || game.player.wantPunch) && game.player.prevAction !== game.player.actionState){
if((game.player.prevAction === "walkPunch" && game.player.actionState === "punch") ||(game.player.prevAction === "punch" && game.player.actionState === "walkPunch" ))return;
if((game.player.prevAction === "walkHighPunch" && game.player.actionState === "highPunch") ||(game.player.prevAction === "highPunch" && game.player.actionState === "walkHighPunch" ))return;
sfxHuh();
for (const enemy of game.enemies) {
enemy.prevHit = false;
}
}
}
function gameStatus() {
const p = game.player;
// Cleanup: remove dead enemies
game.enemies = game.enemies.filter(e => {
if (e.state === "dead") {
game.kills++;
game.totalKills++;
return false;
}
return true;
});
// Player death check
if (p.y > 4000 || p.hitPoints <= 0) {
game.score = Math.max(game.score -500,0);
game.deaths++;
respawnPlayer();
}
// Enemy death check
for (const enemy of game.enemies) {
if (enemy.hitPoints <= 0 && enemy.state !== "dying") {
game.score += 20;
enemy.state = "dying";
enemy.actionState = "none"
enemy.moveState = "dying";
}else if(enemy.y > 4000){
enemy.state = "dead";
}
}
if (game.kills >= game.objective.count) {
const exit = game.doors.find(d => d.type === "exit");
if(exit){
exit.locked = false;
}else{
game.state = STATE.WIN;
showWinTally(game);
}
}
}
// Main loop
last = performance.now();
acc = 0;
const dt = 16;
function loop(now) {
let delta = now - last;
if (delta > 200) delta = 200;
acc += delta;
last = now;
while (acc >= dt) {
// 1. INPUT
gatherInput(dt);
if (game.state === STATE.PLAYING) {
game.time += dt / 1000;
updateSpawn(dt);
// 2. APPLY INTENT (movement input velocity)
applyIntent(game.player,dt);
// 3. PHYSICS (velocity position, collisions, onGround)
updatePhysics(game.player,dt);
updateRecoil(game.player, dt);
for (const enemy of game.enemies) {
applyIntent(enemy,dt);
updateEnemyAI(enemy)
updatePhysics(enemy, dt);
updateRecoil(enemy, dt);
}
applySeparation();
// 4. MOVEMENT STATE (idle/walk/jump/fall)
updateMovementState(game.player);
// 5. ACTION STATE (punch/kick/jumpPunch/jumpKick)
game.player.prevAction = game.player.actionState;
updateActionState(game.player);
updatePlayerAudio();
// 6. ANIMATION STATE (combine movement + action)
selectAnimationState(game.player);
updateAnimation(game.player,dt);
smoothBoneRotations(game.player,dt);
for (const enemy of game.enemies) {
updateMovementState(enemy);
updateActionState(enemy);
selectAnimationState(enemy);
updateAnimation(enemy,dt);
smoothBoneRotations(enemy,dt);
}
playerEnemyCollision();
updateWallPhysics(game.player,dt);
for (const enemy of game.enemies) {
updateWallPhysics(enemy, dt);
}
updateHud(game.player)
updateDoors(dt);
updateCamera(dt);
updateViewBox();
gameStatus();
}
acc -= dt;
}
// 8. RENDER
render(dt);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
*/ |
| SC Arguments: [Name:SC_ACTION Type:uint64 Value:'1' Name:SC_CODE Type:string Value:'// Copyright 2024. Civilware. All rights reserved.
// TELA Decentralized Web Document (TELA-DOC-1)
Function InitializePrivate() Uint64
10 IF init() == 0 THEN GOTO 30
20 RETURN 1
30 STORE("nameHdr", "main.js")
31 STORE("descrHdr", "Main JS file")
32 STORE("iconURLHdr", "")
33 STORE("dURL", "main.js")
34 STORE("docType", "TELA-JS-1")
35 STORE("subDir", "")
36 STORE("fileCheckC", "223f65df5b5f9c2e1b606beb006143be6e4633687942eb2fe76308bd08635867")
37 STORE("fileCheckS", "11dd619ff9df671e0e957904534f2f46ae2890b1ef47af63112ab0b474edd3bf")
100 RETURN 0
End Function
Function init() Uint64
10 IF EXISTS("owner") == 0 THEN GOTO 30
20 RETURN 1
30 STORE("owner", address())
50 STORE("docVersion", "1.0.0")
60 STORE("hash", HEX(TXID()))
70 STORE("likes", 0)
80 STORE("dislikes", 0)
100 RETURN 0
End Function
Function address() String
10 DIM s as String
20 LET s = SIGNER()
30 IF IS_ADDRESS_VALID(s) THEN GOTO 50
40 RETURN "anon"
50 RETURN ADDRESS_STRING(s)
End Function
Function Rate(r Uint64) Uint64
10 DIM addr as String
15 LET addr = address()
16 IF r < 100 && EXISTS(addr) == 0 && addr != "anon" THEN GOTO 30
20 RETURN 1
30 STORE(addr, ""+r+"_"+BLOCK_HEIGHT())
40 IF r < 50 THEN GOTO 70
50 STORE("likes", LOAD("likes")+1)
60 RETURN 0
70 STORE("dislikes", LOAD("dislikes")+1)
100 RETURN 0
End Function
/*
//----------
// Rendering
function render(t) {
// depth sort
const living = [game.player, ...game.enemies.filter(e => !e.dead)];
for (const e of living) e.depth = e.y + e.height;
living.sort((a, b) => a.depth - b.depth);
// reorder DOM groups
const layer = document.getElementById("players");
for (const e of living) {
const g = document.getElementById(e.id) || createSVGGroup(e.id);
layer.appendChild(g); // append moves it to correct depth
}
// now render bones
renderSkeleton(game.player);
for (const enemy of game.enemies) {
renderSkeleton(enemy);
deathAnimation(enemy);
}
renderHats(t)
}
function transformPoint(px, py, ox, oy, rot) {
const cos = Math.cos(rot);
const sin = Math.sin(rot);
return {
x: ox + px * cos - py * sin,
y: oy + px * sin + py * cos
};
}
function mirrorPoly(poly) {
return poly.map(p => ({ x: -p.x, y: p.y }));
}
function drawBone(entity, bone, boneName, parentX, parentY, parentRot) {
const [px, py] = bone.pivot;
const [cx, cy] = bone.childOrigin;
const pivotWorld = transformPoint(px, py, parentX, parentY, parentRot);
let rot = 0
if(entity.facing < 0) {
rot = parentRot - (bone.rotation || 0)
if(boneName === "hat"){
bone.flipX = true;
}
}else {
rot = parentRot + (bone.rotation || 0)
bone.flipX = false;
}
let renderPoly = {}
if (bone.flipX){
renderPoly = mirrorPoly(bone.poly);
}else{
renderPoly = bone.poly;
}
// STORE WORLD TRANSFORM FOR COLLISION DETECTION
bone.worldX = pivotWorld.x;
bone.worldY = pivotWorld.y;
bone.worldRot = rot;
const pts = renderPoly.map(p => {
const wp = transformPoint(p.x, p.y, pivotWorld.x, pivotWorld.y, rot);
return `${wp.x},${wp.y}`;
}).join(" ");
bones.push({
name: boneName,
worldX :pivotWorld.x,
worldY :pivotWorld.y,
z: bone.z,
points: pts,
worldRot: rot,
});
const childWorld = transformPoint(cx, cy, pivotWorld.x, pivotWorld.y, rot);
for (let name in bone.children) {
drawBone(entity, bone.children[name], name, childWorld.x, childWorld.y, rot);
}
}
function applyZMap(bone, boneName) {
if (zMap[boneName] !== undefined) {
bone.z = zMap[boneName];
}
for (const childName in bone.children) {
applyZMap(bone.children[childName], childName);
}
}
let bones = [];
function renderSkeleton(entity) {
const g = document.getElementById(entity.id);
g.innerHTML = "";
const p = entity;
const torso = p.skeleton.torso;
if (p.facing > 0) {
zMap.hat = 7;
zMap.head = 6; // front
zMap.torso = 5; // back
} else {
zMap.hat = 5;
zMap.head = 6; // front
zMap.torso = 7; // back
}
// Apply z-map BEFORE collecting bones
applyZMap(torso, "torso");
drawBone(entity, torso, "torso", p.x, p.y + p.skeletonOffsetY, 0);
// Sort by z (facing-aware)
if (p.facing > 0) {
bones.sort((a, b) => a.z - b.z);
} else {
bones.sort((a, b) => b.z - a.z);
}
// Render
for (let i = 0; i < bones.length; i++) {
const poly = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
poly.setAttribute("points", bones[i].points);
poly.setAttribute("fill", getBoneFill(entity,bones[i]));
poly.setAttribute("stroke", "grey");
poly.setAttribute("stroke-width", ".25");
g.appendChild(poly);
}
bones = [];
}
function getBoneFill(entity,bone){
if(entity.id ==="player"){
if(bone.name === "torso"){
return renderGradient(bone,"player_torso_grad")
}
if(bone.name === "upperArmL" || bone.name === "upperArmR"||bone.name === "lowerArmL" || bone.name === "lowerArmR"){
return "#ccf";
}
if(bone.name === "head"){
return "#fff";
}
return "#54f"
}else if(entity.type !== "boss"){
if(bone.name === "torso"){
return renderGradient(bone,`${entity.id}_torso_grad`);
}
if(bone.name === "upperArmL" || bone.name === "upperArmR"||bone.name === "lowerArmL" || bone.name === "lowerArmR"){
return "#d2c59a";
}
if(bone.name === "head"){
return "#fff";
}
return "#b8a878"
}else{
if(bone.name === "head"){
return "#fff";
}
return "#000"
}
}
function renderGradient(bone,gradId) {
const grad = document.getElementById(gradId);
const deg = bone.worldRot * (180 / Math.PI);
grad.setAttribute("gradientUnits", "userSpaceOnUse");
grad.setAttribute("x1", bone.worldX);
grad.setAttribute("y1", bone.worldY - 40);
grad.setAttribute("x2", bone.worldX);
grad.setAttribute("y2", bone.worldY + 40);
grad.setAttribute("gradientTransform", `rotate(${deg}, ${bone.worldX}, ${bone.worldY})`);
return "url(#"+gradId+")"
}
function renderHats(t){
for (const h of game.hats) {
// physics
h.x += h.vx;
h.y += h.vy;
h.vy += game.gravity * t * 3;
h.rotation += h.rotationSpeed * 2;
h.life -= t;
if (h.life <= 0) {
h.g.remove();
delete h
continue;
}
// update transform
h.g.setAttribute("transform",
`translate(${h.x},${h.y}) rotate(${h.rotation})`
);
// update polygon points (local coords)
h.node.setAttribute(
"points",
h.poly.map(p => `${p.x},${p.y}`).join(" ")
);
}
}
//physics
function applySeparation() {
const all = [game.player, ...game.enemies];
for (let i = 0; i < all.length; i++) {
for (let j = i + 1; j < all.length; j++) {
const a = all[i];
const b = all[j];
const dx = b.x - a.x;
const dy = b.y - a.y;
const dist = Math.hypot(dx, dy);
const minDist = (a.radius || 18) + (b.radius || 18);
if (dist < minDist && dist > 0.001) {
const overlap = minDist - dist;
const nx = dx / dist;
const ny = dy / dist;
const push = overlap * 0.5;
a.x -= nx * push;
a.y -= ny * push;
b.x += nx * push;
b.y += ny * push;
}
}
}
}
function updatePhysics(p, dt) {
const tolerance = 30;
p.onGround = false;
// 1. Save last position
const oldX = p.x;
const headY = p.y - p.height * .5;
const feetY = p.y + p.height * .5;
for (let plat of game.platforms) {
for (let seg of plat.segments) {
if (seg.isWall) continue;
const dx = seg.x2 - seg.x1;
const dy = seg.y2 - seg.y1;
const angle = seg.angle;
// horizontal + vertical bounding box check
if (
p.x >= seg.x1 && p.x <= seg.x2 &&
seg.maxY >= feetY - tolerance &&
seg.minY <= feetY + tolerance
) {
// slope interpolation
const t = (p.x - seg.x1) / (seg.x2 - seg.x1);
const platY = seg.y1 + (seg.y2 - seg.y1) * t;
const t2 = (feetY - seg.y1) / (seg.y2 - seg.y1);
const platXfeet = seg.x1 + (seg.x2 - seg.x1) * t2;
if (feetY < platY) continue;
// TOO STEEP wall + slide
if (Math.abs(angle) > maxClimbAngle ) {
let sliding = false;
if (p.x-5 <= platXfeet && dy < 0) {
sliding = true;
} else if(p.x+5 >= platXfeet && dy > 0){
sliding = true;
}
if(sliding){
p.vx = 0;
const slideSpeed = Math.abs(Math.sin(angle)) * 0.5;
p.vx += Math.sign(angle) * slideSpeed;
}
}
// NORMAL LANDING
if (p.vy >= 0 && p.y < platY) {
if (feetY >= platY - tolerance) {
p.y = platY - p.height * .5;
p.vy = 0;
p.inAir = false;
p.onGround = true;
}
}
}
}
}
// gravity
p.vy += game.gravity * dt;
const maxFallSpeed = 1.2;
if (p.vy > maxFallSpeed) p.vy = maxFallSpeed;
// friction
if (!p.inAir && p.moveInput === 0) {
p.vx *= 0.8;
if (Math.abs(p.vx) < 0.01) p.vx = 0;
}
}
function updateWallPhysics(p, dt) {
// 1. Save last position
const oldX = p.x;
const kneeY = p.y + p.height * .25;
const feetY = p.y + p.height * .5;
for (let plat of game.platforms) {
for (let seg of plat.segments) {
if (seg.isWall) {
if ( feetY -1 <= seg.minY || kneeY+1 >= seg.maxY) continue;// headY+1 >= seg.maxY
const left = p.x - p.width/2;
const right = p.x + p.width/2;
// hit from left
if (right > seg.x1 && p.x <= seg.x1 && (seg.oneWay === "left" || !seg.oneWay) ) {
p.x = oldX; // rollback X
p.vx = -(right - seg.x1) * .1;
break;
}
// hit from right
if (left < seg.x1 && p.x >= seg.x1 && (seg.oneWay === "right" || !seg.oneWay)) {
p.x = oldX; // rollback X
p.vx = (seg.x1 - left) * .1;
break;
}
}
}
}
p.x += p.vx * dt;
p.y += p.vy * dt;
}
function distance(a,b){
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.hypot(dx, dy);
}
function updateExit(exitDoor){
if(!exitDoor)return;
if (exitDoor.closed && !exitDoor.locked) {
// treat door as a wall segment
if (distance(game.player, exitDoor) < 100) {
openDoor(exitDoor);
}
return;
}else if(!exitDoor.locked && !exitDoor.closed && (game.player.verticalInput === 1 || game.player.wantJump) && distance(game.player, exitDoor) < 100){
loadLevel(++currentLevel)
game.score +500
respawnPlayer()
}
if(exitDoor.node === null)return;
if (Math.abs(exitDoor.targetScaleX - exitDoor.scaleX) < 0.1) {
return
}
const dtSec = dt * 0.001;
exitDoor.scaleX += (exitDoor.targetScaleX - exitDoor.scaleX) * dtSec * exitDoor.speed;
exitDoor.node.setAttribute(
"transform",
` scale(${exitDoor.scaleX}, 1)`
);
}
function updateDoors(dt) {
const doors = game.doors;
updateExit(game.doors.find(d => d.type === "exit"));
}
function openDoor(door) {
door.scaleX = 1;
door.closed = false;
}
function closeDoor() {
door.targetScaleX = 0.05;
door.closed = true;
}
function updateHud(player) {
const hpText = document.getElementById("hpText");
const scoreText = document.getElementById("scoreText");
const timeText = document.getElementById("timeText");
hpText.textContent = player.hitPoints;
hpText.setAttribute("fill", getHealthColor(player.hitPoints, player.maxPoints));
scoreText.textContent = `Score: ${game.score}`;
timeText.textContent = `Time: ${formatTime(game.time)}`;
}
function getHealthColor(hp, maxHp) {
const pct = hp / maxHp;
if (pct > 0.7) return "#00ff00"; // green
if (pct > 0.4) return "#ffff00"; // yellow
if (pct > 0.2) return "#ff8800"; // orange
return "#ff0000"; // red
}
function formatTime(t) {
const m = Math.floor(t / 60);
const s = Math.floor(t % 60);
return `${m}:${s.toString().padStart(2, "0")}`;
}
function showWinTally(game) {
const modal = document.getElementById("modal");
const time = formatTime(game.time);
const kills = game.totalKills;
const deaths = game.deaths;
const score = game.score;
document.getElementById("modalText").innerHTML = `
<text x="50%" y="20%" text-anchor="middle" font-size="42" fill="white">ENEMIES DEFEATED</text>
<text x="50%" y="35%" text-anchor="middle" font-size="28" fill="white">Time: ${time}</text>
<text x="50%" y="42%" text-anchor="middle" font-size="28" fill="white">Kills: ${kills}</text>
<text x="50%" y="49%" text-anchor="middle" font-size="28" fill="white">Deaths: ${deaths}</text>
<text x="50%" y="56%" text-anchor="middle" font-size="28" fill="white">Score: ${score}</text>
<text x="50%" y="63%" text-anchor="middle" font-size="28" fill="white">Press Start or Escape to Restart</text>
`;
modal.style.opacity = 1;
}
function menu(startPressed,move){
// PAUSE
if (startPressed && !prevStartPressed) {
if (game.state === STATE.PAUSED){
game.state = STATE.PLAYING;
document.getElementById("modal").style.opacity = 0;
}else if(game.state === STATE.PLAYING){
game.state = STATE.PAUSED;
document.getElementById("modal").style.opacity = 1;
}
}
if(game.state === STATE.PLAYING)return;
// STATS / RESART
if(game.state === STATE.WIN && startPressed && !prevStartPressed){
resetStats();
currentLevel = 1;
loadLevel(currentLevel);
document.getElementById("modal").style.opacity = 0;
game.state = STATE.PLAYING;
}else if(game.state === STATE.WIN) return;
// VOLUME
if(move === -1)volume -= 0.02;
if(move === 1)volume += 0.02;
volume = Math.max(0, Math.min(1, volume));
document.getElementById("modalText").innerHTML = `
<text x="50%" y="30%" text-anchor="middle" font-size="42" fill="white">PAUSED</text>
<text x="50%" y="40%" text-anchor="middle" font-size="28" fill="white">VOLUME: ${(volume * 100)|0}%</text>
<text x="50%" y="45%" text-anchor="middle" font-size="28" fill="white">CONTROLS: </text>
<text x="50%" y="52%" text-anchor="middle" font-size="24" fill="white">Move Left "a", Move Right "d", Jump "Spacebar" </text>
<text x="50%" y="57%" text-anchor="middle" font-size="24" fill="white">Crouch/Attack Low "s", Attack High "w"</text>
<text x="50%" y="62%" text-anchor="middle" font-size="24" fill="white">Punch "l", Kick ";" </text>
`;
}
function updatePlayerAudio(){
if((game.player.wantKick || game.player.wantPunch) && game.player.prevAction !== game.player.actionState){
if((game.player.prevAction === "walkPunch" && game.player.actionState === "punch") ||(game.player.prevAction === "punch" && game.player.actionState === "walkPunch" ))return;
if((game.player.prevAction === "walkHighPunch" && game.player.actionState === "highPunch") ||(game.player.prevAction === "highPunch" && game.player.actionState === "walkHighPunch" ))return;
sfxHuh();
for (const enemy of game.enemies) {
enemy.prevHit = false;
}
}
}
function gameStatus() {
const p = game.player;
// Cleanup: remove dead enemies
game.enemies = game.enemies.filter(e => {
if (e.state === "dead") {
game.kills++;
game.totalKills++;
return false;
}
return true;
});
// Player death check
if (p.y > 4000 || p.hitPoints <= 0) {
game.score = Math.max(game.score -500,0);
game.deaths++;
respawnPlayer();
}
// Enemy death check
for (const enemy of game.enemies) {
if (enemy.hitPoints <= 0 && enemy.state !== "dying") {
game.score += 20;
enemy.state = "dying";
enemy.actionState = "none"
enemy.moveState = "dying";
}else if(enemy.y > 4000){
enemy.state = "dead";
}
}
if (game.kills >= game.objective.count) {
const exit = game.doors.find(d => d.type === "exit");
if(exit){
exit.locked = false;
}else{
game.state = STATE.WIN;
showWinTally(game);
}
}
}
// Main loop
last = performance.now();
acc = 0;
const dt = 16;
function loop(now) {
let delta = now - last;
if (delta > 200) delta = 200;
acc += delta;
last = now;
while (acc >= dt) {
// 1. INPUT
gatherInput(dt);
if (game.state === STATE.PLAYING) {
game.time += dt / 1000;
updateSpawn(dt);
// 2. APPLY INTENT (movement input velocity)
applyIntent(game.player,dt);
// 3. PHYSICS (velocity position, collisions, onGround)
updatePhysics(game.player,dt);
updateRecoil(game.player, dt);
for (const enemy of game.enemies) {
applyIntent(enemy,dt);
updateEnemyAI(enemy)
updatePhysics(enemy, dt);
updateRecoil(enemy, dt);
}
applySeparation();
// 4. MOVEMENT STATE (idle/walk/jump/fall)
updateMovementState(game.player);
// 5. ACTION STATE (punch/kick/jumpPunch/jumpKick)
game.player.prevAction = game.player.actionState;
updateActionState(game.player);
updatePlayerAudio();
// 6. ANIMATION STATE (combine movement + action)
selectAnimationState(game.player);
updateAnimation(game.player,dt);
smoothBoneRotations(game.player,dt);
for (const enemy of game.enemies) {
updateMovementState(enemy);
updateActionState(enemy);
selectAnimationState(enemy);
updateAnimation(enemy,dt);
smoothBoneRotations(enemy,dt);
}
playerEnemyCollision();
updateWallPhysics(game.player,dt);
for (const enemy of game.enemies) {
updateWallPhysics(enemy, dt);
}
updateHud(game.player)
updateDoors(dt);
updateCamera(dt);
updateViewBox();
gameStatus();
}
acc -= dt;
}
// 8. RENDER
render(dt);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
*/'] |