🎮ArcadeLab

FREE-FISH

by MegaFlare51
3506 lines51.1 KB
▶ Play
<!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.