🎮ArcadeLab

Fortnite Season 1

by StormPanda35
499 lines23.0 KB
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fortnite Season 1</title>
<style>
*{box-sizing:border-box}
body{margin:0;font-family:Arial,sans-serif;color:white;background:linear-gradient(135deg,#123b76,#1687c5,#101d48);min-height:100vh}
button,input{font:inherit}button{cursor:pointer;font-weight:900;border:0;transition:.15s}button:hover{filter:brightness(1.15)}
.hidden{display:none!important}
#authScreen{min-height:100vh;display:flex;justify-content:center;align-items:center;padding:20px;background:linear-gradient(135deg,rgba(4,16,46,.7),rgba(7,77,132,.45))}
.authbox{width:100%;max-width:430px;padding:32px;background:#0a1d3aee;border:2px solid #548ac0;box-shadow:0 20px 60px #06132b80}
.logo{font-size:32px;font-weight:1000;font-style:italic;text-align:center;margin-bottom:6px}
.logo span{color:#ffda48}
.auth-sub{text-align:center;color:#bcd3ef;font-size:13px;margin-bottom:25px}
.auth-tabs{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:22px}
.auth-tabs button{padding:13px;background:#25476e;color:white}
.auth-tabs button.active{background:#ffcf3d;color:#14213c}
label{display:block;font-size:12px;font-weight:bold;margin:14px 0 7px}
input{width:100%;padding:13px;background:#07152c;border:1px solid #54749a;color:white;outline:none}
input:focus{border-color:#ffdb4d}
.primary{width:100%;padding:15px;background:#ffcf3d;color:#16233d;margin-top:20px}
#authMessage{min-height:35px;color:#ffd85b;font-size:13px;line-height:1.5;margin-top:14px}
.fineprint{font-size:11px;line-height:1.5;color:#9bb5d5;margin-top:14px}
header{height:70px;background:#07152df2;display:flex;align-items:center;padding:0 20px;gap:16px;position:sticky;top:0;z-index:5;border-bottom:2px solid #456b95}
.smalllogo{font-size:21px;font-weight:1000;font-style:italic}.smalllogo span{color:#ffda48}
nav{display:flex;gap:4px;flex-wrap:wrap}
nav button{background:transparent;color:#c2d3e9;padding:13px 12px}
nav button.active{color:white;border-bottom:3px solid #ffda48;background:#ffffff12}
.userbar{margin-left:auto;font-size:12px;font-weight:bold;display:flex;align-items:center;gap:10px}
.avatar{background:#f8ce45;color:#17223c;border-radius:50%;width:34px;height:34px;display:grid;place-items:center}
.logout{background:#294464;color:white;padding:9px}
.screen{display:none;min-height:calc(100vh - 70px)}.screen.active{display:block}
.lobby{display:flex;align-items:center;gap:20px;padding:35px 5%;min-height:calc(100vh - 70px);position:relative;overflow:hidden}
.left{width:330px;max-width:100%;z-index:2}
.season{font-size:12px;color:#ffe16a;letter-spacing:2px;font-weight:1000}
h1{font-size:36px;font-style:italic;margin:12px 0;font-weight:1000}
.sub{color:#c8ddf5;font-size:13px;line-height:1.5}
.card{background:#081a3cce;border:1px solid #7694b4;padding:16px;margin-top:22px}
.modes{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px}
.mode{background:#29476f;color:white;padding:14px 4px;border:2px solid transparent}
.mode.selected{border-color:#ffdb49;background:#3c6393}
#play{width:100%;padding:17px;background:#ffcf3d;color:#17233b;font-size:20px;margin-top:15px}
#unready{width:100%;padding:12px;background:#29476f;color:white;margin-top:10px}
#status{font-size:11px;margin-top:14px;color:#d4e4f7;font-weight:bold}
.character-area{flex:1;min-width:210px;min-height:480px;display:flex;flex-direction:column;justify-content:center;align-items:center;position:relative}
.charname{font-size:20px;font-weight:1000;z-index:1;text-shadow:0 2px 5px #000;margin-bottom:15px}
.charname small{display:block;text-align:center;color:#ffe16a;font-size:10px;letter-spacing:2px;margin-top:5px}
.figure{width:190px;height:365px;position:relative;z-index:1;animation:float 3s ease-in-out infinite;filter:drop-shadow(0 10px 8px #06132c70)}
@keyframes float{50%{transform:translateY(-8px)}}
.head{position:absolute;width:94px;height:100px;left:48px;top:15px;background:#e5b98e;border:3px solid #17213a;border-radius:28px 28px 23px 23px;z-index:4}
.hair{position:absolute;left:46px;top:8px;width:98px;height:31px;border-radius:35px 35px 10px 10px;background:#30291f;border:3px solid #17213a;z-index:5}
.eye{position:absolute;width:9px;height:12px;top:45px;left:20px;background:#17213a;border-radius:50%}
.eye.right{left:59px}
.mouth{position:absolute;width:31px;height:12px;left:28px;top:70px;border-bottom:4px solid #803d32;border-radius:50%}
.body{position:absolute;top:108px;left:33px;width:124px;height:148px;background:#2779c7;border:4px solid #17213a;border-radius:22px 22px 12px 12px;z-index:3}
.belt{position:absolute;bottom:15px;left:-3px;height:19px;width:124px;background:#23344e;border-top:3px solid #17213a;border-bottom:3px solid #17213a}
.buckle{position:absolute;width:19px;height:19px;left:48px;bottom:13px;background:#e4bc45;border:3px solid #17213a;z-index:2}
.arm{position:absolute;top:120px;width:37px;height:124px;background:#e5b98e;border:4px solid #17213a;border-radius:17px;z-index:2}
.arm.left{left:7px;transform:rotate(8deg)}.arm.right{right:7px;transform:rotate(-8deg)}
.sleeve{position:absolute;top:119px;width:42px;height:47px;background:#2779c7;border:4px solid #17213a;z-index:4;border-radius:12px}
.sleeve.left{left:3px;transform:rotate(8deg)}.sleeve.right{right:3px;transform:rotate(-8deg)}
.legs{position:absolute;top:248px;left:38px;width:114px;height:122px;display:flex;gap:5px;z-index:2}
.leg{position:relative;flex:1;background:#283e66;border:4px solid #17213a;border-radius:8px 8px 13px 13px}
.leg:after{content:"";position:absolute;height:22px;bottom:-7px;left:-5px;right:-5px;background:#161e2b;border:3px solid #101726;border-radius:8px}
.platform{width:270px;height:48px;background:linear-gradient(#9ac9ef,#3569a3);border:3px solid #ffffff80;transform:perspective(300px) rotateX(50deg);border-radius:50%;box-shadow:0 0 40px #7dc6ff55;margin-top:-8px}
.clowner .hair{background:#e83a45;height:35px}
.clowner .head{background:#f5f1e8}
.clowner .body{background:#e63d4e}
.clowner .sleeve{background:#f4cf43}
.clowner .arm{background:#f5f1e8}
.clowner .leg{background:#463c88}
.clowner .eye{width:12px;height:14px}
.clowner .mouth{width:48px;height:19px;left:20px;top:65px;background:#bb263e;border:3px solid #7f1830;border-radius:8px 8px 50% 50%}
.clown-nose{display:none;position:absolute;top:53px;left:34px;width:23px;height:23px;background:#f52b39;border:3px solid #a71426;border-radius:50%;z-index:8}
.clowner .clown-nose{display:block}
.clown-hat{display:none;position:absolute;left:59px;top:-9px;width:73px;height:26px;background:#f4cf43;border:3px solid #17213a;border-radius:50% 50% 5px 5px;z-index:6;transform:rotate(-8deg)}
.clowner .clown-hat{display:block}
.clown-cheek{display:none;position:absolute;top:62px;width:15px;height:10px;background:#ed4354;border-radius:50%}
.clown-cheek.left{left:7px}.clown-cheek.right{right:7px}
.clowner .clown-cheek{display:block}
.page{padding:35px 5%}
.page-title{font-size:34px;font-weight:1000;font-style:italic}
.locker-layout{display:grid;grid-template-columns:1.2fr .8fr;gap:20px;max-width:1050px}
.skin-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.skin-card{background:#081a3cce;color:white;border:2px solid #ffffff30;padding:15px;min-width:0;text-align:left;overflow:hidden}
.skin-card.selected{border-color:#ffdb49}
.skin-card.equipped{box-shadow:inset 0 -4px #ffdb49}
.skin-card .figure{transform:scale(.55);transform-origin:top center;margin:0 auto -165px;animation:none}
.skin-name{display:block;font-size:17px;font-weight:1000;margin-top:8px}
.rarity{font-size:10px;color:#b5d0f0;display:block;margin-top:5px}
.preview{min-height:430px;background:#081a3cce;border:1px solid #ffffff30;display:flex;align-items:center;flex-direction:column;justify-content:center;overflow:hidden}
.preview .figure{transform:scale(.75);transform-origin:center;margin-bottom:-65px}
.preview-name{font-size:25px;font-weight:1000;margin-top:12px}
.preview-rarity{font-size:11px;color:#ffdf5c;margin-top:5px}
#equip{width:80%;padding:15px;background:#ffcf3d;color:#17233b;margin-top:17px}
#equip:disabled{background:#526a85;color:white}
#lockerNotice{font-size:12px;color:#bfe5ff;margin:12px;text-align:center}
.notice{font-size:12px;color:#c5d9f3;margin-top:12px}
@media(max-width:750px){header{height:auto;min-height:70px;flex-wrap:wrap;padding:10px}.userbar{margin-left:auto}.lobby{flex-direction:column;align-items:stretch}.character-area{min-height:440px;width:100%}.locker-layout{grid-template-columns:1fr}.page{padding:25px 4%}h1{font-size:30px}}
</style>
</head>
<body>

<!-- ACCOUNT SCREEN -->
<section id="authScreen">
  <div class="authbox">
    <div class="logo">FORTNITE<span>★</span></div>
    <div class="auth-sub">CHAPTER 1 · SEASON 1</div>
    <div class="auth-tabs">
      <button id="loginTab" class="active">LOG IN</button>
      <button id="signinTab">SIGN IN</button>
    </div>

    <form id="authForm">
      <label for="username">USERNAME</label>
      <input id="username" maxlength="20" minlength="3"
        autocomplete="username" placeholder="Choose a username" required>

      <label for="password">PASSWORD</label>
      <input id="password" type="password" minlength="6"
        autocomplete="current-password" placeholder="Enter your password" required>

      <button id="authSubmit" class="primary" type="submit">CREATE ACCOUNT</button>
    </form>

    <div id="authMessage" role="status"></div>
    <div class="fineprint">
      Use a made-up password for this demo, not a password you use for other accounts.
      Accounts are saved only in this browser. This is a prototype, not a secure online account system.
    </div>
  </div>
</section>

<!-- GAME -->
<div id="game" class="hidden">
<header>
  <div class="smalllogo">FORTNITE<span>★</span></div>
  <nav>
    <button class="active" data-tab="lobby">LOBBY</button>
    <button data-tab="locker">LOCKER</button>
    <button data-tab="shop">ITEM SHOP</button>
    <button data-tab="career">CAREER</button>
  </nav>
  <div class="userbar">
    <div class="avatar">1</div>
    <span id="headerUser">PLAYER</span>
    <button id="logout" class="logout">LOG OUT</button>
  </div>
</header>

<main id="lobby" class="screen active">
  <div class="lobby">
    <section class="left">
      <div class="season">CHAPTER 1 · SEASON 1</div>
      <h1>BATTLE ROYALE</h1>
      <div class="sub">The original island. The original vibe.<br>Drop in and be the last player standing.</div>
      <div class="card">
        <b>SELECT GAME MODE</b>
        <div class="modes">
          <button class="mode selected" data-mode="SOLO">👤 SOLO</button>
          <button class="mode" data-mode="DUOS">👥 DUOS</button>
        </div>
        <p id="modeDescription" class="sub">Every player for themselves.</p>
        <button id="play">PLAY</button>
        <button id="unready" class="hidden">UNREADY</button>
        <div id="status">● ONLINE · IN LOBBY</div>
      </div>
    </section>

    <section class="character-area">
      <div class="charname"><span id="lobbySkinName">Blue</span><small>OUTFIT · EQUIPPED</small></div>
      <div id="lobbyFigure" class="figure">
        <div class="clown-hat"></div><div class="hair"></div>
        <div class="head"><div class="eye"></div><div class="eye right"></div><div class="clown-cheek left"></div><div class="clown-cheek right"></div><div class="clown-nose"></div><div class="mouth"></div></div>
        <div class="arm left"></div><div class="arm right"></div><div class="sleeve left"></div><div class="sleeve right"></div>
        <div class="body"><div class="belt"></div><div class="buckle"></div></div>
        <div class="legs"><div class="leg"></div><div class="leg"></div></div>
      </div>
      <div class="platform"></div>
    </section>
  </div>
</main>

<main id="locker" class="screen">
  <section class="page">
    <h1 class="page-title">MY LOCKER</h1>
    <p class="sub">Choose your outfit. Your account keeps its equipped skin.</p>
    <div class="locker-layout">
      <div class="skin-grid">
        <button class="skin-card" data-skin="Blue">
          <div class="figure">
            <div class="hair"></div><div class="head"><div class="eye"></div><div class="eye right"></div><div class="mouth"></div></div>
            <div class="arm left"></div><div class="arm right"></div><div class="sleeve left"></div><div class="sleeve right"></div>
            <div class="body"><div class="belt"></div><div class="buckle"></div></div>
            <div class="legs"><div class="leg"></div><div class="leg"></div></div>
          </div>
          <span class="skin-name">Blue</span><span class="rarity">COMMON OUTFIT</span>
        </button>

        <button class="skin-card" data-skin="Clowner">
          <div class="figure clowner">
            <div class="clown-hat"></div><div class="hair"></div>
            <div class="head"><div class="eye"></div><div class="eye right"></div><div class="clown-cheek left"></div><div class="clown-cheek right"></div><div class="clown-nose"></div><div class="mouth"></div></div>
            <div class="arm left"></div><div class="arm right"></div><div class="sleeve left"></div><div class="sleeve right"></div>
            <div class="body"><div class="belt"></div><div class="buckle"></div></div>
            <div class="legs"><div class="leg"></div><div class="leg"></div></div>
          </div>
          <span class="skin-name">Clowner 🤡</span><span class="rarity">UNCOMMON OUTFIT</span>
        </button>
      </div>

      <div class="preview">
        <div id="previewFigure" class="figure">
          <div class="clown-hat"></div><div class="hair"></div>
          <div class="head"><div class="eye"></div><div class="eye right"></div><div class="clown-cheek left"></div><div class="clown-cheek right"></div><div class="clown-nose"></div><div class="mouth"></div></div>
          <div class="arm left"></div><div class="arm right"></div><div class="sleeve left"></div><div class="sleeve right"></div>
          <div class="body"><div class="belt"></div><div class="buckle"></div></div>
          <div class="legs"><div class="leg"></div><div class="leg"></div></div>
        </div>
        <div class="platform"></div>
        <div id="previewName" class="preview-name">Blue</div>
        <div id="previewRarity" class="preview-rarity">COMMON OUTFIT</div>
        <button id="equip" disabled>EQUIPPED</button>
        <div id="lockerNotice">Blue is equipped.</div>
      </div>
    </div>
  </section>
</main>

<main id="shop" class="screen">
  <section class="page"><h1>ITEM SHOP</h1><p class="sub">The shop isn't available yet.</p><button class="primary" onclick="showTab('lobby')">BACK TO LOBBY</button></section>
</main>

<main id="career" class="screen">
  <section class="page"><h1>CAREER</h1><p class="sub">PLAYER: <span id="careerUser"></span></p><p class="sub">OUTFIT: <span id="careerSkin"></span></p><button class="primary" onclick="showTab('lobby')">BACK TO LOBBY</button></section>
</main>
</div>

<script>
const ACCOUNT_KEY = "season1_demo_accounts_v1";
const SESSION_KEY = "season1_demo_session_v1";

let authMode = "login";
let currentUser = null;
let selectedMode = "SOLO";
let selectedSkin = "Blue";
let equippedSkin = "Blue";
let isMatchmaking = false;

const $ = id => document.getElementById(id);

function getAccounts() {
  try {
    return JSON.parse(localStorage.getItem(ACCOUNT_KEY) || "{}");
  } catch {
    return {};
  }
}

function saveAccounts(accounts) {
  localStorage.setItem(ACCOUNT_KEY, JSON.stringify(accounts));
}

// Usernames are case-insensitive: "Prem" and "prem" count as the same name.
function usernameKey(name) {
  return name.trim().toLowerCase();
}

function setAuthMode(mode) {
  authMode = mode;
  $("loginTab").classList.toggle("active", mode === "login");
  $("signinTab").classList.toggle("active", mode === "signin");
  $("authSubmit").textContent = mode === "login" ? "CREATE ACCOUNT" : "SIGN IN";
  $("username").placeholder = mode === "login" ? "Choose a username" : "Your username";
  $("password").autocomplete = mode === "login" ? "new-password" : "current-password";
  $("authMessage").textContent = "";
}

$("loginTab").addEventListener("click", () => setAuthMode("login"));
$("signinTab").addEventListener("click", () => setAuthMode("signin"));

async function hashPassword(password) {
  // Hashing is better than storing plain text, but browser-only storage is
  // still not a secure replacement for a real authentication server.
  if (window.crypto && crypto.subtle) {
    const data = new TextEncoder().encode(password);
    const hash = await crypto.subtle.digest("SHA-256", data);
    return Array.from(new Uint8Array(hash))
      .map(b => b.toString(16).padStart(2, "0")).join("");
  }
  // Fallback for environments without Web Crypto. Demo use only.
  return "demo-" + btoa(unescape(encodeURIComponent(password)));
}

$("authForm").addEventListener("submit", async event => {
  event.preventDefault();

  const username = $("username").value.trim();
  const password = $("password").value;

  if (!/^[A-Za-z0-9_]{3,20}$/.test(username)) {
    $("authMessage").textContent =
      "Username must be 3–20 characters: letters, numbers, and underscores only.";
    return;
  }

  if (password.length < 6) {
    $("authMessage").textContent = "Password must be at least 6 characters.";
    return;
  }

  const key = usernameKey(username);
  const accounts = getAccounts();
  const passHash = await hashPassword(password);

  if (authMode === "login") {
    if (accounts[key]) {
      $("authMessage").textContent =
        "That username is already taken. Choose another one or sign in.";
      return;
    }

    // Volxer alone starts with Clowner. Every other new account starts with Blue.
    accounts[key] = {
      username,
      passwordHash: passHash,
      skin: key === "volxer" ? "Clowner" : "Blue"
    };

    saveAccounts(accounts);
    signInUser(key);
    return;
  }

  if (!accounts[key]) {
    $("authMessage").textContent = "Account not found. Create an account first.";
    return;
  }

  if (accounts[key].passwordHash !== passHash) {
    $("authMessage").textContent = "Incorrect password. Try again.";
    return;
  }

  signInUser(key);
});

function signInUser(key) {
  const accounts = getAccounts();
  const account = accounts[key];
  if (!account) return;

  currentUser = key;
  selectedSkin = account.skin || (key === "volxer" ? "Clowner" : "Blue");
  equippedSkin = selectedSkin;

  // Keep the selected account logged in within this browser.
  sessionStorage.setItem(SESSION_KEY, key);

  $("authScreen").classList.add("hidden");
  $("game").classList.remove("hidden");
  $("headerUser").textContent = account.username;
  $("careerUser").textContent = account.username;
  updateSkinUI();
  showTab("lobby");
}

function logout() {
  sessionStorage.removeItem(SESSION_KEY);
  currentUser = null;
  isMatchmaking = false;
  $("game").classList.add("hidden");
  $("authScreen").classList.remove("hidden");
  $("password").value = "";
  $("authMessage").textContent = "";
  $("play").textContent = "PLAY";
  $("play").disabled = false;
  $("unready").classList.add("hidden");
  $("status").textContent = "● ONLINE · IN LOBBY";
  setAuthMode("signin");
}

$("logout").addEventListener("click", logout);

function showTab(tabName) {
  document.querySelectorAll(".screen").forEach(s => s.classList.remove("active"));
  $(tabName).classList.add("active");
  document.querySelectorAll("nav button").forEach(b => {
    b.classList.toggle("active", b.dataset.tab === tabName);
  });
}

document.querySelectorAll("nav button").forEach(button => {
  button.addEventListener("click", () => showTab(button.dataset.tab));
});

document.querySelectorAll(".mode").forEach(button => {
  button.addEventListener("click", () => {
    selectedMode = button.dataset.mode;
    document.querySelectorAll(".mode").forEach(b => b.classList.toggle("selected", b === button));
    $("modeDescription").textContent = selectedMode === "SOLO"
      ? "Every player for themselves. Be the last one standing."
      : "Team up with one friend and fight to be the last duo.";
    if (isMatchmaking) $("status").textContent = "● MATCHMAKING · " + selectedMode;
  });
});

$("play").addEventListener("click", () => {
  isMatchmaking = true;
  $("play").textContent = "MATCHMAKING...";
  $("play").disabled = true;
  $("unready").classList.remove("hidden");
  $("status").textContent = "● MATCHMAKING · " + selectedMode;
});

$("unready").addEventListener("click", () => {
  isMatchmaking = false;
  $("play").textContent = "PLAY";
  $("play").disabled = false;
  $("unready").classList.add("hidden");
  $("status").textContent = "● ONLINE · IN LOBBY";
});

document.querySelectorAll(".skin-card").forEach(card => {
  card.addEventListener("click", () => {
    selectedSkin = card.dataset.skin;
    updateSkinUI();
  });
});

$("equip").addEventListener("click", () => {
  if (!currentUser) return;

  // Volxer is the special account with Clowner permanently assigned.
  if (currentUser === "volxer") {
    equippedSkin = "Clowner";
    selectedSkin = "Clowner";
    $("lockerNotice").textContent = "Clowner is exclusive to the Volxer account!";
  } else {
    equippedSkin = selectedSkin;
    $("lockerNotice").textContent = equippedSkin + " equipped!";
  }

  const accounts = getAccounts();
  accounts[currentUser].skin = equippedSkin;
  saveAccounts(accounts);
  updateSkinUI();
});

function updateSkinUI() {
  const isVolxer = currentUser === "volxer";

  if (isVolxer) {
    equippedSkin = "Clowner";
    selectedSkin = "Clowner";
  }

  $("lobbyFigure").classList.toggle("clowner", equippedSkin === "Clowner");
  $("lobbySkinName").textContent = equippedSkin;
  $("careerSkin").textContent = equippedSkin;

  $("previewFigure").classList.toggle("clowner", selectedSkin === "Clowner");
  $("previewName").textContent = selectedSkin;
  $("previewRarity").textContent =
    selectedSkin === "Clowner" ? "UNCOMMON OUTFIT" : "COMMON OUTFIT";

  document.querySelectorAll(".skin-card").forEach(card => {
    const skin = card.dataset.skin;
    card.classList.toggle("selected", skin === selectedSkin);
    card.classList.toggle("equipped", skin === equippedSkin);
  });

  if (isVolxer) {
    $("equip").disabled = true;
    $("equip").textContent = "VOLXER EXCLUSIVE";
    $("lockerNotice").textContent = "Clowner is exclusive to the Volxer account!";
  } else if (selectedSkin === equippedSkin) {
    $("equip").disabled = true;
    $("equip").textContent = "EQUIPPED";
    $("lockerNotice").textContent = equippedSkin + " is equipped.";
  } else {
    $("equip").disabled = false;
    $("equip").textContent = "EQUIP " + selectedSkin.toUpperCase();
    $("lockerNotice").textContent = "Choose EQUIP to wear " + selectedSkin + ".";
  }
}

// Restore the current session if the page reloads in the same tab.
const savedSession = sessionStorage.getItem(SESSION_KEY);
if (savedSession && getAccounts()[savedSession]) {
  signInUser(savedSession);
}
</script>
</body>
</html>

Game Source: Fortnite Season 1

Creator: StormPanda35

Libraries: none

Complexity: complex (499 lines, 23.0 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: fortnite-season-1-stormpanda35" to link back to the original. Then publish at arcadelab.ai/publish.