FREE-FISH
by MegaFlare513506 lines51.1 KB
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>FREE-FISH</title>
<style>
*{
box-sizing:border-box;
margin:0;
padding:0;
user-select:none;
-webkit-user-select:none;
-webkit-tap-highlight-color:transparent;
}
html,body{
width:100%;
height:100%;
overflow:hidden;
background:#020817;
font-family:Arial,Helvetica,sans-serif;
color:white;
}
button{
font-family:inherit;
border:0;
cursor:pointer;
}
.screen{
position:absolute;
inset:0;
display:none;
}
.screen.active{
display:block;
}
/* =========================
LOBBY
========================= */
#lobby{
background:
radial-gradient(circle at 50% 30%,#073c70 0%,#03172d 38%,#010711 100%);
}
#lobbyCanvas{
position:absolute;
inset:0;
width:100%;
height:100%;
}
.lobbyUI{
position:absolute;
inset:0;
pointer-events:none;
}
.logo{
position:absolute;
top:20px;
left:50%;
transform:translateX(-50%);
font-size:clamp(30px,6vw,64px);
font-weight:1000;
letter-spacing:5px;
color:#fff;
text-shadow:
0 0 8px #20cfff,
0 0 22px #008cff,
0 0 45px #006eff;
}
.logo span{
color:#29d9ff;
}
.profile{
position:absolute;
left:20px;
top:20px;
width:230px;
padding:14px;
border-radius:16px;
background:rgba(2,12,28,.78);
border:1px solid rgba(45,190,255,.45);
box-shadow:0 0 25px rgba(0,150,255,.18);
pointer-events:auto;
}
.profileName{
font-size:20px;
font-weight:900;
}
.profileLevel{
margin-top:4px;
color:#7edcff;
font-size:13px;
}
.currency{
display:flex;
gap:8px;
margin-top:12px;
}
.money{
flex:1;
padding:8px;
border-radius:10px;
background:rgba(255,255,255,.06);
text-align:center;
font-weight:900;
}
.sideMenu{
position:absolute;
left:20px;
bottom:20px;
display:flex;
flex-direction:column;
gap:8px;
pointer-events:auto;
}
.menuBtn{
min-width:180px;
padding:11px 16px;
color:white;
background:rgba(4,23,46,.88);
border:1px solid rgba(49,187,255,.4);
border-radius:12px;
font-weight:800;
text-align:left;
}
.menuBtn:hover{
background:#073867;
}
.playArea{
position:absolute;
left:50%;
top:52%;
transform:translate(-50%,-50%);
text-align:center;
pointer-events:auto;
}
.bigFish{
width:210px;
height:120px;
margin:auto;
position:relative;
animation:floatFish 2.2s ease-in-out infinite;
}
.bigFishBody{
position:absolute;
left:45px;
top:25px;
width:120px;
height:70px;
border-radius:55% 48% 48% 55%;
background:#159cff;
box-shadow:0 0 30px #159cff;
}
.bigFishTail{
position:absolute;
left:20px;
top:28px;
width:0;
height:0;
border-top:32px solid transparent;
border-bottom:32px solid transparent;
border-right:55px solid #0645b5;
}
.bigFishEye{
position:absolute;
right:60px;
top:40px;
width:13px;
height:13px;
background:white;
border-radius:50%;
}
.bigFishEye:after{
content:"";
position:absolute;
width:6px;
height:6px;
background:#001225;
border-radius:50%;
left:4px;
top:4px;
}
.playBtn{
margin-top:25px;
padding:18px 70px;
border-radius:15px;
font-size:25px;
font-weight:1000;
color:white;
background:linear-gradient(135deg,#00a8ff,#0056ff);
box-shadow:
0 0 15px #008cff,
0 0 40px rgba(0,140,255,.45);
}
.playBtn:active{
transform:scale(.96);
}
.bottomInfo{
position:absolute;
right:20px;
bottom:20px;
display:flex;
gap:10px;
pointer-events:auto;
}
.infoCard{
min-width:150px;
padding:12px;
border-radius:12px;
background:rgba(3,18,36,.82);
border:1px solid rgba(50,190,255,.35);
}
.infoTitle{
color:#6edcff;
font-size:12px;
font-weight:900;
}
.infoValue{
margin-top:4px;
font-weight:900;
}
/* =========================
GAME
========================= */
#game{
background:#03182b;
}
#gameWorld{
position:absolute;
width:5500px;
height:5500px;
left:0;
top:0;
transform-origin:0 0;
background:
radial-gradient(circle at 50% 50%,rgba(0,170,255,.25),transparent 35%),
repeating-radial-gradient(
circle,
rgba(255,255,255,.035) 0 2px,
transparent 2px 55px
),
linear-gradient(135deg,#064d73,#031d40 50%,#021225);
}
.worldGrid{
position:absolute;
inset:0;
background-image:
linear-gradient(rgba(71,214,255,.04) 1px,transparent 1px),
linear-gradient(90deg,rgba(71,214,255,.04) 1px,transparent 1px);
background-size:100px 100px;
}
#npcLayer{
position:absolute;
inset:0;
}
.npc,
.playerFish{
position:absolute;
width:70px;
height:42px;
transform-origin:50% 50%;
pointer-events:auto;
}
.fishBody{
position:absolute;
left:18px;
top:6px;
width:44px;
height:30px;
border-radius:55% 48% 48% 55%;
box-shadow:0 0 12px currentColor;
}
.fishTail{
position:absolute;
left:0;
top:8px;
width:0;
height:0;
border-top:13px solid transparent;
border-bottom:13px solid transparent;
border-right:23px solid currentColor;
}
.fishEye{
position:absolute;
left:51px;
top:11px;
width:7px;
height:7px;
background:white;
border-radius:50%;
z-index:3;
}
.fishEye:after{
content:"";
position:absolute;
width:3px;
height:3px;
left:2px;
top:2px;
background:#001;
border-radius:50%;
}
.fishFin{
position:absolute;
left:34px;
top:29px;
width:18px;
height:9px;
background:currentColor;
clip-path:polygon(0 0,100% 0,65% 100%);
opacity:.8;
}
.npc.dead{
opacity:.12;
filter:grayscale(1);
pointer-events:none;
}
.healthBar{
position:absolute;
left:12px;
top:-11px;
width:48px;
height:5px;
background:rgba(0,0,0,.6);
border-radius:10px;
overflow:hidden;
}
.healthFill{
height:100%;
width:100%;
background:#31ff70;
}
.nameTag{
position:absolute;
top:-28px;
left:50%;
transform:translateX(-50%);
white-space:nowrap;
font-size:9px;
font-weight:900;
text-shadow:0 1px 3px black;
}
.power{
position:absolute;
width:45px;
height:45px;
border-radius:50%;
display:flex;
align-items:center;
justify-content:center;
font-size:24px;
font-weight:1000;
animation:powerPulse .8s infinite alternate;
box-shadow:0 0 20px currentColor;
border:2px solid rgba(255,255,255,.75);
}
.power.speed{
color:#55d9ff;
background:rgba(0,130,255,.5);
}
.power.life{
color:#55ff76;
background:rgba(0,190,70,.5);
}
.power.size{
color:#ff9bff;
background:rgba(180,0,255,.5);
}
.power.damage{
color:#ff6a55;
background:rgba(255,40,0,.5);
}
#gameHUD{
position:absolute;
inset:0;
pointer-events:none;
}
.topHUD{
position:absolute;
top:12px;
left:12px;
right:12px;
display:flex;
justify-content:space-between;
align-items:flex-start;
}
.hudBox{
padding:9px 13px;
border-radius:11px;
background:rgba(0,10,25,.72);
border:1px solid rgba(73,202,255,.4);
backdrop-filter:blur(5px);
font-weight:900;
}
.hudBox small{
display:block;
color:#7bdfff;
font-size:9px;
margin-bottom:3px;
}
#minimap{
position:absolute;
right:15px;
top:65px;
width:150px;
height:150px;
border-radius:12px;
background:rgba(0,25,50,.78);
border:2px solid rgba(69,210,255,.5);
overflow:hidden;
}
.mapDot{
position:absolute;
width:4px;
height:4px;
border-radius:50%;
background:#aaa;
}
.mapPlayer{
width:8px;
height:8px;
background:white;
box-shadow:0 0 8px white;
}
#joystick{
position:absolute;
left:25px;
bottom:25px;
width:130px;
height:130px;
border-radius:50%;
background:rgba(0,35,65,.62);
border:2px solid rgba(90,210,255,.5);
pointer-events:auto;
touch-action:none;
}
#stick{
position:absolute;
width:55px;
height:55px;
left:37px;
top:37px;
border-radius:50%;
background:rgba(40,180,255,.7);
border:2px solid rgba(255,255,255,.7);
box-shadow:0 0 18px rgba(0,170,255,.7);
}
#attackBtn{
position:absolute;
right:25px;
bottom:30px;
width:105px;
height:105px;
border-radius:50%;
color:white;
background:rgba(220,35,55,.78);
border:3px solid rgba(255,255,255,.6);
font-weight:1000;
font-size:17px;
pointer-events:auto;
box-shadow:0 0 25px rgba(255,30,50,.5);
}
#backBtn{
position:absolute;
right:175px;
bottom:35px;
padding:12px 16px;
border-radius:11px;
background:rgba(0,10,25,.8);
color:white;
border:1px solid rgba(100,210,255,.5);
pointer-events:auto;
font-weight:900;
}
#powerInfo{
position:absolute;
left:50%;
bottom:15px;
transform:translateX(-50%);
padding:8px 14px;
border-radius:10px;
background:rgba(0,10,25,.75);
font-size:11px;
font-weight:900;
}
#gameOver{
position:absolute;
inset:0;
display:none;
align-items:center;
justify-content:center;
background:rgba(0,4,12,.72);
z-index:100;
}
.gameOverBox{
width:min(420px,90vw);
padding:25px;
text-align:center;
border-radius:18px;
background:#06172b;
border:1px solid #2ccaff;
box-shadow:0 0 40px rgba(0,150,255,.4);
}
.gameOverBox h1{
font-size:35px;
margin-bottom:10px;
}
.gameOverBox p{
margin:8px;
color:#9bdfff;
}
.actionBtn{
margin-top:12px;
padding:12px 22px;
border-radius:10px;
color:white;
background:#007cff;
font-weight:900;
}
/* =========================
MODAL
========================= */
#modal{
position:absolute;
inset:0;
display:none;
align-items:center;
justify-content:center;
background:rgba(0,4,12,.78);
z-index:500;
}
.modalBox{
width:min(800px,92vw);
max-height:88vh;
overflow:auto;
padding:20px;
border-radius:18px;
background:#06172b;
border:1px solid #28caff;
box-shadow:0 0 45px rgba(0,150,255,.35);
}
.modalHeader{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:15px;
}
.closeBtn{
width:35px;
height:35px;
border-radius:9px;
background:#142b45;
color:white;
font-size:18px;
}
.skinGrid{
display:grid;
grid-template-columns:repeat(4,1fr);
gap:10px;
}
.skinCard{
padding:10px;
border-radius:12px;
background:#0b243e;
border:1px solid rgba(60,190,255,.25);
text-align:center;
}
.skinPreview{
width:75px;
height:45px;
position:relative;
margin:0 auto 7px;
}
.skinCard button{
margin-top:7px;
width:100%;
padding:7px;
border-radius:8px;
background:#087dca;
color:white;
font-weight:800;
}
.skinPrice{
font-size:10px;
color:#8adfff;
}
.rankingRow{
display:flex;
justify-content:space-between;
padding:10px;
margin-bottom:6px;
border-radius:9px;
background:#0a233d;
}
.codeBox input,
.nameInput{
width:100%;
padding:12px;
border-radius:9px;
border:1px solid #247fa8;
outline:none;
background:#031425;
color:white;
font-size:16px;
}
.modalAction{
margin-top:10px;
padding:11px 18px;
border-radius:9px;
background:#008dff;
color:white;
font-weight:900;
}
.settingsRow{
display:flex;
justify-content:space-between;
padding:12px 0;
border-bottom:1px solid rgba(255,255,255,.08);
}
@keyframes floatFish{
0%,100%{transform:translateY(0)}
50%{transform:translateY(-10px)}
}
@keyframes powerPulse{
from{transform:scale(.92)}
to{transform:scale(1.08)}
}
@media(max-width:700px){
.profile{
width:175px;
padding:9px;
}
.profileName{
font-size:15px;
}
.sideMenu{
left:10px;
bottom:10px;
}
.menuBtn{
min-width:135px;
padding:8px;
font-size:11px;
}
.bottomInfo{
right:10px;
bottom:10px;
}
.infoCard{
min-width:100px;
padding:8px;
font-size:10px;
}
#minimap{
width:110px;
height:110px;
}
.skinGrid{
grid-template-columns:repeat(3,1fr);
}
}
</style>
</head>
<body>
<!-- =========================
LOBBY
========================= -->
<section id="lobby" class="screen active">
<canvas id="lobbyCanvas"></canvas>
<div class="lobbyUI">
<div class="logo">
FREE-<span>FISH</span>
</div>
<div class="profile">
<div class="profileName" id="lobbyName">Loyk</div>
<div class="profileLevel" id="lobbyLevel">NÍVEL 1</div>
<div class="currency">
<div class="money">💰 <span id="coins">0</span></div>
<div class="money">💎 <span id="gems">0</span></div>
</div>
</div>
<div class="playArea">
<div class="bigFish">
<div class="bigFishTail" id="bigTail"></div>
<div class="bigFishBody" id="bigBody"></div>
<div class="bigFishEye"></div>
</div>
<button class="playBtn" id="playBtn">
JOGAR
</button>
</div>
<div class="sideMenu">
<button class="menuBtn" onclick="openSkins()">
🎨 SKINS
</button>
<button class="menuBtn" onclick="openRanking()">
🏆 RANKING
</button>
<button class="menuBtn" onclick="openName()">
👤 MUDAR NOME
</button>
<button class="menuBtn" onclick="openCodes()">
🎁 CÓDIGOS
</button>
<button class="menuBtn" onclick="openSettings()">
⚙️ CONFIGURAÇÕES
</button>
</div>
<div class="bottomInfo">
<div class="infoCard">
<div class="infoTitle">MAPA</div>
<div class="infoValue">5500 × 5500</div>
</div>
<div class="infoCard">
<div class="infoTitle">PEIXES</div>
<div class="infoValue">50 NPCs</div>
</div>
<div class="infoCard">
<div class="infoTitle">SKINS</div>
<div class="infoValue">40</div>
</div>
</div>
</div>
</section>
<!-- =========================
GAME
========================= -->
<section id="game" class="screen">
<div id="gameWorld">
<div class="worldGrid"></div>
<div id="npcLayer"></div>
<div id="powerLayer"></div>
<div id="playerFish" class="playerFish">
<div class="fishTail" id="playerTail"></div>
<div class="fishBody" id="playerBody"></div>
<div class="fishFin"></div>
<div class="fishEye"></div>
<div class="healthBar">
<div class="healthFill" id="playerHealthFill"></div>
</div>
</div>
</div>
<div id="gameHUD">
<div class="topHUD">
<div class="hudBox">
<small>VIDA</small>
<span id="healthText">100</span>
</div>
<div class="hudBox">
<small>VIVOS</small>
<span id="aliveText">51</span>
</div>
<div class="hudBox">
<small>ELIMINAÇÕES</small>
<span id="killsText">0</span>
</div>
<div class="hudBox">
<small>DINHEIRO</small>
R$ <span id="gameMoney">0</span>
</div>
</div>
<div id="minimap"></div>
<div id="joystick">
<div id="stick"></div>
</div>
<button id="attackBtn">
⚔️<br>ATACAR
</button>
<button id="backBtn">
↩ LOBBY
</button>
<div id="powerInfo">
Sem poder ativo
</div>
</div>
<div id="gameOver">
<div class="gameOverBox">
<h1>🐟 FIM DE JOGO</h1>
<p id="finalPosition">Posição: --</p>
<p id="finalKills">Eliminações: 0</p>
<p id="finalReward">Recompensa: R$0</p>
<button class="actionBtn" onclick="restartGame()">
JOGAR NOVAMENTE
</button>
<button class="actionBtn" onclick="backToLobby()">
VOLTAR AO LOBBY
</button>
</div>
</div>
</section>
<!-- =========================
MODAL
========================= -->
<div id="modal">
<div class="modalBox">
<div class="modalHeader">
<h2 id="modalTitle">FREE-FISH</h2>
<button class="closeBtn" onclick="closeModal()">×</button>
</div>
<div id="modalContent"></div>
</div>
</div>
<script>
"use strict";
/* =========================================================
CONFIGURAÇÃO PRINCIPAL
========================================================= */
const MAP_SIZE = 5500;
/* PLAYER 2× MAIS RÁPIDO */
const PLAYER_SPEED = 11;
const NPC_COUNT = 50;
/*
10 PEIXES DE DISTÂNCIA PARA DETECTAR.
Como cada peixe tem aproximadamente 70 unidades,
10 peixes ≈ 700 unidades.
*/
const FISH_DISTANCE = 70;
const NPC_DETECT_DISTANCE = FISH_DISTANCE * 10;
const NPC_ATTACK_DISTANCE = FISH_DISTANCE * 9;
const NPC_MIN_DISTANCE = 60;
const NPC_BASE_SPEED = 1.35;
const NPC_ATTACK_COOLDOWN = 1000;
const NORMAL_DAMAGE = 30;
const SUPER_DAMAGE = 80;
/* 2 NPCs por minuto */
const AUTO_ELIMINATION_INTERVAL = 60000;
const AUTO_ELIMINATION_MAX = 2;
/* Poderes */
const POWER_COUNT = 10;
const POWER_RESPAWN_TIME = 15000;
/* =========================================================
ELEMENTOS
========================================================= */
const lobby =
document.getElementById("lobby");
const game =
document.getElementById("game");
const gameWorld =
document.getElementById("gameWorld");
const npcLayer =
document.getElementById("npcLayer");
const powerLayer =
document.getElementById("powerLayer");
const playerFish =
document.getElementById("playerFish");
const playerBody =
document.getElementById("playerBody");
const playerTail =
document.getElementById("playerTail");
const minimap =
document.getElementById("minimap");
const modal =
document.getElementById("modal");
const modalTitle =
document.getElementById("modalTitle");
const modalContent =
document.getElementById("modalContent");
/* =========================================================
DADOS SALVOS
========================================================= */
let playerName =
localStorage.getItem("freefish_player_name") || "Loyk";
let coins =
Number(localStorage.getItem("freefish_coins") || 0);
let gems =
Number(localStorage.getItem("freefish_gems") || 0);
let selectedSkin =
Number(localStorage.getItem("freefish_selected_skin") || 0);
let ownedSkins =
JSON.parse(
localStorage.getItem("freefish_owned_skins") ||
"[0]"
);
let totalKills =
Number(localStorage.getItem("freefish_total_kills") || 0);
let level =
Number(localStorage.getItem("freefish_level") || 1);
/* =========================================================
40 SKINS
========================================================= */
const skins = [
["Peixe Azul",2000,"#159cff","#0645b5"],
["Peixe Verde",3000,"#42e65c","#078b39"],
["Peixe Amarelo",3000,"#ffe33b","#e09b00"],
["Peixe Vermelho",4000,"#ff5050","#a80020"],
["Peixe Roxo",4000,"#b55cff","#5420a0"],
["Peixe Laranja",4500,"#ff9b35","#d94c00"],
["Peixe Rosa",4800,"#ff6bc7","#b91b79"],
["Peixe Ciano",5000,"#38ffff","#008fb8"],
["Peixe Branco",5000,"#ffffff","#9ec7dd"],
["Peixe Dourado",5500,"#ffd84a","#b87900"],
["Peixe Oceano",6000,"#27bfff","#0058a0"],
["Peixe Lima",7000,"#a5ff39","#4e9d00"],
["Peixe Coral",7000,"#ff806b","#c53736"],
["Peixe Safira",7000,"#4d83ff","#1837a0"],
["Peixe Esmeralda",7000,"#38e5a0","#08734d"],
["Peixe Neon",8400,"#49ffcc","#00a875"],
["Peixe Elétrico",8700,"#66d9ff","#4c34e8"],
["Peixe Glacial",9000,"#dfffff","#55bde8"],
["Peixe Solar",10000,"#fff35a","#ff7d00"],
["Peixe Lunar",10000,"#d6d9ff","#665fbc"],
["Peixe Estelar",10000,"#a879ff","#302080"],
["Peixe Turquesa",10000,"#40ffe1","#009b8b"],
["Peixe Magenta",10000,"#ff4bd8","#a50091"],
["Peixe Índigo",10000,"#7d6cff","#28158c"],
["Peixe Rubi",10000,"#ff526d","#85001d"],
["Peixe Âmbar",10000,"#ffc04d","#a95600"],
["Peixe Violeta",10000,"#d05cff","#6810a5"],
["Peixe Cristal",10000,"#dffcff","#76c5e8"],
["Peixe Tempestade",10000,"#738aff","#202c87"],
["Peixe Aurora",10000,"#55ffb1","#158a88"],
["Peixe Mestre",12000,"#55eaff","#1245b5"],
["Peixe Fênix",13000,"#ff693f","#9e1900"],
["Peixe Dragão",14000,"#50ff62","#075f2d"],
["Peixe Cósmico",15000,"#cf68ff","#391080"],
["Peixe Abissal",15000,"#4565ff","#07174f"],
["Peixe Fantasma",15000,"#d5eaff","#607ca0"],
["Peixe Relâmpago",16000,"#ffff55","#8c8c00"],
["Peixe Supremo",17000,"#ff65ff","#6e167e"],
["Peixe Lendário",18000,"#ffe96a","#b34a00"],
["PEIXE MASTER",20000,"#ffffff","#00cfff"]
];
/* =========================================================
ESTADO DO JOGO
========================================================= */
const player = {
x:MAP_SIZE/2,
y:MAP_SIZE/2,
direction:0,
health:100,
maxHealth:100,
moving:false,
speedMultiplier:2,
damage:NORMAL_DAMAGE,
sizeMultiplier:1,
powerSpeedUntil:0,
powerSizeUntil:0,
powerDamageUntil:0
};
let npcs = [];
let powers = [];
let camera = {
x:MAP_SIZE/2,
y:MAP_SIZE/2
};
let inputX = 0;
let inputY = 0;
let gameRunning = false;
let gameStartTime = 0;
let lastAutoElimination = 0;
let autoEliminationsThisMinute = 0;
let gameKills = 0;
let gameMoney = 0;
let lastFrame = 0;
/* =========================================================
UTILITÁRIOS
========================================================= */
function saveData(){
localStorage.setItem(
"freefish_player_name",
playerName
);
localStorage.setItem(
"freefish_coins",
String(coins)
);
localStorage.setItem(
"freefish_gems",
String(gems)
);
localStorage.setItem(
"freefish_selected_skin",
String(selectedSkin)
);
localStorage.setItem(
"freefish_owned_skins",
JSON.stringify(ownedSkins)
);
localStorage.setItem(
"freefish_total_kills",
String(totalKills)
);
localStorage.setItem(
"freefish_level",
String(level)
);
}
function clamp(value,min,max){
return Math.max(min,Math.min(max,value));
}
function distance(a,b){
return Math.hypot(
a.x-b.x,
a.y-b.y
);
}
function random(min,max){
return Math.random()*(max-min)+min;
}
function randomInt(min,max){
return Math.floor(
Math.random()*(max-min+1)+min
);
}
function choose(arr){
return arr[
Math.floor(Math.random()*arr.length)
];
}
/* =========================================================
SKIN
========================================================= */
function applyPlayerSkin(){
const skin =
skins[selectedSkin] || skins[0];
playerBody.style.background =
skin[2];
playerBody.style.color =
skin[2];
playerTail.style.borderRightColor =
skin[3];
playerTail.style.color =
skin[3];
playerFish.style.color =
skin[2];
}
function skinHTML(index,mini=false){
const skin = skins[index];
return `
<div class="skinPreview"
style="color:${skin[2]}">
<div class="fishTail"
style="left:0;top:8px;
border-right-color:${skin[3]}"></div>
<div class="fishBody"
style="left:18px;top:6px;
background:${skin[2]}"></div>
</div>
`;
}
/* =========================================================
LOBBY
========================================================= */
function updateLobby(){
document.getElementById("lobbyName")
.textContent = playerName;
document.getElementById("lobbyLevel")
.textContent = "NÍVEL " + level;
document.getElementById("coins")
.textContent = coins.toLocaleString("pt-BR");
document.getElementById("gems")
.textContent = gems.toLocaleString("pt-BR");
applyPlayerSkin();
}
function openSkins(){
modalTitle.textContent =
"🎨 SKINS — 40";
let html =
'<div class="skinGrid">';
skins.forEach((skin,index)=>{
const owned =
ownedSkins.includes(index);
const selected =
selectedSkin === index;
html += `
<div class="skinCard">
${skinHTML(index)}
<b>${index+1}. ${skin[0]}</b>
<div class="skinPrice">
💰 ${skin[1].toLocaleString("pt-BR")}
</div>
<button
onclick="selectSkin(${index})">
${
selected
? "EQUIPADA"
: owned
? "EQUIPAR"
: "COMPRAR"
}
</button>
</div>
`;
});
html += "</div>";
modalContent.innerHTML = html;
modal.style.display = "flex";
}
function selectSkin(index){
const skin = skins[index];
if(ownedSkins.includes(index)){
selectedSkin = index;
saveData();
updateLobby();
openSkins();
return;
}
if(coins < skin[1]){
showNotice(
"Você precisa de R$"+
skin[1].toLocaleString("pt-BR")+
" para comprar essa skin."
);
return;
}
coins -= skin[1];
ownedSkins.push(index);
selectedSkin = index;
saveData();
updateLobby();
openSkins();
}
function openRanking(){
modalTitle.textContent =
"🏆 RANKING";
const ranking = [
{
name:playerName,
kills:totalKills,
money:coins
},
{name:"FishMaster",kills:128,money:28400},
{name:"OceanKing",kills:113,money:24100},
{name:"BlueHunter",kills:97,money:19800},
{name:"AquaPro",kills:84,money:17200},
{name:"FishBoss",kills:71,money:15100},
{name:"NeonFish",kills:62,money:13300},
{name:"LegendFish",kills:55,money:12100},
{name:"OceanPro",kills:48,money:10500},
{name:"MasterFish",kills:41,money:9200}
];
ranking.sort(
(a,b)=>b.kills-a.kills
);
let html="";
ranking.forEach((r,i)=>{
html += `
<div class="rankingRow">
<span>#${i+1} 🐟 ${r.name}</span>
<span>${r.kills} 💀</span>
</div>
`;
});
modalContent.innerHTML = html;
modal.style.display = "flex";
}
function openName(){
modalTitle.textContent =
"👤 MUDAR NOME";
modalContent.innerHTML = `
<p style="margin-bottom:10px">
Nome atual: <b>${playerName}</b>
</p>
<input
id="newName"
class="nameInput"
maxlength="18"
placeholder="Digite seu novo nome"
value="${playerName}"
>
<button
class="modalAction"
onclick="changeName()">
CONFIRMAR
</button>
`;
modal.style.display="flex";
}
function changeName(){
const input =
document.getElementById("newName");
let name =
input.value.trim();
if(!name){
showNotice("Digite um nome.");
return;
}
playerName =
name.substring(0,18);
saveData();
updateLobby();
closeModal();
}
function openCodes(){
modalTitle.textContent =
"🎁 CÓDIGOS";
modalContent.innerHTML = `
<p>
Digite um código promocional:
</p>
<div class="codeBox">
<input
id="codeInput"
maxlength="30"
placeholder="CÓDIGO">
<button
class="modalAction"
onclick="redeemCode()">
RESGATAR
</button>
</div>
<p style="margin-top:15px;color:#8bdcff">
Alguns códigos podem dar moedas ou gemas.
</p>
`;
modal.style.display="flex";
}
function redeemCode(){
const input =
document.getElementById("codeInput");
const code =
input.value.trim().toUpperCase();
const rewards = {
"FREEFISH":
{coins:5000,gems:10},
"FISH":
{coins:1000,gems:2},
"MASTER":
{coins:10000,gems:25}
};
if(!rewards[code]){
showNotice("Código inválido.");
return;
}
const reward = rewards[code];
coins += reward.coins;
gems += reward.gems;
saveData();
updateLobby();
showNotice(
"Código resgatado! +" +
reward.coins +
" moedas e +" +
reward.gems +
" gemas."
);
}
function openSettings(){
modalTitle.textContent =
"⚙️ CONFIGURAÇÕES";
modalContent.innerHTML = `
<div class="settingsRow">
<span>Qualidade</span>
<b>ALTA</b>
</div>
<div class="settingsRow">
<span>Câmera</span>
<b>SUAVE</b>
</div>
<div class="settingsRow">
<span>Mapa</span>
<b>5500 × 5500</b>
</div>
<div class="settingsRow">
<span>NPCs</span>
<b>50</b>
</div>
`;
modal.style.display="flex";
}
function closeModal(){
modal.style.display="none";
}
function showNotice(text){
modalTitle.textContent =
"FREE-FISH";
modalContent.innerHTML = `
<p style="font-size:17px;line-height:1.5">
${text}
</p>
<button
class="modalAction"
onclick="closeModal()">
OK
</button>
`;
modal.style.display="flex";
}
/* =========================================================
LOBBY CANVAS
========================================================= */
const lobbyCanvas =
document.getElementById("lobbyCanvas");
const lobbyCtx =
lobbyCanvas.getContext("2d");
let lobbyBubbles=[];
function resizeLobby(){
lobbyCanvas.width =
window.innerWidth;
lobbyCanvas.height =
window.innerHeight;
}
function createLobbyBubbles(){
lobbyBubbles=[];
for(let i=0;i<80;i++){
lobbyBubbles.push({
x:random(0,window.innerWidth),
y:random(0,window.innerHeight),
r:random(1,5),
speed:random(.2,1)
});
}
}
function lobbyAnimation(){
lobbyCtx.clearRect(
0,
0,
lobbyCanvas.width,
lobbyCanvas.height
);
lobbyCtx.fillStyle =
"rgba(0,110,180,.12)";
lobbyCtx.fillRect(
0,
0,
lobbyCanvas.width,
lobbyCanvas.height
);
for(const b of lobbyBubbles){
b.y -= b.speed;
if(b.y < -10){
b.y =
lobbyCanvas.height+10;
b.x =
random(0,lobbyCanvas.width);
}
lobbyCtx.beginPath();
lobbyCtx.arc(
b.x,
b.y,
b.r,
0,
Math.PI*2
);
lobbyCtx.fillStyle =
"rgba(100,220,255,.35)";
lobbyCtx.fill();
}
requestAnimationFrame(lobbyAnimation);
}
/* =========================================================
CRIAR NPC
========================================================= */
function createNPC(index){
let x;
let y;
do{
x =
random(100,MAP_SIZE-100);
y =
random(100,MAP_SIZE-100);
}while(
Math.hypot(
x-player.x,
y-player.y
) < 450
);
const skinIndex =
randomInt(0,skins.length-1);
const skin =
skins[skinIndex];
/*
Cada NPC escolhe aleatoriamente
1×, 2× ou 3×.
*/
const speedMultiplier =
choose([1,2,3]);
const el =
document.createElement("div");
el.className="npc";
el.dataset.id=index;
el.innerHTML = `
<div class="nameTag">
${skin[0]}
</div>
<div class="healthBar">
<div class="healthFill"></div>
</div>
<div
class="fishTail"
style="
border-right-color:${skin[3]};
color:${skin[3]}
">
</div>
<div
class="fishBody"
style="
background:${skin[2]};
color:${skin[2]}
">
</div>
<div
class="fishFin"
style="
color:${skin[2]}
">
</div>
<div class="fishEye"></div>
`;
npcLayer.appendChild(el);
const npc = {
id:index,
x,
y,
health:100,
maxHealth:100,
direction:random(0,Math.PI*2),
speedMultiplier,
baseSpeed:
NPC_BASE_SPEED *
speedMultiplier,
target:null,
lastAttack:0,
dead:false,
el,
body:
el.querySelector(".fishBody"),
tail:
el.querySelector(".fishTail"),
healthFill:
el.querySelector(".healthFill"),
sizeMultiplier:1,
damage:NORMAL_DAMAGE,
speedPowerUntil:0,
sizePowerUntil:0,
damagePowerUntil:0
};
el.addEventListener(
"pointerdown",
e=>{
e.stopPropagation();
attackNPC(npc);
}
);
npcs.push(npc);
}
/* =========================================================
CRIAR 50 NPCs
========================================================= */
function createNPCs(){
npcs=[];
npcLayer.innerHTML="";
for(
let i=0;
i<NPC_COUNT;
i++
){
createNPC(i);
}
}
/* =========================================================
PODERES
========================================================= */
const powerTypes = [
{
type:"speed",
icon:"⚡",
name:"VELOCIDADE ×5"
},
{
type:"life",
icon:"❤️",
name:"VIDA +400"
},
{
type:"size",
icon:"🔮",
name:"TAMANHO ×3"
},
{
type:"damage",
icon:"💥",
name:"SUPER DANO 80"
}
];
function spawnPowers(){
powerLayer.innerHTML="";
powers=[];
for(
let i=0;
i<POWER_COUNT;
i++
){
const type =
choose(powerTypes);
const el =
document.createElement("div");
el.className =
"power "+type.type;
el.textContent =
type.icon;
el.title =
type.name;
const power = {
id:i,
type:type.type,
x:random(100,MAP_SIZE-100),
y:random(100,MAP_SIZE-100),
el
};
el.style.left =
power.x+"px";
el.style.top =
power.y+"px";
powerLayer.appendChild(el);
powers.push(power);
}
}
function checkPowerPickups(){
for(
let i=powers.length-1;
i>=0;
i--
){
const power =
powers[i];
if(
distance(
player,
power
) < 70
){
collectPower(
player,
power,
true
);
power.el.remove();
powers.splice(i,1);
continue;
}
for(const npc of npcs){
if(npc.dead) continue;
if(
distance(npc,power)<55
){
collectPower(
npc,
power,
false
);
power.el.remove();
powers.splice(i,1);
break;
}
}
}
}
function collectPower(entity,power,isPlayer){
const now =
performance.now();
if(power.type==="speed"){
if(isPlayer){
player.powerSpeedUntil =
now+12000;
}else{
entity.speedPowerUntil =
now+12000;
}
}
if(power.type==="life"){
entity.maxHealth += 400;
entity.health += 400;
if(isPlayer){
player.health =
Math.min(
player.health,
player.maxHealth
);
}
}
if(power.type==="size"){
if(isPlayer){
player.sizeMultiplier=3;
player.powerSizeUntil =
now+12000;
}else{
entity.sizeMultiplier=3;
entity.powerSizeUntil =
now+12000;
}
}
if(power.type==="damage"){
if(isPlayer){
player.damage =
SUPER_DAMAGE;
player.powerDamageUntil =
now+12000;
}else{
entity.damage =
SUPER_DAMAGE;
entity.powerDamageUntil =
now+12000;
}
}
if(isPlayer){
updatePowerInfo();
}
}
/* =========================================================
ATUALIZAR PODERES
========================================================= */
function updatePowerEffects(now){
if(
player.powerSpeedUntil &&
now>player.powerSpeedUntil
){
player.powerSpeedUntil=0;
}
if(
player.powerSizeUntil &&
now>player.powerSizeUntil
){
player.powerSizeUntil=0;
player.sizeMultiplier=1;
}
if(
player.powerDamageUntil &&
now>player.powerDamageUntil
){
player.powerDamageUntil=0;
player.damage=NORMAL_DAMAGE;
}
for(const npc of npcs){
if(npc.dead) continue;
if(
npc.speedPowerUntil &&
now>npc.speedPowerUntil
){
npc.speedPowerUntil=0;
}
if(
npc.powerSizeUntil &&
now>npc.powerSizeUntil
){
npc.powerSizeUntil=0;
npc.sizeMultiplier=1;
}
if(
npc.powerDamageUntil &&
now>npc.powerDamageUntil
){
npc.powerDamageUntil=0;
npc.damage=NORMAL_DAMAGE;
}
}
}
function updatePowerInfo(){
const info =
document.getElementById("powerInfo");
const now =
performance.now();
let powersText=[];
if(
player.powerSpeedUntil>now
)
powersText.push("⚡ ×5");
if(
player.maxHealth>100
)
powersText.push("❤️ +400");
if(
player.powerSizeUntil>now
)
powersText.push("🔮 ×3");
if(
player.powerDamageUntil>now
)
powersText.push("💥 80");
info.textContent =
powersText.length
? powersText.join(" ")
: "Sem poder ativo";
}
/* =========================================================
IA DOS NPCs
========================================================= */
function findNPCNearestTarget(npc){
let closest=null;
let closestDistance=Infinity;
for(const other of npcs){
if(other===npc) continue;
if(other.dead) continue;
const d =
distance(npc,other);
/*
NPC detecta até 10 peixes.
*/
if(
d<=NPC_DETECT_DISTANCE &&
d<closestDistance
){
closest=other;
closestDistance=d;
}
}
/*
NPC também pode caçar o player
se estiver dentro da mesma distância.
*/
const playerDistance =
distance(npc,player);
if(
player.health>0 &&
playerDistance<=NPC_DETECT_DISTANCE &&
playerDistance<closestDistance
){
return player;
}
return closest;
}
function updateNPCs(now){
for(const npc of npcs){
if(npc.dead) continue;
/*
Escolhe alvo aleatoriamente quando não possui alvo
ou quando o alvo ficou distante demais/morreu.
*/
if(
!npc.target ||
npc.target.dead ||
(
npc.target!==player &&
distance(npc,npc.target)>
NPC_DETECT_DISTANCE
)
){
npc.target =
findNPCNearestTarget(npc);
}
const target =
npc.target;
let vx=0;
let vy=0;
if(target){
vx =
target.x-npc.x;
vy =
target.y-npc.y;
const d =
Math.hypot(vx,vy);
if(d>0){
npc.direction =
Math.atan2(vy,vx);
/*
9 peixes de distância = ataque.
*/
if(
d>NPC_ATTACK_DISTANCE
){
const speed =
npc.baseSpeed *
(
npc.speedPowerUntil>now
? 5
: 1
);
npc.x +=
vx/d*speed;
npc.y +=
vy/d*speed;
}else{
if(
now-npc.lastAttack >=
NPC_ATTACK_COOLDOWN
){
npc.lastAttack=now;
npcAttack(
npc,
target
);
}
}
}
}else{
/*
Sem alvo: nada de ficar parado.
Nada lentamente pelo oceano.
*/
npc.x +=
Math.cos(npc.direction) *
npc.baseSpeed*.35;
npc.y +=
Math.sin(npc.direction) *
npc.baseSpeed*.35;
if(Math.random()<.015){
npc.direction +=
random(-.8,.8);
}
}
/*
Limites do mapa.
*/
npc.x =
clamp(
npc.x,
50,
MAP_SIZE-50
);
npc.y =
clamp(
npc.y,
50,
MAP_SIZE-50
);
updateNPCElement(npc);
}
}
function npcAttack(npc,target){
if(target===player){
damagePlayer(
npc.damage
);
return;
}
if(target && !target.dead){
damageNPC(
target,
npc.damage,
npc
);
}
}
/* =========================================================
ATAQUE DO PLAYER
========================================================= */
function attackNearest(){
let target=null;
let best=Infinity;
for(const npc of npcs){
if(npc.dead) continue;
const d =
distance(player,npc);
if(
d<=180 &&
d<best
){
best=d;
target=npc;
}
}
if(target){
attackNPC(target);
}
}
function attackNPC(npc){
if(
!npc ||
npc.dead
) return;
damageNPC(
npc,
player.damage,
player
);
}
/* =========================================================
DANO
========================================================= */
function damageNPC(npc,damage,attacker){
if(npc.dead) return;
npc.health -= damage;
if(npc.health<=0){
killNPC(
npc,
attacker
);
}else{
updateNPCHealth(npc);
}
}
function killNPC(npc,attacker){
if(npc.dead) return;
npc.dead=true;
npc.health=0;
npc.target=null;
npc.el.classList.add("dead");
updateNPCHealth(npc);
/*
Se foi o player:
+R$100 fictícios
*/
if(attacker===player){
gameKills++;
totalKills++;
gameMoney += 100;
coins += 100;
level =
Math.max(
1,
1+Math.floor(totalKills/10)
);
saveData();
}
updateHUD();
}
function damagePlayer(damage){
if(!gameRunning) return;
player.health -= damage;
if(player.health<=0){
player.health=0;
playerDead();
}
updateHUD();
}
/* =========================================================
MORTE AUTOMÁTICA
========================================================= */
function automaticEliminations(now){
if(
now-lastAutoElimination<
AUTO_ELIMINATION_INTERVAL
) return;
lastAutoElimination=now;
autoEliminationsThisMinute=0;
/*
Pode eliminar até 2 NPCs.
Nunca o player.
*/
for(
let i=0;
i<AUTO_ELIMINATION_MAX;
i++
){
const alive =
npcs.filter(
n=>!n.dead
);
if(alive.length<=1)
break;
const npc =
choose(alive);
/*
Conta como eliminação do ambiente,
parecendo que outro peixe matou.
*/
npc.dead=true;
npc.health=0;
npc.target=null;
npc.el.classList.add("dead");
updateNPCHealth(npc);
autoEliminationsThisMinute++;
}
updateHUD();
}
/* =========================================================
POSIÇÃO / ELEMENTOS
========================================================= */
function updateNPCHealth(npc){
const percent =
clamp(
npc.health/npc.maxHealth*100,
0,
100
);
npc.healthFill.style.width =
percent+"%";
}
function updateNPCElement(npc){
const scale =
npc.sizeMultiplier;
npc.el.style.left =
npc.x+"px";
npc.el.style.top =
npc.y+"px";
npc.el.style.transform =
`translate(-50%,-50%)
rotate(${npc.direction}rad)
scale(${scale})`;
}
function updatePlayerElement(){
playerFish.style.left =
player.x+"px";
playerFish.style.top =
player.y+"px";
playerFish.style.transform =
`translate(-50%,-50%)
rotate(${player.direction}rad)
scale(${player.sizeMultiplier})`;
}
/* =========================================================
CÂMERA
========================================================= */
function updateCamera(){
const targetX =
player.x;
const targetY =
player.y;
camera.x +=
(targetX-camera.x)*.12;
camera.y +=
(targetY-camera.y)*.12;
const screenW =
window.innerWidth;
const screenH =
window.innerHeight;
let offsetX =
screenW/2-camera.x;
let offsetY =
screenH/2-camera.y;
/*
Não deixa câmera sair do mapa.
*/
offsetX =
clamp(
offsetX,
screenW-MAP_SIZE,
0
);
offsetY =
clamp(
offsetY,
screenH-MAP_SIZE,
0
);
gameWorld.style.transform =
`translate(${offsetX}px,${offsetY}px)`;
}
/* =========================================================
MINIMAPA
========================================================= */
function updateMinimap(){
minimap.innerHTML="";
/*
NPCs
*/
for(const npc of npcs){
if(npc.dead) continue;
const dot =
document.createElement("div");
dot.className="mapDot";
dot.style.left =
(npc.x/MAP_SIZE*100)+"%";
dot.style.top =
(npc.y/MAP_SIZE*100)+"%";
minimap.appendChild(dot);
}
/*
Player
*/
const p =
document.createElement("div");
p.className =
"mapDot mapPlayer";
p.style.left =
(player.x/MAP_SIZE*100)+"%";
p.style.top =
(player.y/MAP_SIZE*100)+"%";
minimap.appendChild(p);
}
/* =========================================================
HUD
========================================================= */
function getAliveCount(){
return (
npcs.filter(
n=>!n.dead
).length+1
);
}
function getPlayerPosition(){
return getAliveCount();
}
function updateHUD(){
document.getElementById(
"healthText"
).textContent =
Math.ceil(player.health);
document.getElementById(
"aliveText"
).textContent =
getAliveCount();
document.getElementById(
"killsText"
).textContent =
gameKills;
document.getElementById(
"gameMoney"
).textContent =
gameMoney.toLocaleString("pt-BR");
document.getElementById(
"playerHealthFill"
).style.width =
clamp(
player.health/
player.maxHealth*100,
0,
100
)+"%";
}
/* =========================================================
RECOMPENSAS
========================================================= */
function getPlacementReward(position){
/*
Regras solicitadas:
Top 40-50 = +10
Top 30-39 = +50
Top 20-29 = +150
Top 10-19 = +300
Top 5-9 = +500
4 = +600
3 = +700
2 = +800
1 = +1000
*/
if(position===1) return 1000;
if(position===2) return 800;
if(position===3) return 700;
if(position===4) return 600;
if(position>=5 && position<=9)
return 500;
if(position>=10 && position<=19)
return 300;
if(position>=20 && position<=29)
return 150;
if(position>=30 && position<=39)
return 50;
if(position>=40 && position<=50)
return 10;
return 0;
}
function finishGame(){
const position =
getPlayerPosition();
const reward =
getPlacementReward(position);
coins += reward;
gameMoney += reward;
saveData();
document.getElementById(
"finalPosition"
).textContent =
"🏆 Posição: #"+position;
document.getElementById(
"finalKills"
).textContent =
"💀 Eliminações: "+gameKills;
document.getElementById(
"finalReward"
).textContent =
"💰 Recompensa: R$"+
reward.toLocaleString("pt-BR");
document.getElementById(
"gameOver"
).style.display="flex";
}
/* =========================================================
MORTE PLAYER
========================================================= */
function playerDead(){
if(!gameRunning) return;
gameRunning=false;
finishGame();
}
/* =========================================================
CONTROLES
========================================================= */
const joystick =
document.getElementById("joystick");
const stick =
document.getElementById("stick");
let joystickActive=false;
const JOYSTICK_MAX=40;
function setJoystick(clientX,clientY){
const rect =
joystick.getBoundingClientRect();
const centerX =
rect.left+rect.width/2;
const centerY =
rect.top+rect.height/2;
let dx =
clientX-centerX;
let dy =
clientY-centerY;
const len =
Math.hypot(dx,dy);
if(len>JOYSTICK_MAX){
dx =
dx/len*JOYSTICK_MAX;
dy =
dy/len*JOYSTICK_MAX;
}
inputX =
dx/JOYSTICK_MAX;
inputY =
dy/JOYSTICK_MAX;
stick.style.transform =
`translate(${dx}px,${dy}px)`;
player.moving =
Math.hypot(
inputX,
inputY
)>.08;
}
function resetJoystick(){
inputX=0;
inputY=0;
player.moving=false;
stick.style.transform=
"translate(0,0)";
}
joystick.addEventListener(
"pointerdown",
e=>{
joystickActive=true;
joystick.setPointerCapture(
e.pointerId
);
setJoystick(
e.clientX,
e.clientY
);
}
);
joystick.addEventListener(
"pointermove",
e=>{
if(!joystickActive) return;
setJoystick(
e.clientX,
e.clientY
);
}
);
joystick.addEventListener(
"pointerup",
e=>{
joystickActive=false;
resetJoystick();
}
);
joystick.addEventListener(
"pointercancel",
resetJoystick
);
document.getElementById(
"attackBtn"
).addEventListener(
"pointerdown",
e=>{
e.preventDefault();
attackNearest();
}
);
/* =========================================================
MOVIMENTO PLAYER
========================================================= */
function updatePlayer(delta){
if(!gameRunning) return;
if(player.moving){
const length =
Math.hypot(
inputX,
inputY
);
if(length>0){
const dx =
inputX/Math.max(1,length);
const dy =
inputY/Math.max(1,length);
const now =
performance.now();
const powerMultiplier =
player.powerSpeedUntil>now
? 5
: 1;
const speed =
PLAYER_SPEED *
powerMultiplier *
Math.min(delta/16.67,2);
player.x +=
dx*speed;
player.y +=
dy*speed;
player.direction =
Math.atan2(dy,dx);
}
}
player.x =
clamp(
player.x,
50,
MAP_SIZE-50
);
player.y =
clamp(
player.y,
50,
MAP_SIZE-50
);
}
/* =========================================================
INÍCIO DO JOGO
========================================================= */
function startGame(){
lobby.classList.remove("active");
game.classList.add("active");
resetGameState();
createNPCs();
spawnPowers();
gameRunning=true;
gameStartTime =
performance.now();
lastAutoElimination =
gameStartTime;
lastFrame =
performance.now();
updateLobby();
updateHUD();
updateMinimap();
}
function resetGameState(){
player.x =
MAP_SIZE/2;
player.y =
MAP_SIZE/2;
player.direction=0;
player.health=100;
player.maxHealth=100;
player.damage=NORMAL_DAMAGE;
player.sizeMultiplier=1;
player.powerSpeedUntil=0;
player.powerSizeUntil=0;
player.powerDamageUntil=0;
gameKills=0;
gameMoney=0;
inputX=0;
inputY=0;
resetJoystick();
document.getElementById(
"gameOver"
).style.display="none";
}
/* =========================================================
REINICIAR
========================================================= */
function restartGame(){
document.getElementById(
"gameOver"
).style.display="none";
startGame();
}
/* =========================================================
VOLTAR AO LOBBY
========================================================= */
function backToLobby(){
gameRunning=false;
game.classList.remove("active");
lobby.classList.add("active");
npcLayer.innerHTML="";
powerLayer.innerHTML="";
npcs=[];
powers=[];
updateLobby();
}
/* =========================================================
LOOP PRINCIPAL
========================================================= */
function gameLoop(timestamp){
const delta =
timestamp-lastFrame;
lastFrame=timestamp;
if(gameRunning){
updatePlayer(delta);
updateNPCs(timestamp);
updatePowerEffects(timestamp);
checkPowerPickups();
automaticEliminations(timestamp);
updatePlayerElement();
updateCamera();
updateMinimap();
updateHUD();
updatePowerInfo();
}
requestAnimationFrame(gameLoop);
}
/* =========================================================
CLIQUE NO MAPA
========================================================= */
gameWorld.addEventListener(
"pointerdown",
e=>{
if(!gameRunning) return;
/*
Ataque rápido ao tocar perto de um NPC.
*/
const rect =
gameWorld.getBoundingClientRect();
/*
Não usamos o ponto da tela diretamente
porque a câmera transforma o mundo.
*/
let nearest=null;
let best=220;
for(const npc of npcs){
if(npc.dead) continue;
const d =
distance(player,npc);
if(d<best){
best=d;
nearest=npc;
}
}
if(nearest){
attackNPC(nearest);
}
}
);
/* =========================================================
INICIALIZAÇÃO
========================================================= */
document.getElementById(
"playBtn"
).addEventListener(
"click",
startGame
);
document.getElementById(
"backBtn"
).addEventListener(
"click",
backToLobby
);
window.addEventListener(
"resize",
()=>{
resizeLobby();
createLobbyBubbles();
}
);
resizeLobby();
createLobbyBubbles();
applyPlayerSkin();
updateLobby();
lobbyAnimation();
requestAnimationFrame(gameLoop);
/* =========================================================
ATALHOS DE TECLADO
========================================================= */
window.addEventListener(
"keydown",
e=>{
if(e.key==="w" || e.key==="ArrowUp")
inputY=-1;
if(e.key==="s" || e.key==="ArrowDown")
inputY=1;
if(e.key==="a" || e.key==="ArrowLeft")
inputX=-1;
if(e.key==="d" || e.key==="ArrowRight")
inputX=1;
if(
["w","a","s","d",
"ArrowUp","ArrowDown",
"ArrowLeft","ArrowRight"]
.includes(e.key)
){
player.moving=true;
}
if(e.code==="Space"){
attackNearest();
}
}
);
window.addEventListener(
"keyup",
e=>{
if(
["w","s","ArrowUp","ArrowDown"]
.includes(e.key)
){
inputY=0;
}
if(
["a","d","ArrowLeft","ArrowRight"]
.includes(e.key)
){
inputX=0;
}
if(
Math.abs(inputX)<.01 &&
Math.abs(inputY)<.01
){
player.moving=false;
}
}
);
</script>
</body>
</html>Game Source: FREE-FISH
Creator: MegaFlare51
Libraries: none
Complexity: complex (3506 lines, 51.1 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: free-fish-megaflare51" to link back to the original. Then publish at arcadelab.ai/publish.